├── .gitignore ├── .travis.yml ├── LICENSE ├── NOTES.txt ├── README.md ├── pom.xml └── src ├── dependency-check └── suppressions.xml ├── main ├── java │ └── org │ │ └── microbean │ │ └── kubernetes │ │ └── controller │ │ ├── AbstractEvent.java │ │ ├── Controller.java │ │ ├── Event.java │ │ ├── EventCache.java │ │ ├── EventDistributor.java │ │ ├── EventQueue.java │ │ ├── EventQueueCollection.java │ │ ├── HasMetadatas.java │ │ ├── Reflector.java │ │ ├── ResourceTrackingEventQueueConsumer.java │ │ ├── SynchronizationEvent.java │ │ └── package-info.java └── javadoc │ ├── css │ └── stylesheet.css │ └── overview.html ├── site ├── markdown │ └── index.md.vm ├── resources │ └── css │ │ └── site.css └── site.xml ├── spotbugs └── exclude.xml └── test ├── java └── org │ └── microbean │ └── kubernetes │ └── controller │ ├── TestInterruptionBehavior.java │ └── TestReflectorBasics.java └── resources └── kubernetes └── configMap00.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *~ 3 | nbactions.xml 4 | src/site/markdown/*.html 5 | target/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | cache: 3 | directories: 4 | - "${HOME}/.m2/repository" 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTES.txt: -------------------------------------------------------------------------------- 1 | A SharedIndexInformer creates a new Indexer. You can get it, but you 2 | can't set it 3 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/shared_informer.go#L83). 4 | 5 | The Indexer creates a new ThreadSafeStore; you can't change this 6 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/store.go#L241). 7 | 8 | The ThreadSafeStore is paired with user-supplied Indexers 9 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/store.go#L241). 10 | 11 | An Indexers (plural English, singular object) is a map of strings to 12 | indexing functions 13 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/index.go#L84). 14 | 15 | SharedIndexInformer.Run() creates a new DeltaFIFO with the new Indexer 16 | (https://github.com/kubernetes/client-go/blob/master/tools/cache/shared_informer.go#L192). 17 | 18 | The Indexer supplied to the new DeltaFIFO functions as its KeyLister 19 | and its KeyGetter. 20 | 21 | This means the ThreadSafeStore that is created by the Indexer that 22 | belongs to the SharedIndexInformer is where known objects live. 23 | 24 | The HandleDeltas function that the SharedIndexInformer uses to process 25 | things updates the Indexer 26 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/shared_informer.go#L355). 27 | 28 | This means that the `ThreadSafeStore` will never be cleaned out. 29 | Specifically, Delete will not be called on it unless there's a 30 | corresponding delete in Kubernetes itself. 31 | 32 | --- 33 | 34 | You might be tempted to think that we're storing too many redundant 35 | copies of the state of any given Kubernetes resource. But see 36 | https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/delta_fifo.go#L320. 37 | 38 | In other words, an event comes in with its state. The Go code stores 39 | it in a queue. So add, modify, modify: there will be three copies of 40 | the object in that queue. 41 | 42 | But that queue is constantly being emptied. In the Go code, the 43 | ProcessFunc is usually HandleDeltas, and HandleDeltas takes the Delta 44 | (the event) from the queue, grabs its object (the resource, one of 45 | several possibly redundant representations) and then adds it to the 46 | knownObjects (the Indexer) 47 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/shared_informer.go#L350-L370). 48 | 49 | So we *are* in fact being faithful to the Go code: we have a map of 50 | event queues indexed by key (just as the Go code has a map of queues 51 | containing Deltas indexed by key). 52 | 53 | The Go code indicates that the cache (the knownObjects, the Indexer) 54 | may be fresher than an event 55 | (https://github.com/kubernetes/client-go/blob/03bfb9bdcfe5482795b999f39ca3ed9ad42ce5bb/tools/cache/shared_informer.go#L36-L37): 56 | that is, if you ask the cache (the knownObjects, the Indexer) for a 57 | pod named foo/bar, the representation you get back may be slightly 58 | "newer" than the representation in the event that caused you to take 59 | action in the first place. This is because in HandleDeltas (see the 60 | link) the knownObjects/cache/Indexer is mutated first, then the event 61 | is distributed. Equivalently, in the Java code here, it's because 62 | knownObjects is mutated under its own lock first, then the lock is 63 | released, then the event is broadcast. 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microBean Kubernetes Controller 2 | 3 | [![Build Status](https://travis-ci.org/microbean/microbean-kubernetes-controller.svg?branch=master)](https://travis-ci.org/microbean/microbean-kubernetes-controller) 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-kubernetes-controller/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-kubernetes-controller) 5 | 6 | The `microbean-kubernetes-controller` project contains an idiomatic 7 | Java implementation of the [Kubernetes controller 8 | framework][tools-cache]. This lets you write 9 | _controllers_—sometimes known as _operators_—in Java, 10 | using the [fabric8 Kubernetes client][kubernetes-client]. 11 | 12 | There is a [hopefully useful eleven-part series of blog posts on the 13 | subject][blog]. 14 | 15 | [kubernetes-client]: https://github.com/fabric8io/kubernetes-client/blob/master/README.md 16 | [tools-cache]: https://github.com/kubernetes/kubernetes/blob/v1.9.0/staging/src/k8s.io/client-go/tools/cache/ 17 | [blog]: https://lairdnelson.wordpress.com/2018/01/07/understanding-kubernetes-tools-cache-package-part-0/ 18 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.microbean 6 | microbean-kubernetes-controller 7 | 0.3.1-SNAPSHOT 8 | 9 | 10 | org.microbean 11 | microbean-pluginmanagement-pom 12 | 17 13 | 14 | 15 | 16 | microBean Kubernetes Controller 17 | Tools for writing Kubernetes controllers in Java. 18 | 2017 19 | https://microbean.github.io/${project.artifactId} 20 | 21 | 22 | scm:git:git@github.com:microbean/${project.artifactId}.git 23 | scm:git:git@github.com:microbean/${project.artifactId}.git 24 | https://github.com/microbean/${project.artifactId}/ 25 | HEAD 26 | 27 | 28 | 29 | Github 30 | https://github.com/microbean/${project.artifactId}/issues/ 31 | 32 | 33 | 34 | 35 | 36 | 37 | com.fasterxml.jackson.core 38 | jackson-annotations 39 | 2.12.1 40 | jar 41 | 42 | 43 | 44 | com.fasterxml.jackson.core 45 | jackson-core 46 | 2.12.1 47 | jar 48 | 49 | 50 | 51 | com.fasterxml.jackson.core 52 | jackson-databind 53 | 2.12.1 54 | jar 55 | 56 | 57 | 58 | com.fasterxml.jackson.dataformat 59 | jackson-dataformat-yaml 60 | 2.12.1 61 | jar 62 | 63 | 64 | 65 | com.fasterxml.jackson.module 66 | jackson-module-jaxb-annotations 67 | 2.12.1 68 | jar 69 | 70 | 71 | 72 | com.squareup.okhttp3 73 | logging-interceptor 74 | 4.9.0 75 | jar 76 | 77 | 78 | 79 | com.squareup.okhttp3 80 | okhttp 81 | 4.9.0 82 | jar 83 | 84 | 85 | 86 | io.fabric8 87 | kubernetes-client 88 | 4.11.1 89 | jar 90 | 91 | 92 | org.slf4j 93 | jul-to-slf4j 94 | 95 | 96 | 97 | 98 | 99 | io.fabric8 100 | kubernetes-model 101 | 4.11.1 102 | jar 103 | 104 | 105 | javax.validation 106 | validation-api 107 | 108 | 109 | 110 | 111 | 112 | net.jcip 113 | jcip-annotations 114 | 1.0 115 | jar 116 | 117 | 118 | 119 | org.microbean 120 | microbean-development-annotations 121 | 0.2.3 122 | jar 123 | 124 | 125 | 126 | org.slf4j 127 | slf4j-simple 128 | 1.7.30 129 | jar 130 | 131 | 132 | 133 | org.yaml 134 | snakeyaml 135 | 1.27 136 | jar 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | junit 150 | junit 151 | jar 152 | test 153 | 154 | 155 | 156 | org.slf4j 157 | slf4j-simple 158 | jar 159 | test 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | io.fabric8 168 | kubernetes-client 169 | jar 170 | compile 171 | 172 | 173 | 174 | io.fabric8 175 | kubernetes-model 176 | jar 177 | compile 178 | 179 | 180 | 181 | net.jcip 182 | jcip-annotations 183 | jar 184 | compile 185 | 186 | 187 | 188 | org.microbean 189 | microbean-development-annotations 190 | jar 191 | compile 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | com.github.spotbugs 205 | spotbugs-maven-plugin 206 | 4.2.0 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | maven-surefire-plugin 216 | 217 | 218 | ${skipClusterTests} 219 | ${project.build.directory} 220 | ${org.slf4j.simpleLogger.log.io.fabric8} 221 | 222 | 223 | 224 | 225 | 226 | com.github.github 227 | site-maven-plugin 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | https://microbean.github.io/microbean-kubernetes/apidocs/,https://microbean.github.io/microbean-development-annotations/apidocs/,http://jcip.net/annotations/doc/,https://static.javadoc.io/io.fabric8/kubernetes-client/4.1.1,https://static.javadoc.io/io.fabric8/kubernetes-model/4.1.1,https://square.github.io/okhttp/3.x/okhttp/ 237 | 238 | true 239 | 240 | debug 241 | 242 | 243 | 244 | 245 | 246 | 247 | maven-javadoc-plugin 248 | 249 | 250 | 251 | javadoc-no-fork 252 | test-javadoc-no-fork 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /src/dependency-check/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | ^io\.fabric8:kubernetes-client:.*$ 8 | cpe:/a:kubernetes:kubernetes 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/AbstractEvent.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.Serializable; // for javadoc only 20 | 21 | import java.util.EventObject; 22 | import java.util.Objects; 23 | 24 | import io.fabric8.kubernetes.api.model.HasMetadata; 25 | 26 | /** 27 | * An {@code abstract} {@link EventObject} that represents another 28 | * event that has occurred to a Kubernetes resource, usually as found 29 | * in an {@link EventCache} implementation. 30 | * 31 | * @param a type of Kubernetes resource 32 | * 33 | * @author Laird Nelson 35 | * 36 | * @see EventCache 37 | */ 38 | public abstract class AbstractEvent extends EventObject { 39 | 40 | 41 | /* 42 | * Static fields. 43 | */ 44 | 45 | 46 | /** 47 | * The version of this class for {@linkplain Serializable 48 | * serialization purposes}. 49 | * 50 | * @see Serializable 51 | */ 52 | private static final long serialVersionUID = 1L; 53 | 54 | 55 | /* 56 | * Instance fields. 57 | */ 58 | 59 | 60 | /** 61 | * The key that identifies this {@link AbstractEvent}'s {@linkplain 62 | * #getResource() resource} only when its final state is 63 | * unknown. 64 | * 65 | *

This field can be—and often is—{@code null}.

66 | * 67 | * @see #getKey() 68 | * 69 | * @see #setKey(Object) 70 | */ 71 | private volatile Object key; 72 | 73 | /** 74 | * The {@link Type} describing the type of this {@link 75 | * AbstractEvent}. 76 | * 77 | *

This field is never {@code null}.

78 | * 79 | * @see #getType() 80 | */ 81 | private final Type type; 82 | 83 | /** 84 | * A Kubernetes resource representing the prior state of 85 | * the resource returned by this {@link AbstractEvent}'s {@link 86 | * #getResource()} method. 87 | * 88 | *

This field may be {@code null}.

89 | * 90 | *

The prior state of a given Kubernetes resource is often not 91 | * known, so this field is often {@code null}.

92 | * 93 | * @see #getResource() 94 | */ 95 | private final T priorResource; 96 | 97 | /** 98 | * A Kubernetes resource representing its state at the time of this 99 | * event. 100 | * 101 | *

This field is never {@code null}.

102 | * 103 | * @see #getResource() 104 | */ 105 | private final T resource; 106 | 107 | 108 | /* 109 | * Constructors. 110 | */ 111 | 112 | 113 | /** 114 | * A private zero-argument constructor to reinforce to readers and 115 | * subclassers alike that this is not only an {@code abstract} 116 | * class, but one with a finite, known number of subclasses. 117 | * 118 | * @exception NullPointerException when invoked 119 | * 120 | * @see #AbstractEvent(Object, Type, HasMetadata, HasMetadata) 121 | */ 122 | private AbstractEvent() { 123 | this(null, null, null, null); 124 | } 125 | 126 | /** 127 | * Creates a new {@link AbstractEvent}. 128 | * 129 | * @param source the creator; must not be {@code null} 130 | * 131 | * @param type the {@link Type} of this {@link AbstractEvent}; must not be 132 | * {@code null} 133 | * 134 | * @param priorResource a {@link HasMetadata} representing the 135 | * prior state of the {@linkplain #getResource() Kubernetes 136 | * resource this AbstractEvent primarily concerns}; may 137 | * be—and often is—null 138 | * 139 | * @param resource a {@link HasMetadata} representing a Kubernetes 140 | * resource; must not be {@code null} 141 | * 142 | * @exception NullPointerException if {@code source}, {@code type} 143 | * or {@code resource} is {@code null} 144 | * 145 | * @exception IllegalStateException if somehow a subclass invoking 146 | * this constructor manages illicitly to be neither an instance of 147 | * {@link Event} nor an instance of {@link SynchronizationEvent} 148 | * 149 | * @see Type 150 | * 151 | * @see EventObject#getSource() 152 | */ 153 | AbstractEvent(final Object source, final Type type, final T priorResource, final T resource) { 154 | super(source); 155 | if (!(Event.class.isAssignableFrom(this.getClass()) || SynchronizationEvent.class.isAssignableFrom(this.getClass()))) { 156 | throw new IllegalStateException("Unexpected subclass"); 157 | } 158 | this.type = Objects.requireNonNull(type); 159 | this.priorResource = priorResource; 160 | this.resource = Objects.requireNonNull(resource); 161 | } 162 | 163 | 164 | /* 165 | * Instance methods. 166 | */ 167 | 168 | 169 | /** 170 | * Returns a {@link Type} representing the type of this {@link 171 | * AbstractEvent}. 172 | * 173 | *

This method never returns {@code null}.

174 | * 175 | * @return a non-{@code null} {@link Type} 176 | * 177 | * @see Type 178 | */ 179 | public final Type getType() { 180 | return this.type; 181 | } 182 | 183 | /** 184 | * Returns a {@link HasMetadata} representing the prior 185 | * state of the Kubernetes resource this {@link AbstractEvent} 186 | * primarily concerns. 187 | * 188 | *

This method may return {@code null}, and often does.

189 | * 190 | *

The prior state of a Kubernetes resource is often not known at 191 | * {@link AbstractEvent} construction time so it is common for this method 192 | * to return {@code null}. 193 | * 194 | * @return a {@link HasMetadata} representing the prior 195 | * state of the {@linkplain #getResource() Kubernetes resource 196 | * this AbstractEvent primarily concerns}, or {@code null} 197 | * 198 | * @see #getResource() 199 | */ 200 | public final T getPriorResource() { 201 | return this.priorResource; 202 | } 203 | 204 | /** 205 | * Returns a {@link HasMetadata} representing the Kubernetes 206 | * resource this {@link AbstractEvent} concerns. 207 | * 208 | *

This method never returns {@code null}.

209 | * 210 | * @return a non-{@code null} Kubernetes resource 211 | */ 212 | public final T getResource() { 213 | return this.resource; 214 | } 215 | 216 | /** 217 | * Returns {@code true} if this {@link AbstractEvent}'s {@linkplain 218 | * #getResource() resource} is an accurate representation of its 219 | * last known state. 220 | * 221 | *

This should only return {@code true} for some, but not all, 222 | * deletion scenarios. Any other behavior should be considered to 223 | * be an error.

224 | * 225 | * @return {@code true} if this {@link AbstractEvent}'s {@linkplain 226 | * #getResource() resource} is an accurate representation of its 227 | * last known state; {@code false} otherwise 228 | */ 229 | public final boolean isFinalStateKnown() { 230 | return this.key == null; 231 | } 232 | 233 | /** 234 | * Sets the key identifying the Kubernetes resource this {@link 235 | * AbstractEvent} describes. 236 | * 237 | * @param key the new key; may be {@code null} 238 | * 239 | * @see #getKey() 240 | */ 241 | final void setKey(final Object key) { 242 | this.key = key; 243 | } 244 | 245 | /** 246 | * Returns a key that can be used to unambiguously identify this 247 | * {@link AbstractEvent}'s {@linkplain #getResource() resource}. 248 | * 249 | *

This method may return {@code null} in exceptional cases, but 250 | * normally does not.

251 | * 252 | *

Overrides of this method must not return {@code null} except 253 | * in exceptional cases.

254 | * 255 | *

The default implementation of this method returns the return 256 | * value of the {@link HasMetadatas#getKey(HasMetadata)} method.

257 | * 258 | * @return a key for this {@link AbstractEvent}, or {@code null} 259 | * 260 | * @see HasMetadatas#getKey(HasMetadata) 261 | */ 262 | public Object getKey() { 263 | Object returnValue = this.key; 264 | if (returnValue == null) { 265 | returnValue = HasMetadatas.getKey(this.getResource()); 266 | } 267 | return returnValue; 268 | } 269 | 270 | /** 271 | * Returns a hashcode for this {@link AbstractEvent}. 272 | * 273 | * @return a hashcode for this {@link AbstractEvent} 274 | */ 275 | @Override 276 | public int hashCode() { 277 | int hashCode = 37; 278 | 279 | final Object source = this.getSource(); 280 | int c = source == null ? 0 : source.hashCode(); 281 | hashCode = hashCode * 17 + c; 282 | 283 | final Object key = this.getKey(); 284 | c = key == null ? 0 : key.hashCode(); 285 | hashCode = hashCode * 17 + c; 286 | 287 | final Object type = this.getType(); 288 | c = type == null ? 0 : type.hashCode(); 289 | hashCode = hashCode * 17 + c; 290 | 291 | final Object resource = this.getResource(); 292 | c = resource == null ? 0 : resource.hashCode(); 293 | hashCode = hashCode * 17 + c; 294 | 295 | final Object priorResource = this.getPriorResource(); 296 | c = priorResource == null ? 0 : priorResource.hashCode(); 297 | hashCode = hashCode * 17 + c; 298 | 299 | return hashCode; 300 | } 301 | 302 | /** 303 | * Returns {@code true} if the supplied {@link Object} is also an 304 | * {@link AbstractEvent} and is equal in every respect to this one. 305 | * 306 | * @param other the {@link Object} to test; may be {@code null} in 307 | * which case {@code false} will be returned 308 | * 309 | * @return {@code true} if the supplied {@link Object} is also an 310 | * {@link AbstractEvent} and is equal in every respect to this one; {@code 311 | * false} otherwise 312 | */ 313 | @Override 314 | public boolean equals(final Object other) { 315 | if (other == this) { 316 | return true; 317 | } else if (other instanceof AbstractEvent) { 318 | 319 | final AbstractEvent her = (AbstractEvent)other; 320 | 321 | final Object source = this.getSource(); 322 | if (source == null) { 323 | if (her.getSource() != null) { 324 | return false; 325 | } 326 | } else if (!source.equals(her.getSource())) { 327 | return false; 328 | } 329 | 330 | final Object key = this.getKey(); 331 | if (key == null) { 332 | if (her.getKey() != null) { 333 | return false; 334 | } 335 | } else if (!key.equals(her.getKey())) { 336 | return false; 337 | } 338 | 339 | final Object type = this.getType(); 340 | if (type == null) { 341 | if (her.getType() != null) { 342 | return false; 343 | } 344 | } else if (!type.equals(her.getType())) { 345 | return false; 346 | } 347 | 348 | final Object resource = this.getResource(); 349 | if (resource == null) { 350 | if (her.getResource() != null) { 351 | return false; 352 | } 353 | } else if (!resource.equals(her.getResource())) { 354 | return false; 355 | } 356 | 357 | final Object priorResource = this.getPriorResource(); 358 | if (priorResource == null) { 359 | if (her.getPriorResource() != null) { 360 | return false; 361 | } 362 | } else if (!priorResource.equals(her.getPriorResource())) { 363 | return false; 364 | } 365 | 366 | 367 | return true; 368 | } else { 369 | return false; 370 | } 371 | } 372 | 373 | /** 374 | * Returns a {@link String} representation of this {@link AbstractEvent}. 375 | * 376 | *

This method never returns {@code null}.

377 | * 378 | *

Overrides of this method must not return {@code null}.

379 | * 380 | * @return a non-{@code null} {@link String} representation of this 381 | * {@link AbstractEvent} 382 | */ 383 | @Override 384 | public String toString() { 385 | final StringBuilder sb = new StringBuilder().append(this.getType()).append(": "); 386 | final Object priorResource = this.getPriorResource(); 387 | if (priorResource != null) { 388 | sb.append(priorResource).append(" --> "); 389 | } 390 | sb.append(this.getResource()); 391 | return sb.toString(); 392 | } 393 | 394 | 395 | /* 396 | * Inner and nested classes. 397 | */ 398 | 399 | 400 | /** 401 | * The type of an {@link AbstractEvent}. 402 | * 403 | * @author Laird Nelson 405 | */ 406 | public static enum Type { 407 | 408 | /** 409 | * A {@link Type} representing the addition of a resource. 410 | */ 411 | ADDITION, 412 | 413 | /** 414 | * A {@link Type} representing the modification of a resource. 415 | */ 416 | MODIFICATION, 417 | 418 | /** 419 | * A {@link Type} representing the deletion of a resource. 420 | */ 421 | DELETION 422 | 423 | } 424 | 425 | } 426 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/Controller.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.Closeable; 20 | import java.io.IOException; 21 | 22 | import java.time.Duration; 23 | 24 | import java.util.Map; 25 | import java.util.Objects; 26 | 27 | import java.util.concurrent.Future; 28 | import java.util.concurrent.ScheduledExecutorService; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | import java.util.function.Consumer; 32 | import java.util.function.Function; 33 | 34 | import java.util.logging.Level; 35 | import java.util.logging.Logger; 36 | 37 | import io.fabric8.kubernetes.api.model.HasMetadata; 38 | import io.fabric8.kubernetes.api.model.KubernetesResourceList; 39 | 40 | import io.fabric8.kubernetes.client.KubernetesClientException; // for javadoc only 41 | import io.fabric8.kubernetes.client.Watcher; 42 | 43 | import io.fabric8.kubernetes.client.dsl.Listable; 44 | import io.fabric8.kubernetes.client.dsl.VersionWatchable; 45 | 46 | import net.jcip.annotations.Immutable; 47 | import net.jcip.annotations.ThreadSafe; 48 | 49 | import org.microbean.development.annotation.Blocking; 50 | import org.microbean.development.annotation.NonBlocking; 51 | 52 | /** 53 | * A convenient combination of a {@link Reflector}, a {@link 54 | * VersionWatchable} and {@link Listable} implementation, an 55 | * (internal) {@link EventQueueCollection}, a {@link Map} of known 56 | * Kubernetes resources and an {@link EventQueue} {@link Consumer} 57 | * that {@linkplain Reflector#start() mirrors Kubernetes cluster 58 | * events} into a {@linkplain EventQueueCollection collection of 59 | * EventQueues} and {@linkplain 60 | * EventQueueCollection#start(Consumer) arranges for their consumption 61 | * and processing}. 62 | * 63 | *

{@linkplain #start() Starting} a {@link Controller} {@linkplain 64 | * EventQueueCollection#start(Consumer) starts the 65 | * Consumer} supplied at construction time, and 66 | * {@linkplain Reflector#start() starts the embedded 67 | * Reflector}. {@linkplain #close() Closing} a {@link 68 | * Controller} {@linkplain Reflector#close() closes its embedded 69 | * Reflector} and {@linkplain 70 | * EventQueueCollection#close() causes the Consumer 71 | * supplied at construction time to stop receiving 72 | * Events}.

73 | * 74 | *

Several {@code protected} methods in this class exist to make 75 | * customization easier; none require overriding and their default 76 | * behavior is usually just fine.

77 | * 78 | *

Thread Safety

79 | * 80 | *

Instances of this class are safe for concurrent use by multiple 81 | * threads.

82 | * 83 | *

Design Notes

84 | * 85 | *

This class loosely models a combination of a {@code 87 | * Controller} type and a {@code 89 | * SharedIndexInformer} type as found in {@code 91 | * controller.go} and {@code 93 | * shared_informer.go} respectively.

94 | * 95 | * @param a Kubernetes resource type 96 | * 97 | * @author Laird Nelson 99 | * 100 | * @see Reflector 101 | * 102 | * @see EventQueueCollection 103 | * 104 | * @see ResourceTrackingEventQueueConsumer 105 | * 106 | * @see #start() 107 | * 108 | * @see #close() 109 | */ 110 | @Immutable 111 | @ThreadSafe 112 | public class Controller implements Closeable { 113 | 114 | 115 | /* 116 | * Instance fields. 117 | */ 118 | 119 | 120 | /** 121 | * A {@link Logger} used by this {@link Controller}. 122 | * 123 | *

This field is never {@code null}.

124 | * 125 | * @see #createLogger() 126 | */ 127 | protected final Logger logger; 128 | 129 | /** 130 | * The {@link Reflector} used by this {@link Controller} to mirror 131 | * Kubernetes events. 132 | * 133 | *

This field is never {@code null}.

134 | */ 135 | private final Reflector reflector; 136 | 137 | /** 138 | * The {@link EventQueueCollection} used by the {@link #reflector 139 | * Reflector} and by the {@link Consumer} supplied at construction 140 | * time. 141 | * 142 | *

This field is never {@code null}.

143 | * 144 | * @see EventQueueCollection#add(Object, AbstractEvent.Type, 145 | * HasMetadata) 146 | * 147 | * @see EventQueueCollection#replace(Collection, Object) 148 | * 149 | * @see EventQueueCollection#synchronize() 150 | * 151 | * @see EventQueueCollection#start(Consumer) 152 | */ 153 | private final EventQueueCollection eventQueueCollection; 154 | 155 | private final EventQueueCollection.SynchronizationAwaitingPropertyChangeListener synchronizationAwaiter; 156 | 157 | /** 158 | * A {@link Consumer} of {@link EventQueue}s that processes {@link 159 | * Event}s produced, ultimately, by the {@link #reflector 160 | * Reflector}. 161 | * 162 | *

This field is never {@code null}.

163 | */ 164 | private final Consumer> eventQueueConsumer; 165 | 166 | 167 | /* 168 | * Constructors. 169 | */ 170 | 171 | 172 | /** 173 | * Creates a new {@link Controller} but does not {@linkplain 174 | * #start() start it}. 175 | * 176 | * @param a {@link Listable} and {@link VersionWatchable} that 177 | * will be used by the embedded {@link Reflector}; must not be 178 | * {@code null} 179 | * 180 | * @param operation a {@link Listable} and a {@link 181 | * VersionWatchable} that produces Kubernetes events; must not be 182 | * {@code null} 183 | * 184 | * @param eventQueueConsumer the {@link Consumer} that will process 185 | * each {@link EventQueue} as it becomes ready; must not be {@code 186 | * null} 187 | * 188 | * @exception NullPointerException if {@code operation} or {@code 189 | * eventQueueConsumer} is {@code null} 190 | * 191 | * @see #Controller(Listable, ScheduledExecutorService, Duration, 192 | * Map, Consumer) 193 | * 194 | * @see #start() 195 | */ 196 | @SuppressWarnings("rawtypes") 197 | public 198 | & VersionWatchable>> Controller(final X operation, 200 | final Consumer> eventQueueConsumer) { 201 | this(operation, null, null, null, eventQueueConsumer); 202 | } 203 | 204 | /** 205 | * Creates a new {@link Controller} but does not {@linkplain 206 | * #start() start it}. 207 | * 208 | * @param a {@link Listable} and {@link VersionWatchable} that 209 | * will be used by the embedded {@link Reflector}; must not be 210 | * {@code null} 211 | * 212 | * @param operation a {@link Listable} and a {@link 213 | * VersionWatchable} that produces Kubernetes events; must not be 214 | * {@code null} 215 | * 216 | * @param knownObjects a {@link Map} containing the last known state 217 | * of Kubernetes resources the embedded {@link EventQueueCollection} 218 | * is caching events for; may be {@code null} if this {@link 219 | * Controller} is not interested in tracking deletions of objects; 220 | * if non-{@code null} will be synchronized on by this 221 | * class during retrieval and traversal operations 222 | * 223 | * @param eventQueueConsumer the {@link Consumer} that will process 224 | * each {@link EventQueue} as it becomes ready; must not be {@code 225 | * null} 226 | * 227 | * @exception NullPointerException if {@code operation} or {@code 228 | * eventQueueConsumer} is {@code null} 229 | * 230 | * @see #Controller(Listable, ScheduledExecutorService, Duration, 231 | * Map, Consumer) 232 | * 233 | * @see #start() 234 | */ 235 | @SuppressWarnings("rawtypes") 236 | public 237 | & VersionWatchable>> Controller(final X operation, 239 | final Map knownObjects, 240 | final Consumer> eventQueueConsumer) { 241 | this(operation, null, null, knownObjects, eventQueueConsumer); 242 | } 243 | 244 | /** 245 | * Creates a new {@link Controller} but does not {@linkplain 246 | * #start() start it}. 247 | * 248 | * @param a {@link Listable} and {@link VersionWatchable} that 249 | * will be used by the embedded {@link Reflector}; must not be 250 | * {@code null} 251 | * 252 | * @param operation a {@link Listable} and a {@link 253 | * VersionWatchable} that produces Kubernetes events; must not be 254 | * {@code null} 255 | * 256 | * @param synchronizationInterval a {@link Duration} representing 257 | * the time in between one {@linkplain EventCache#synchronize() 258 | * synchronization operation} and another; may be {@code null} in 259 | * which case no synchronization will occur 260 | * 261 | * @param eventQueueConsumer the {@link Consumer} that will process 262 | * each {@link EventQueue} as it becomes ready; must not be {@code 263 | * null} 264 | * 265 | * @exception NullPointerException if {@code operation} or {@code 266 | * eventQueueConsumer} is {@code null} 267 | * 268 | * @see #Controller(Listable, ScheduledExecutorService, Duration, 269 | * Map, Consumer) 270 | * 271 | * @see #start() 272 | */ 273 | @SuppressWarnings("rawtypes") 274 | public 275 | & VersionWatchable>> Controller(final X operation, 277 | final Duration synchronizationInterval, 278 | final Consumer> eventQueueConsumer) { 279 | this(operation, null, synchronizationInterval, null, eventQueueConsumer); 280 | } 281 | 282 | /** 283 | * Creates a new {@link Controller} but does not {@linkplain 284 | * #start() start it}. 285 | * 286 | * @param a {@link Listable} and {@link VersionWatchable} that 287 | * will be used by the embedded {@link Reflector}; must not be 288 | * {@code null} 289 | * 290 | * @param operation a {@link Listable} and a {@link 291 | * VersionWatchable} that produces Kubernetes events; must not be 292 | * {@code null} 293 | * 294 | * @param synchronizationInterval a {@link Duration} representing 295 | * the time in between one {@linkplain EventCache#synchronize() 296 | * synchronization operation} and another; may be {@code null} in 297 | * which case no synchronization will occur 298 | * 299 | * @param knownObjects a {@link Map} containing the last known state 300 | * of Kubernetes resources the embedded {@link EventQueueCollection} 301 | * is caching events for; may be {@code null} if this {@link 302 | * Controller} is not interested in tracking deletions of objects; 303 | * if non-{@code null} will be synchronized on by this 304 | * class during retrieval and traversal operations 305 | * 306 | * @param eventQueueConsumer the {@link Consumer} that will process 307 | * each {@link EventQueue} as it becomes ready; must not be {@code 308 | * null} 309 | * 310 | * @exception NullPointerException if {@code operation} or {@code 311 | * eventQueueConsumer} is {@code null} 312 | * 313 | * @see #Controller(Listable, ScheduledExecutorService, Duration, 314 | * Map, Consumer) 315 | * 316 | * @see #start() 317 | */ 318 | @SuppressWarnings("rawtypes") 319 | public 320 | & VersionWatchable>> Controller(final X operation, 322 | final Duration synchronizationInterval, 323 | final Map knownObjects, 324 | final Consumer> eventQueueConsumer) { 325 | this(operation, null, synchronizationInterval, knownObjects, eventQueueConsumer); 326 | } 327 | 328 | /** 329 | * Creates a new {@link Controller} but does not {@linkplain 330 | * #start() start it}. 331 | * 332 | * @param a {@link Listable} and {@link VersionWatchable} that 333 | * will be used by the embedded {@link Reflector}; must not be 334 | * {@code null} 335 | * 336 | * @param operation a {@link Listable} and a {@link 337 | * VersionWatchable} that produces Kubernetes events; must not be 338 | * {@code null} 339 | * 340 | * @param synchronizationExecutorService the {@link 341 | * ScheduledExecutorService} that will be passed to the {@link 342 | * Reflector} constructor; may be {@code null} in which case a 343 | * default {@link ScheduledExecutorService} may be used instead 344 | * 345 | * @param synchronizationInterval a {@link Duration} representing 346 | * the time in between one {@linkplain EventCache#synchronize() 347 | * synchronization operation} and another; may be {@code null} in 348 | * which case no synchronization will occur 349 | * 350 | * @param knownObjects a {@link Map} containing the last known state 351 | * of Kubernetes resources the embedded {@link EventQueueCollection} 352 | * is caching events for; may be {@code null} if this {@link 353 | * Controller} is not interested in tracking deletions of objects; 354 | * if non-{@code null} will be synchronized on by this 355 | * class during retrieval and traversal operations 356 | * 357 | * @param eventQueueConsumer the {@link Consumer} that will process 358 | * each {@link EventQueue} as it becomes ready; must not be {@code 359 | * null} 360 | * 361 | * @exception NullPointerException if {@code operation} or {@code 362 | * eventQueueConsumer} is {@code null} 363 | * 364 | * @see #start() 365 | */ 366 | @SuppressWarnings("rawtypes") 367 | public 368 | & VersionWatchable>> Controller(final X operation, 370 | final ScheduledExecutorService synchronizationExecutorService, 371 | final Duration synchronizationInterval, 372 | final Map knownObjects, 373 | final Consumer> eventQueueConsumer) { 374 | this(operation, synchronizationExecutorService, synchronizationInterval, null, knownObjects, eventQueueConsumer); 375 | } 376 | 377 | /** 378 | * Creates a new {@link Controller} but does not {@linkplain 379 | * #start() start it}. 380 | * 381 | * @param a {@link Listable} and {@link VersionWatchable} that 382 | * will be used by the embedded {@link Reflector}; must not be 383 | * {@code null} 384 | * 385 | * @param operation a {@link Listable} and a {@link 386 | * VersionWatchable} that produces Kubernetes events; must not be 387 | * {@code null} 388 | * 389 | * @param synchronizationExecutorService the {@link 390 | * ScheduledExecutorService} that will be passed to the {@link 391 | * Reflector} constructor; may be {@code null} in which case a 392 | * default {@link ScheduledExecutorService} may be used instead 393 | * 394 | * @param synchronizationInterval a {@link Duration} representing 395 | * the time in between one {@linkplain EventCache#synchronize() 396 | * synchronization operation} and another; may be {@code null} in 397 | * which case no synchronization will occur 398 | * 399 | * @param errorHandler a {@link Function} that accepts a {@link 400 | * Throwable} and returns a {@link Boolean} indicating whether the 401 | * error was handled or not; used to handle truly unanticipated 402 | * errors from within a {@link ScheduledExecutorService} used 403 | * during {@linkplain EventCache#synchronize() synchronization} and 404 | * event consumption activities; may be {@code null} 405 | * 406 | * @param knownObjects a {@link Map} containing the last known state 407 | * of Kubernetes resources the embedded {@link EventQueueCollection} 408 | * is caching events for; may be {@code null} if this {@link 409 | * Controller} is not interested in tracking deletions of objects; 410 | * if non-{@code null} will be synchronized on by this 411 | * class during retrieval and traversal operations 412 | * 413 | * @param eventQueueConsumer the {@link Consumer} that will process 414 | * each {@link EventQueue} as it becomes ready; must not be {@code 415 | * null} 416 | * 417 | * @exception NullPointerException if {@code operation} or {@code 418 | * eventQueueConsumer} is {@code null} 419 | * 420 | * @see #start() 421 | */ 422 | @SuppressWarnings("rawtypes") 423 | public 424 | & VersionWatchable>> Controller(final X operation, 426 | final ScheduledExecutorService synchronizationExecutorService, 427 | final Duration synchronizationInterval, 428 | final Function errorHandler, 429 | final Map knownObjects, 430 | final Consumer> eventQueueConsumer) { 431 | super(); 432 | this.logger = this.createLogger(); 433 | if (this.logger == null) { 434 | throw new IllegalStateException("createLogger() == null"); 435 | } 436 | final String cn = this.getClass().getName(); 437 | final String mn = ""; 438 | if (this.logger.isLoggable(Level.FINER)) { 439 | this.logger.entering(cn, mn, new Object[] { operation, synchronizationExecutorService, synchronizationInterval, errorHandler, knownObjects, eventQueueConsumer }); 440 | } 441 | this.eventQueueConsumer = Objects.requireNonNull(eventQueueConsumer); 442 | this.eventQueueCollection = new ControllerEventQueueCollection(knownObjects, errorHandler, 16, 0.75f); 443 | this.synchronizationAwaiter = new EventQueueCollection.SynchronizationAwaitingPropertyChangeListener(); 444 | this.eventQueueCollection.addPropertyChangeListener(this.synchronizationAwaiter); 445 | this.reflector = new ControllerReflector(operation, synchronizationExecutorService, synchronizationInterval, errorHandler); 446 | if (this.logger.isLoggable(Level.FINER)) { 447 | this.logger.exiting(cn, mn); 448 | } 449 | } 450 | 451 | 452 | /* 453 | * Instance methods. 454 | */ 455 | 456 | 457 | /** 458 | * Returns a {@link Logger} for use by this {@link Controller}. 459 | * 460 | *

This method never returns {@code null}.

461 | * 462 | *

Overrides of this method must not return {@code null}.

463 | * 464 | * @return a non-{@code null} {@link Logger} 465 | */ 466 | protected Logger createLogger() { 467 | return Logger.getLogger(this.getClass().getName()); 468 | } 469 | 470 | /** 471 | * Blocks until the {@link EventQueueCollection} affiliated with 472 | * this {@link Controller} {@linkplain 473 | * EventQueueCollection#isSynchronized() has synchronized}. 474 | * 475 | * @exception InterruptedException if the current {@link Thread} was 476 | * interrupted 477 | */ 478 | @Blocking 479 | public final void awaitEventCacheSynchronization() throws InterruptedException { 480 | this.synchronizationAwaiter.await(); 481 | } 482 | 483 | /** 484 | * Blocks for the desired amount of time until the {@link 485 | * EventQueueCollection} affiliated with this {@link Controller} 486 | * {@linkplain EventQueueCollection#isSynchronized() has 487 | * synchronized} or the amount of time has elapsed. 488 | * 489 | * @param timeout the amount of time to wait 490 | * 491 | * @param timeUnit the {@link TimeUnit} designating the amount of 492 | * time to wait; must not be {@code null} 493 | * 494 | * @return {@code false} if the waiting time elapsed before the 495 | * event cache synchronized; {@code true} otherwise 496 | * 497 | * @exception InterruptedException if the current {@link Thread} was 498 | * interrupted 499 | * 500 | * @exception NullPointerException if {@code timeUnit} is {@code 501 | * null} 502 | * 503 | * @see EventQueueCollection.SynchronizationAwaitingPropertyChangeListener 504 | */ 505 | @Blocking 506 | public final boolean awaitEventCacheSynchronization(final long timeout, final TimeUnit timeUnit) throws InterruptedException { 507 | return this.synchronizationAwaiter.await(timeout, timeUnit); 508 | } 509 | 510 | /** 511 | * {@linkplain EventQueueCollection#start(Consumer) Starts the 512 | * embedded EventQueueCollection consumption machinery} 513 | * and then {@linkplain Reflector#start() starts the embedded 514 | * Reflector}. 515 | * 516 | * @exception IOException if {@link Reflector#start()} throws an 517 | * {@link IOException} 518 | * 519 | * @exception KubernetesClientException if the {@linkplain Reflector 520 | * embedded Reflector} could not be started 521 | * 522 | * @see EventQueueCollection#start(Consumer) 523 | * 524 | * @see Reflector#start() 525 | */ 526 | @NonBlocking 527 | public final void start() throws IOException { 528 | final String cn = this.getClass().getName(); 529 | final String mn = "start"; 530 | if (this.logger.isLoggable(Level.FINER)) { 531 | this.logger.entering(cn, mn); 532 | } 533 | 534 | // Start the consumer that is going to drain our associated 535 | // EventQueueCollection. 536 | if (this.logger.isLoggable(Level.INFO)) { 537 | this.logger.logp(Level.INFO, cn, mn, "Starting {0}", this.eventQueueConsumer); 538 | } 539 | final Future eventQueueConsumerTask = this.eventQueueCollection.start(this.eventQueueConsumer); 540 | assert eventQueueConsumerTask != null; 541 | 542 | // Start the Reflector--the machinery that is going to connect to 543 | // Kubernetes and "reflect" its (relevant) contents into the 544 | // EventQueueCollection. 545 | if (this.logger.isLoggable(Level.INFO)) { 546 | this.logger.logp(Level.INFO, cn, mn, "Starting {0}", this.reflector); 547 | } 548 | try { 549 | this.reflector.start(); 550 | } catch (final IOException | RuntimeException | Error reflectorStartFailure) { 551 | try { 552 | // TODO: this is problematic, I think; reflector.close() means 553 | // that (potentially) it will never be able to restart it. 554 | // The Go code appears to make some feints in the direction of 555 | // restartability, and then just basically gives up. I think 556 | // we can do better here. 557 | this.reflector.close(); 558 | } catch (final Throwable suppressMe) { 559 | reflectorStartFailure.addSuppressed(suppressMe); 560 | } 561 | eventQueueConsumerTask.cancel(true); 562 | assert eventQueueConsumerTask.isDone(); 563 | try { 564 | this.eventQueueCollection.close(); 565 | } catch (final Throwable suppressMe) { 566 | reflectorStartFailure.addSuppressed(suppressMe); 567 | } 568 | throw reflectorStartFailure; 569 | } 570 | 571 | if (this.logger.isLoggable(Level.FINER)) { 572 | this.logger.exiting(cn, mn); 573 | } 574 | } 575 | 576 | /** 577 | * {@linkplain Reflector#close() Closes the embedded 578 | * Reflector} and then {@linkplain 579 | * EventQueueCollection#close() closes the embedded 580 | * EventQueueCollection}, handling exceptions 581 | * appropriately. 582 | * 583 | * @exception IOException if the {@link Reflector} could not 584 | * {@linkplain Reflector#close() close} properly 585 | * 586 | * @see Reflector#close() 587 | * 588 | * @see EventQueueCollection#close() 589 | */ 590 | @Override 591 | public final void close() throws IOException { 592 | final String cn = this.getClass().getName(); 593 | final String mn = "close"; 594 | if (this.logger.isLoggable(Level.FINER)) { 595 | this.logger.entering(cn, mn); 596 | } 597 | Exception throwMe = null; 598 | try { 599 | if (this.logger.isLoggable(Level.INFO)) { 600 | this.logger.logp(Level.INFO, cn, mn, "Closing {0}", this.reflector); 601 | } 602 | this.reflector.close(); 603 | } catch (final Exception everything) { 604 | throwMe = everything; 605 | } 606 | 607 | try { 608 | if (this.logger.isLoggable(Level.INFO)) { 609 | this.logger.logp(Level.INFO, cn, mn, "Closing {0}", this.eventQueueCollection); 610 | } 611 | this.eventQueueCollection.close(); 612 | } catch (final RuntimeException | Error runtimeException) { 613 | if (throwMe == null) { 614 | throw runtimeException; 615 | } 616 | throwMe.addSuppressed(runtimeException); 617 | } 618 | 619 | if (throwMe instanceof IOException) { 620 | throw (IOException)throwMe; 621 | } else if (throwMe instanceof RuntimeException) { 622 | throw (RuntimeException)throwMe; 623 | } else if (throwMe != null) { 624 | throw new IllegalStateException(throwMe.getMessage(), throwMe); 625 | } 626 | 627 | if (this.logger.isLoggable(Level.FINER)) { 628 | this.logger.exiting(cn, mn); 629 | } 630 | } 631 | 632 | /** 633 | * Returns if the embedded {@link Reflector} should {@linkplain 634 | * Reflector#shouldSynchronize() synchronize}. 635 | * 636 | *

This implementation returns {@code true}.

637 | * 638 | * @return {@code true} if the embedded {@link Reflector} should 639 | * {@linkplain Reflector#shouldSynchronize() synchronize}; {@code 640 | * false} otherwise 641 | */ 642 | protected boolean shouldSynchronize() { 643 | final String cn = this.getClass().getName(); 644 | final String mn = "shouldSynchronize"; 645 | if (this.logger.isLoggable(Level.FINER)) { 646 | this.logger.entering(cn, mn); 647 | } 648 | final boolean returnValue = true; 649 | if (this.logger.isLoggable(Level.FINER)) { 650 | this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 651 | } 652 | return returnValue; 653 | } 654 | 655 | /** 656 | * Invoked after the embedded {@link Reflector} {@linkplain 657 | * Reflector#onClose() closes}. 658 | * 659 | *

This implementation does nothing.

660 | * 661 | * @see Reflector#close() 662 | * 663 | * @see Reflector#onClose() 664 | */ 665 | protected void onClose() { 666 | 667 | } 668 | 669 | /** 670 | * Returns a key that can be used to identify the supplied {@link 671 | * HasMetadata}. 672 | * 673 | *

This method never returns {@code null}.

674 | * 675 | *

Overrides of this method must not return {@code null}.

676 | * 677 | *

The default implementation of this method returns the return 678 | * value of invoking the {@link HasMetadatas#getKey(HasMetadata)} 679 | * method.

680 | * 681 | * @param resource the Kubernetes resource for which a key is 682 | * desired; must not be {@code null} 683 | * 684 | * @return a non-{@code null} key for the supplied {@link 685 | * HasMetadata} 686 | * 687 | * @exception NullPointerException if {@code resource} is {@code 688 | * null} 689 | */ 690 | protected Object getKey(final T resource) { 691 | final String cn = this.getClass().getName(); 692 | final String mn = "getKey"; 693 | if (this.logger.isLoggable(Level.FINER)) { 694 | this.logger.entering(cn, mn, resource); 695 | } 696 | final Object returnValue = HasMetadatas.getKey(Objects.requireNonNull(resource)); 697 | if (this.logger.isLoggable(Level.FINER)) { 698 | this.logger.exiting(cn, mn, returnValue); 699 | } 700 | return returnValue; 701 | } 702 | 703 | /** 704 | * Creates a new {@link Event} when invoked. 705 | * 706 | *

This method never returns {@code null}.

707 | * 708 | *

Overrides of this method must not return {@code null}.

709 | * 710 | *

Overrides of this method must return a new {@link Event} or 711 | * subclass with each invocation.

712 | * 713 | * @param source the source of the new {@link Event}; must not be 714 | * {@code null} 715 | * 716 | * @param eventType the {@link Event.Type} for the new {@link 717 | * Event}; must not be {@code null} 718 | * 719 | * @param resource the {@link HasMetadata} that the new {@link 720 | * Event} concerns; must not be {@code null} 721 | * 722 | * @return a new, non-{@code null} {@link Event} 723 | * 724 | * @exception NullPointerException if any of the parameters is 725 | * {@code null} 726 | */ 727 | protected Event createEvent(final Object source, final Event.Type eventType, final T resource) { 728 | final String cn = this.getClass().getName(); 729 | final String mn = "createEvent"; 730 | if (this.logger.isLoggable(Level.FINER)) { 731 | this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); 732 | } 733 | final Event returnValue = new Event<>(Objects.requireNonNull(source), Objects.requireNonNull(eventType), null, Objects.requireNonNull(resource)); 734 | if (this.logger.isLoggable(Level.FINER)) { 735 | this.logger.exiting(cn, mn, returnValue); 736 | } 737 | return returnValue; 738 | } 739 | 740 | /** 741 | * Creates a new {@link EventQueue} when invoked. 742 | * 743 | *

This method never returns {@code null}.

744 | * 745 | *

Overrides of this method must not return {@code null}.

746 | * 747 | *

Overrides of this method must return a new {@link EventQueue} 748 | * or subclass with each invocation.

749 | * 750 | * @param key the key to create the new {@link EventQueue} with; 751 | * must not be {@code null} 752 | * 753 | * @return a new, non-{@code null} {@link EventQueue} 754 | * 755 | * @exception NullPointerException if {@code key} is {@code null} 756 | */ 757 | protected EventQueue createEventQueue(final Object key) { 758 | final String cn = this.getClass().getName(); 759 | final String mn = "createEventQueue"; 760 | if (this.logger.isLoggable(Level.FINER)) { 761 | this.logger.entering(cn, mn, key); 762 | } 763 | final EventQueue returnValue = new EventQueue<>(key); 764 | if (this.logger.isLoggable(Level.FINER)) { 765 | this.logger.exiting(cn, mn, returnValue); 766 | } 767 | return returnValue; 768 | } 769 | 770 | 771 | /* 772 | * Inner and nested classes. 773 | */ 774 | 775 | 776 | /** 777 | * An {@link EventQueueCollection} that delegates its overridable 778 | * methods to their equivalents in the {@link Controller} class. 779 | * 780 | * @author Laird Nelson 782 | * 783 | * @see EventQueueCollection 784 | * 785 | * @see EventCache 786 | */ 787 | private final class ControllerEventQueueCollection extends EventQueueCollection { 788 | 789 | 790 | /* 791 | * Constructors. 792 | */ 793 | 794 | 795 | private ControllerEventQueueCollection(final Map knownObjects, 796 | final Function errorHandler, 797 | final int initialCapacity, 798 | final float loadFactor) { 799 | super(knownObjects, errorHandler, initialCapacity, loadFactor); 800 | } 801 | 802 | 803 | /* 804 | * Instance methods. 805 | */ 806 | 807 | 808 | @Override 809 | protected final Event createEvent(final Object source, final Event.Type eventType, final T resource) { 810 | return Controller.this.createEvent(source, eventType, resource); 811 | } 812 | 813 | @Override 814 | protected final EventQueue createEventQueue(final Object key) { 815 | return Controller.this.createEventQueue(key); 816 | } 817 | 818 | @Override 819 | protected final Object getKey(final T resource) { 820 | return Controller.this.getKey(resource); 821 | } 822 | 823 | } 824 | 825 | 826 | /** 827 | * A {@link Reflector} that delegates its overridable 828 | * methods to their equivalents in the {@link Controller} class. 829 | * 830 | * @author Laird Nelson 832 | * 833 | * @see Reflector 834 | */ 835 | private final class ControllerReflector extends Reflector { 836 | 837 | 838 | /* 839 | * Constructors. 840 | */ 841 | 842 | 843 | @SuppressWarnings("rawtypes") 844 | private & VersionWatchable>> ControllerReflector(final X operation, 845 | final ScheduledExecutorService synchronizationExecutorService, 846 | final Duration synchronizationInterval, final Function synchronizationErrorHandler) { 847 | super(operation, Controller.this.eventQueueCollection, synchronizationExecutorService, synchronizationInterval, synchronizationErrorHandler); 848 | } 849 | 850 | 851 | /* 852 | * Instance methods. 853 | */ 854 | 855 | 856 | /** 857 | * Invokes the {@link Controller#shouldSynchronize()} method and 858 | * returns its result. 859 | * 860 | * @return the result of invoking the {@link 861 | * Controller#shouldSynchronize()} method 862 | * 863 | * @see Controller#shouldSynchronize() 864 | */ 865 | @Override 866 | protected final boolean shouldSynchronize() { 867 | return Controller.this.shouldSynchronize(); 868 | } 869 | 870 | /** 871 | * Invokes the {@link Controller#onClose()} method. 872 | * 873 | * @see Controller#onClose() 874 | */ 875 | @Override 876 | protected final void onClose() { 877 | Controller.this.onClose(); 878 | } 879 | } 880 | 881 | } 882 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/Event.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.Serializable; // for javadoc only 20 | 21 | import java.util.EventObject; 22 | 23 | import io.fabric8.kubernetes.api.model.HasMetadata; 24 | 25 | /** 26 | * An {@link AbstractEvent} that represents another event that has 27 | * occurred to a Kubernetes resource, usually as found in an {@link 28 | * EventCache} implementation. 29 | * 30 | * @param a type of Kubernetes resource 31 | * 32 | * @author Laird Nelson 34 | * 35 | * @see EventCache 36 | */ 37 | public class Event extends AbstractEvent { 38 | 39 | 40 | /* 41 | * Static fields. 42 | */ 43 | 44 | 45 | /** 46 | * The version of this class for {@linkplain Serializable 47 | * serialization purposes}. 48 | * 49 | * @see Serializable 50 | */ 51 | private static final long serialVersionUID = 1L; 52 | 53 | 54 | /* 55 | * Constructors. 56 | */ 57 | 58 | 59 | /** 60 | * Creates a new {@link Event}. 61 | * 62 | * @param source the creator; must not be {@code null} 63 | * 64 | * @param type the {@link Type} of this {@link Event}; must not be 65 | * {@code null} 66 | * 67 | * @param priorResource a {@link HasMetadata} representing the 68 | * prior state of the {@linkplain #getResource() Kubernetes 69 | * resource this Event primarily concerns}; may 70 | * be—and often is—null 71 | * 72 | * @param resource a {@link HasMetadata} representing a Kubernetes 73 | * resource; must not be {@code null} 74 | * 75 | * @exception NullPointerException if {@code source}, {@code type} 76 | * or {@code resource} is {@code null} 77 | * 78 | * @see Type 79 | * 80 | * @see EventObject#getSource() 81 | */ 82 | public Event(final Object source, final Type type, final T priorResource, final T resource) { 83 | super(source, type, priorResource, resource); 84 | } 85 | 86 | 87 | /* 88 | * Instance methods. 89 | */ 90 | 91 | 92 | /** 93 | * Returns {@code true} if the supplied {@link Object} is also an 94 | * {@link Event} and is equal in every respect to this one. 95 | * 96 | * @param other the {@link Object} to test; may be {@code null} in 97 | * which case {@code false} will be returned 98 | * 99 | * @return {@code true} if the supplied {@link Object} is also an 100 | * {@link Event} and is equal in every respect to this one; {@code 101 | * false} otherwise 102 | */ 103 | @Override 104 | public boolean equals(final Object other) { 105 | if (other == this) { 106 | return true; 107 | } else if (other instanceof Event) { 108 | 109 | final boolean superEquals = super.equals(other); 110 | if (!superEquals) { 111 | return false; 112 | } 113 | 114 | return true; 115 | } else { 116 | return false; 117 | } 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/EventCache.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.util.Collection; 20 | 21 | import io.fabric8.kubernetes.api.model.HasMetadata; 22 | 23 | /** 24 | * A minimalistic interface indicating that its implementations cache 25 | * {@link Event}s representing Kubernetes resources. 26 | * 27 | *

Thread Safety

28 | * 29 | *

Instances of implementations of this interface must be 30 | * safe for concurrent usage by multiple {@link Thread}s.

31 | * 32 | * @param a type of Kubernetes resource 33 | * 34 | * @author Laird Nelson 36 | * 37 | * @see Event 38 | * 39 | * @see EventQueueCollection 40 | */ 41 | public interface EventCache { 42 | 43 | /** 44 | * Adds a new {@link Event} constructed out of the parameters 45 | * supplied to this method to this {@link EventCache} implementation 46 | * and returns the {@link Event} that was added. 47 | * 48 | *

Implementations of this method may return {@code null} to 49 | * indicate that for whatever reason no {@link Event} was actually 50 | * added.

51 | * 52 | * @param source the {@linkplain Event#getSource() source} of the 53 | * {@link Event} that will be created and added; must not be {@code 54 | * null} 55 | * 56 | * @param eventType the {@linkplain Event#getType() type} of {@link 57 | * Event} that will be created and added; must not be {@code null} 58 | * 59 | * @param resource the {@linkplain Event#getResource() resource} of 60 | * the {@link Event} that will be created and added must not be 61 | * {@code null} 62 | * 63 | * @return the {@link Event} that was created and added, or {@code 64 | * null} if no {@link Event} was actually added as a result of this 65 | * method's invocation 66 | * 67 | * @exception NullPointerException if any of the parameters is 68 | * {@code null} 69 | * 70 | * @see Event 71 | */ 72 | public Event add(final Object source, final Event.Type eventType, final T resource); 73 | 74 | /** 75 | * A "full replace" operation that atomically replaces all internal 76 | * state with new state derived from the supplied {@link Collection} 77 | * of resources. 78 | * 79 | * @param incomingResources the resources comprising the new state; 80 | * must not be {@code null}; must be synchronized 81 | * on when accessing 82 | * 83 | * @param resourceVersion the notional version of the supplied 84 | * {@link Collection}; may be {@code null}; often ignored by 85 | * implementations 86 | * 87 | * @exception NullPointerException if {@code incomingResources} is 88 | * {@code null} 89 | */ 90 | public void replace(final Collection incomingResources, final Object resourceVersion); 91 | 92 | /** 93 | * Synchronizes this {@link EventCache} implementation's state with 94 | * its downstream consumers, if any. 95 | * 96 | *

Not all {@link EventCache} implementations need support 97 | * synchronization. An implementation of this method that does 98 | * nothing is explicitly permitted.

99 | * 100 | *

Implementations of this method must expect to be called on a 101 | * fixed schedule.

102 | 103 | *

Design Notes

104 | * 105 | *

This method emulates the {@code 107 | * Resync} function in the Go code's {@code DeltaFifo} construct 108 | * Specifically, it is anticipated that an implementation of this 109 | * method that does not simply return will go through the internal 110 | * resources that this {@link EventCache} knows about, and, for each 111 | * that does not have an event queue set up for it 112 | * already—i.e. for each that is not currently being 113 | * processed— will fire a {@link SynchronizationEvent}. This 114 | * will have the effect of "heartbeating" the current desired state 115 | * of the system "downstream" to processors that may wish to alter 116 | * the actual state of the system to conform to it.

117 | * 118 | * @see SynchronizationEvent 119 | */ 120 | public void synchronize(); 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/EventDistributor.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.IOException; 20 | 21 | import java.time.Duration; 22 | import java.time.Instant; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Collection; 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | import java.util.Objects; 29 | 30 | import java.util.concurrent.BlockingQueue; 31 | import java.util.concurrent.CancellationException; 32 | import java.util.concurrent.ExecutionException; 33 | import java.util.concurrent.Executor; 34 | import java.util.concurrent.Executors; 35 | import java.util.concurrent.ExecutorService; 36 | import java.util.concurrent.Future; 37 | import java.util.concurrent.CopyOnWriteArrayList; 38 | import java.util.concurrent.LinkedBlockingQueue; 39 | import java.util.concurrent.ScheduledExecutorService; 40 | import java.util.concurrent.ScheduledThreadPoolExecutor; 41 | import java.util.concurrent.ThreadFactory; 42 | import java.util.concurrent.TimeUnit; 43 | 44 | import java.util.concurrent.atomic.AtomicInteger; 45 | 46 | import java.util.concurrent.locks.Lock; 47 | import java.util.concurrent.locks.ReadWriteLock; 48 | import java.util.concurrent.locks.ReentrantReadWriteLock; 49 | 50 | import java.util.function.Consumer; 51 | import java.util.function.Function; 52 | 53 | import java.util.logging.Level; 54 | import java.util.logging.Logger; 55 | 56 | import io.fabric8.kubernetes.api.model.HasMetadata; 57 | 58 | import net.jcip.annotations.Immutable; 59 | import net.jcip.annotations.GuardedBy; 60 | import net.jcip.annotations.ThreadSafe; 61 | 62 | /** 63 | * A {@link ResourceTrackingEventQueueConsumer} that {@linkplain 64 | * ResourceTrackingEventQueueConsumer#accept(EventQueue) consumes 65 | * EventQueue instances} by feeding each {@link 66 | * AbstractEvent} in the {@link EventQueue} being consumed to {@link 67 | * Consumer}s of {@link AbstractEvent}s that have been {@linkplain 68 | * #addConsumer(Consumer) registered}. 69 | * 70 | *

{@link EventDistributor} instances must be {@linkplain #close() 71 | * closed} and discarded after use.

72 | * 73 | * @param a type of Kubernetes resource 74 | * 75 | * @author Laird Nelson 77 | * 78 | * @see #addConsumer(Consumer) 79 | * 80 | * @see #removeConsumer(Consumer) 81 | * 82 | * @see ResourceTrackingEventQueueConsumer#accept(AbstractEvent) 83 | */ 84 | @Immutable 85 | @ThreadSafe 86 | public final class EventDistributor extends ResourceTrackingEventQueueConsumer implements AutoCloseable { 87 | 88 | 89 | /* 90 | * Instance fields. 91 | */ 92 | 93 | 94 | @GuardedBy("readLock && writeLock") 95 | private final Collection> pumps; 96 | 97 | @GuardedBy("readLock && writeLock") 98 | private final Collection> synchronizingPumps; 99 | 100 | private final Duration synchronizationInterval; 101 | 102 | private final Lock readLock; 103 | 104 | private final Lock writeLock; 105 | 106 | 107 | /* 108 | * Constructors. 109 | */ 110 | 111 | 112 | /** 113 | * Creates a new {@link EventDistributor}. 114 | * 115 | * @param knownObjects a mutable {@link Map} of Kubernetes resources 116 | * that contains or will contain Kubernetes resources known to this 117 | * {@link EventDistributor} and whatever mechanism (such as a {@link 118 | * Controller}) is feeding it; may be {@code null} 119 | * 120 | * @see #EventDistributor(Map, Duration) 121 | */ 122 | public EventDistributor(final Map knownObjects) { 123 | this(knownObjects, null); 124 | } 125 | 126 | /** 127 | * Creates a new {@link EventDistributor}. 128 | * 129 | * @param knownObjects a mutable {@link Map} of Kubernetes resources 130 | * that contains or will contain Kubernetes resources known to this 131 | * {@link EventDistributor} and whatever mechanism (such as a {@link 132 | * Controller}) is feeding it; may be {@code null} 133 | * 134 | * @param synchronizationInterval a {@link Duration} representing 135 | * the interval after which an attempt to synchronize might happen; 136 | * may be {@code null} in which case no synchronization will occur 137 | * 138 | * @see 139 | * ResourceTrackingEventQueueConsumer#ResourceTrackingEventQueueConsumer(Map) 140 | */ 141 | public EventDistributor(final Map knownObjects, final Duration synchronizationInterval) { 142 | super(knownObjects); 143 | final ReadWriteLock lock = new ReentrantReadWriteLock(); 144 | this.readLock = lock.readLock(); 145 | this.writeLock = lock.writeLock(); 146 | this.pumps = new ArrayList<>(); 147 | this.synchronizingPumps = new ArrayList<>(); 148 | this.synchronizationInterval = synchronizationInterval; 149 | } 150 | 151 | 152 | /* 153 | * Instance methods. 154 | */ 155 | 156 | 157 | /** 158 | * Adds the supplied {@link Consumer} to this {@link 159 | * EventDistributor} as a listener that will be notified of each 160 | * {@link AbstractEvent} this {@link EventDistributor} receives. 161 | * 162 | *

The supplied {@link Consumer}'s {@link 163 | * Consumer#accept(Object)} method may be called later on a separate 164 | * thread of execution.

165 | * 166 | * @param consumer a {@link Consumer} of {@link AbstractEvent}s; may 167 | * be {@code null} in which case no action will be taken 168 | * 169 | * @see #addConsumer(Consumer, Function) 170 | * 171 | * @see #removeConsumer(Consumer) 172 | */ 173 | public final void addConsumer(final Consumer> consumer) { 174 | this.addConsumer(consumer, null); 175 | } 176 | 177 | /** 178 | * Adds the supplied {@link Consumer} to this {@link 179 | * EventDistributor} as a listener that will be notified of each 180 | * {@link AbstractEvent} this {@link EventDistributor} receives. 181 | * 182 | *

The supplied {@link Consumer}'s {@link 183 | * Consumer#accept(Object)} method may be called later on a separate 184 | * thread of execution.

185 | * 186 | * @param consumer a {@link Consumer} of {@link AbstractEvent}s; may 187 | * be {@code null} in which case no action will be taken 188 | * 189 | * @param errorHandler a {@link Function} to handle any {@link 190 | * Throwable}s encountered; may be {@code null} in which case a 191 | * default error handler will be used instead 192 | * 193 | * @see #removeConsumer(Consumer) 194 | */ 195 | public final void addConsumer(final Consumer> consumer, final Function errorHandler) { 196 | if (consumer != null) { 197 | this.writeLock.lock(); 198 | try { 199 | final Pump pump = new Pump<>(this.synchronizationInterval, consumer, errorHandler); 200 | pump.start(); 201 | this.pumps.add(pump); 202 | this.synchronizingPumps.add(pump); 203 | } finally { 204 | this.writeLock.unlock(); 205 | } 206 | } 207 | } 208 | 209 | /** 210 | * Removes any {@link Consumer} {@linkplain Object#equals(Object) 211 | * equal to} a {@link Consumer} previously {@linkplain 212 | * #addConsumer(Consumer) added} to this {@link EventDistributor}. 213 | * 214 | * @param consumer the {@link Consumer} to remove; may be {@code 215 | * null} in which case no action will be taken 216 | * 217 | * @see #addConsumer(Consumer) 218 | */ 219 | public final void removeConsumer(final Consumer> consumer) { 220 | if (consumer != null) { 221 | this.writeLock.lock(); 222 | try { 223 | final Iterator> iterator = this.pumps.iterator(); 224 | assert iterator != null; 225 | while (iterator.hasNext()) { 226 | final Pump pump = iterator.next(); 227 | if (pump != null && consumer.equals(pump.getEventConsumer())) { 228 | pump.close(); 229 | iterator.remove(); 230 | break; 231 | } 232 | } 233 | } finally { 234 | this.writeLock.unlock(); 235 | } 236 | } 237 | } 238 | 239 | /** 240 | * Releases resources held by this {@link EventDistributor} during 241 | * its execution. 242 | */ 243 | @Override 244 | public final void close() { 245 | this.writeLock.lock(); 246 | try { 247 | this.pumps.stream() 248 | .forEach(pump -> { 249 | pump.close(); 250 | }); 251 | this.synchronizingPumps.clear(); 252 | this.pumps.clear(); 253 | } finally { 254 | this.writeLock.unlock(); 255 | } 256 | } 257 | 258 | /** 259 | * Returns {@code true} if this {@link EventDistributor} should 260 | * synchronize with its upstream source. 261 | * 262 | *

Design Notes

263 | * 264 | *

The Kubernetes {@code tools/cache} package spreads 265 | * synchronization out among the reflector, controller, event cache 266 | * and event processor constructs for no seemingly good reason. 267 | * They should probably be consolidated, particularly in an 268 | * object-oriented environment such as Java.

269 | * 270 | * @return {@code true} if synchronization should occur; {@code 271 | * false} otherwise 272 | * 273 | * @see EventCache#synchronize() 274 | */ 275 | public final boolean shouldSynchronize() { 276 | boolean returnValue = false; 277 | this.writeLock.lock(); 278 | try { 279 | this.synchronizingPumps.clear(); 280 | final Instant now = Instant.now(); 281 | this.pumps.stream() 282 | .filter(pump -> pump.shouldSynchronize(now)) 283 | .forEach(pump -> { 284 | this.synchronizingPumps.add(pump); 285 | pump.determineNextSynchronizationInterval(now); 286 | }); 287 | returnValue = !this.synchronizingPumps.isEmpty(); 288 | } finally { 289 | this.writeLock.unlock(); 290 | } 291 | return returnValue; 292 | } 293 | 294 | /** 295 | * Consumes the supplied {@link AbstractEvent} by forwarding it to 296 | * the {@link Consumer#accept(Object)} method of each {@link 297 | * Consumer} {@linkplain #addConsumer(Consumer) registered} with 298 | * this {@link EventDistributor}. 299 | * 300 | * @param event the {@link AbstractEvent} to forward; may be {@code 301 | * null} in which case no action is taken 302 | * 303 | * @see #addConsumer(Consumer) 304 | * 305 | * @see ResourceTrackingEventQueueConsumer#accept(AbstractEvent) 306 | */ 307 | @Override 308 | protected final void accept(final AbstractEvent event) { 309 | if (event != null) { 310 | if (event instanceof SynchronizationEvent) { 311 | this.accept((SynchronizationEvent)event); 312 | } else if (event instanceof Event) { 313 | this.accept((Event)event); 314 | } else { 315 | assert false : "Unexpected event type: " + event.getClass(); 316 | } 317 | } 318 | } 319 | 320 | private final void accept(final SynchronizationEvent event) { 321 | this.readLock.lock(); 322 | try { 323 | if (!this.synchronizingPumps.isEmpty()) { 324 | this.synchronizingPumps.stream() 325 | .forEach(pump -> pump.accept(event)); 326 | } 327 | } finally { 328 | this.readLock.unlock(); 329 | } 330 | } 331 | 332 | private final void accept(final Event event) { 333 | this.readLock.lock(); 334 | try { 335 | if (!this.pumps.isEmpty()) { 336 | this.pumps.stream() 337 | .forEach(pump -> pump.accept(event)); 338 | } 339 | } finally { 340 | this.readLock.unlock(); 341 | } 342 | } 343 | 344 | 345 | /* 346 | * Inner and nested classes. 347 | */ 348 | 349 | 350 | /** 351 | * A {@link Consumer} of {@link AbstractEvent} instances that puts 352 | * them on an internal queue and, in a separate thread, removes them 353 | * from the queue and forwards them to the "real" {@link Consumer} 354 | * supplied at construction time. 355 | * 356 | *

A {@link Pump} differs from a simple {@link Consumer} of 357 | * {@link AbstractEvent} instances in that it has its own 358 | * {@linkplain #getSynchronizationInterval() synchronization 359 | * interval}, and interposes a blocking queue in between the 360 | * reception of an {@link AbstractEvent} and its eventual broadcast.

361 | * 362 | * @author Laird Nelson 364 | */ 365 | private static final class Pump implements Consumer>, AutoCloseable { 366 | 367 | private final Logger logger; 368 | 369 | private final Consumer> eventConsumer; 370 | 371 | private final Function errorHandler; 372 | 373 | private volatile boolean closing; 374 | 375 | private volatile Instant nextSynchronizationInstant; 376 | 377 | private volatile Duration synchronizationInterval; 378 | 379 | @GuardedBy("this") 380 | private ScheduledExecutorService executor; 381 | 382 | @GuardedBy("this") 383 | private Future task; 384 | 385 | private volatile Future errorHandlingTask; 386 | 387 | final BlockingQueue> queue; 388 | 389 | private Pump(final Duration synchronizationInterval, final Consumer> eventConsumer) { 390 | this(synchronizationInterval, eventConsumer, null); 391 | } 392 | 393 | private Pump(final Duration synchronizationInterval, final Consumer> eventConsumer, final Function errorHandler) { 394 | super(); 395 | final String cn = this.getClass().getName(); 396 | this.logger = Logger.getLogger(cn); 397 | assert this.logger != null; 398 | final String mn = ""; 399 | if (this.logger.isLoggable(Level.FINER)) { 400 | this.logger.entering(cn, mn, new Object[] { synchronizationInterval, eventConsumer, errorHandler }); 401 | } 402 | 403 | // TODO: this should be extensible 404 | this.queue = new LinkedBlockingQueue<>(); 405 | this.eventConsumer = Objects.requireNonNull(eventConsumer); 406 | if (errorHandler == null) { 407 | this.errorHandler = t -> { 408 | if (this.logger.isLoggable(Level.SEVERE)) { 409 | this.logger.logp(Level.SEVERE, this.getClass().getName(), "", t.getMessage(), t); 410 | } 411 | return true; 412 | }; 413 | } else { 414 | this.errorHandler = errorHandler; 415 | } 416 | this.setSynchronizationInterval(synchronizationInterval); 417 | 418 | if (this.logger.isLoggable(Level.FINER)) { 419 | this.logger.exiting(cn, mn); 420 | } 421 | } 422 | 423 | private final void start() { 424 | final String cn = this.getClass().getName(); 425 | final String mn = "start"; 426 | if (this.logger.isLoggable(Level.FINER)) { 427 | this.logger.entering(cn, mn); 428 | } 429 | 430 | synchronized (this) { 431 | 432 | if (this.executor == null) { 433 | assert this.task == null; 434 | assert this.errorHandlingTask == null; 435 | 436 | this.executor = this.createScheduledThreadPoolExecutor(); 437 | if (this.executor == null) { 438 | throw new IllegalStateException("createScheduledThreadPoolExecutor() == null"); 439 | } 440 | 441 | // Schedule a hopefully never-ending task to pump events from 442 | // our queue to the supplied eventConsumer. We *schedule* this, 443 | // even though it will never end, instead of simply *executing* 444 | // it, so that if for any reason it exits (by definition an 445 | // error case) it will get restarted. Cancelling a scheduled 446 | // task will also cancel all resubmissions of it, so this is the 447 | // most robust thing to do. The delay of one second is 448 | // arbitrary. 449 | this.task = this.executor.scheduleWithFixedDelay(() -> { 450 | while (!Thread.currentThread().isInterrupted()) { 451 | try { 452 | this.getEventConsumer().accept(this.queue.take()); 453 | } catch (final InterruptedException interruptedException) { 454 | Thread.currentThread().interrupt(); 455 | } catch (final RuntimeException runtimeException) { 456 | if (!this.errorHandler.apply(runtimeException)) { 457 | throw runtimeException; 458 | } 459 | } catch (final Error error) { 460 | if (!this.errorHandler.apply(error)) { 461 | throw error; 462 | } 463 | } 464 | } 465 | }, 0L, 1L, TimeUnit.SECONDS); 466 | assert this.task != null; 467 | 468 | this.errorHandlingTask = this.executor.submit(() -> { 469 | try { 470 | while (!Thread.currentThread().isInterrupted()) { 471 | // The task is basically never-ending, so this will 472 | // block too, unless there's an exception. That's 473 | // the whole point. 474 | this.task.get(); 475 | } 476 | } catch (final CancellationException ok) { 477 | // The task was cancelled. Possibly redundantly, 478 | // cancel it for sure. This is an expected and normal 479 | // condition. 480 | this.task.cancel(true); 481 | } catch (final ExecutionException executionException) { 482 | // The task encountered an exception while executing. 483 | // Although we got an ExecutionException, the task is 484 | // still in a non-cancelled state. We need to cancel 485 | // it now to (potentially) have it removed from the 486 | // executor queue. 487 | this.task.cancel(true); 488 | final Future errorHandlingTask = this.errorHandlingTask; 489 | if (errorHandlingTask != null) { 490 | errorHandlingTask.cancel(true); // cancel ourselves, too! 491 | } 492 | // Apply the actual error-handling logic to the 493 | // exception. 494 | // TODO: This should have already been done by the 495 | // task itself... 496 | this.errorHandler.apply(executionException.getCause()); 497 | } catch (final InterruptedException interruptedException) { 498 | Thread.currentThread().interrupt(); 499 | } 500 | if (Thread.currentThread().isInterrupted()) { 501 | // The current thread was interrupted, probably 502 | // because everything is closing up shop. Cancel 503 | // everything and go home. 504 | this.task.cancel(true); 505 | final Future errorHandlingTask = this.errorHandlingTask; 506 | if (errorHandlingTask != null) { 507 | errorHandlingTask.cancel(true); // cancel ourselves, too! 508 | } 509 | } 510 | }); 511 | } 512 | 513 | } 514 | 515 | if (this.logger.isLoggable(Level.FINER)) { 516 | this.logger.entering(cn, mn); 517 | } 518 | } 519 | 520 | private final ScheduledExecutorService createScheduledThreadPoolExecutor() { 521 | final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, new PumpThreadFactory()); 522 | executor.setRemoveOnCancelPolicy(true); 523 | return executor; 524 | } 525 | 526 | private final Consumer> getEventConsumer() { 527 | return this.eventConsumer; 528 | } 529 | 530 | /** 531 | * Adds the supplied {@link AbstractEvent} to an internal {@link 532 | * BlockingQueue}. A task will have already been scheduled to 533 | * consume it. 534 | * 535 | * @param event the {@link AbstractEvent} to add; may be {@code 536 | * null} in which case no action is taken 537 | */ 538 | @Override 539 | public final void accept(final AbstractEvent event) { 540 | final String cn = this.getClass().getName(); 541 | final String mn = "accept"; 542 | if (this.logger.isLoggable(Level.FINER)) { 543 | this.logger.entering(cn, mn, event); 544 | } 545 | if (this.closing) { 546 | throw new IllegalStateException(); 547 | } 548 | if (event != null) { 549 | final boolean added = this.queue.add(event); 550 | assert added; 551 | } 552 | if (this.logger.isLoggable(Level.FINER)) { 553 | this.logger.exiting(cn, mn); 554 | } 555 | } 556 | 557 | @Override 558 | public final void close() { 559 | final String cn = this.getClass().getName(); 560 | final String mn = "close"; 561 | if (this.logger.isLoggable(Level.FINER)) { 562 | this.logger.entering(cn, mn); 563 | } 564 | 565 | synchronized (this) { 566 | if (!this.closing) { 567 | try { 568 | assert this.executor != null; 569 | assert this.task != null; 570 | assert this.errorHandlingTask != null; 571 | this.closing = true; 572 | 573 | // Stop accepting new tasks. 574 | this.executor.shutdown(); 575 | 576 | // Cancel our regular task. 577 | this.task.cancel(true); 578 | this.task = null; 579 | 580 | // Cancel our task that surfaces errors from the regular task. 581 | this.errorHandlingTask.cancel(true); 582 | this.errorHandlingTask = null; 583 | 584 | try { 585 | // Wait for our executor to shut down normally, and shut 586 | // it down forcibly if it doesn't. 587 | if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) { 588 | this.executor.shutdownNow(); 589 | if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) { 590 | if (this.logger.isLoggable(Level.WARNING)) { 591 | this.logger.logp(Level.WARNING, cn, mn, "this.executor.awaitTermination() failed"); 592 | } 593 | } 594 | } 595 | } catch (final InterruptedException interruptedException) { 596 | this.executor.shutdownNow(); 597 | Thread.currentThread().interrupt(); 598 | } 599 | this.executor = null; 600 | } finally { 601 | this.closing = false; 602 | } 603 | } 604 | } 605 | 606 | if (this.logger.isLoggable(Level.FINER)) { 607 | this.logger.exiting(cn, mn); 608 | } 609 | } 610 | 611 | 612 | /* 613 | * Synchronization-related methods. It seems odd that one of these 614 | * listeners would need to report details about synchronization, but 615 | * that's what the Go code does. Maybe this functionality could be 616 | * relocated "higher up". 617 | */ 618 | 619 | 620 | private final boolean shouldSynchronize(final Instant now) { 621 | final String cn = this.getClass().getName(); 622 | final String mn = "shouldSynchronize"; 623 | if (this.logger.isLoggable(Level.FINER)) { 624 | this.logger.entering(cn, mn, now); 625 | } 626 | final boolean returnValue; 627 | if (this.closing) { 628 | returnValue = false; 629 | } else { 630 | final Duration interval = this.getSynchronizationInterval(); 631 | if (interval == null || interval.isZero()) { 632 | returnValue = false; 633 | } else if (now == null) { 634 | returnValue = Instant.now().compareTo(this.nextSynchronizationInstant) >= 0; 635 | } else { 636 | returnValue = now.compareTo(this.nextSynchronizationInstant) >= 0; 637 | } 638 | } 639 | if (this.logger.isLoggable(Level.FINER)) { 640 | this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 641 | } 642 | return returnValue; 643 | } 644 | 645 | private final void determineNextSynchronizationInterval(final Instant now) { 646 | final String cn = this.getClass().getName(); 647 | final String mn = "determineNextSynchronizationInterval"; 648 | if (this.logger.isLoggable(Level.FINER)) { 649 | this.logger.entering(cn, mn, now); 650 | } 651 | final Duration synchronizationInterval = this.getSynchronizationInterval(); 652 | if (synchronizationInterval == null) { 653 | if (now == null) { 654 | this.nextSynchronizationInstant = Instant.now(); 655 | } else { 656 | this.nextSynchronizationInstant = now; 657 | } 658 | } else if (now == null) { 659 | this.nextSynchronizationInstant = Instant.now().plus(synchronizationInterval); 660 | } else { 661 | this.nextSynchronizationInstant = now.plus(synchronizationInterval); 662 | } 663 | if (this.logger.isLoggable(Level.FINER)) { 664 | this.logger.entering(cn, mn); 665 | } 666 | } 667 | 668 | public final void setSynchronizationInterval(final Duration synchronizationInterval) { 669 | this.synchronizationInterval = synchronizationInterval; 670 | } 671 | 672 | public final Duration getSynchronizationInterval() { 673 | return this.synchronizationInterval; 674 | } 675 | 676 | 677 | /* 678 | * Inner and nested classes. 679 | */ 680 | 681 | 682 | /** 683 | * A {@link ThreadFactory} that {@linkplain #newThread(Runnable) 684 | * produces new Threads} with sane names. 685 | * 686 | * @author Laird Nelson 688 | */ 689 | private static final class PumpThreadFactory implements ThreadFactory { 690 | 691 | private final ThreadGroup group; 692 | 693 | private final AtomicInteger threadNumber = new AtomicInteger(1); 694 | 695 | private PumpThreadFactory() { 696 | final SecurityManager s = System.getSecurityManager(); 697 | if (s == null) { 698 | this.group = Thread.currentThread().getThreadGroup(); 699 | } else { 700 | this.group = s.getThreadGroup(); 701 | } 702 | } 703 | 704 | @Override 705 | public final Thread newThread(final Runnable runnable) { 706 | final Thread returnValue = new Thread(this.group, runnable, "event-pump-thread-" + this.threadNumber.getAndIncrement(), 0); 707 | if (returnValue.isDaemon()) { 708 | returnValue.setDaemon(false); 709 | } 710 | if (returnValue.getPriority() != Thread.NORM_PRIORITY) { 711 | returnValue.setPriority(Thread.NORM_PRIORITY); 712 | } 713 | return returnValue; 714 | } 715 | } 716 | 717 | } 718 | 719 | } 720 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/EventQueue.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.util.AbstractCollection; 20 | import java.util.Collection; 21 | import java.util.Collections; 22 | import java.util.Iterator; 23 | import java.util.LinkedList; 24 | import java.util.NoSuchElementException; // for javadoc only 25 | import java.util.Objects; 26 | 27 | import java.util.function.Consumer; 28 | 29 | import java.util.logging.Level; 30 | import java.util.logging.Logger; 31 | 32 | import io.fabric8.kubernetes.api.model.HasMetadata; 33 | 34 | import net.jcip.annotations.GuardedBy; 35 | import net.jcip.annotations.ThreadSafe; 36 | 37 | /** 38 | * A publicly-unmodifiable {@link AbstractCollection} of {@link 39 | * AbstractEvent}s produced by an {@link EventQueueCollection}. 40 | * 41 | *

All {@link AbstractEvent}s in an {@link EventQueue} describe the 42 | * life of a single {@linkplain HasMetadata resource} in 43 | * Kubernetes.

44 | * 45 | *

Thread Safety

46 | * 47 | *

This class is safe for concurrent use by multiple {@link 48 | * Thread}s. Some operations, like the usage of the {@link 49 | * #iterator()} method, require that callers synchronize on the {@link 50 | * EventQueue} directly. This class' internals synchronize on {@code 51 | * this} when locking is needed.

52 | * 53 | *

Overrides of this class must also be safe for concurrent use by 54 | * multiple {@link Thread}s.

55 | * 56 | * @param the type of a Kubernetes resource 57 | * 58 | * @author Laird Nelson 60 | * 61 | * @see EventQueueCollection 62 | */ 63 | @ThreadSafe 64 | public class EventQueue extends AbstractCollection> { 65 | 66 | 67 | /* 68 | * Instance fields. 69 | */ 70 | 71 | 72 | /** 73 | * A {@link Logger} for use by this {@link EventQueue}. 74 | * 75 | *

This field is never {@code null}.

76 | * 77 | * @see #createLogger() 78 | */ 79 | protected final Logger logger; 80 | 81 | /** 82 | * The key identifying the Kubernetes resource to which all of the 83 | * {@link AbstractEvent}s managed by this {@link EventQueue} apply. 84 | * 85 | *

This field is never {@code null}.

86 | */ 87 | private final Object key; 88 | 89 | /** 90 | * The actual underlying queue of {@link AbstractEvent}s. 91 | * 92 | *

This field is never {@code null}.

93 | */ 94 | @GuardedBy("this") 95 | private final LinkedList> events; 96 | 97 | 98 | /* 99 | * Constructors. 100 | */ 101 | 102 | 103 | /** 104 | * Creates a new {@link EventQueue}. 105 | * 106 | * @param key the key identifying the Kubernetes resource to which 107 | * all of the {@link AbstractEvent}s managed by this {@link 108 | * EventQueue} apply; must not be {@code null} 109 | * 110 | * @exception NullPointerException if {@code key} is {@code null} 111 | * 112 | * @exception IllegalStateException if the {@link #createLogger()} 113 | * method returns {@code null} 114 | */ 115 | protected EventQueue(final Object key) { 116 | super(); 117 | this.logger = this.createLogger(); 118 | if (this.logger == null) { 119 | throw new IllegalStateException("createLogger() == null"); 120 | } 121 | final String cn = this.getClass().getName(); 122 | final String mn = ""; 123 | if (this.logger.isLoggable(Level.FINER)) { 124 | this.logger.entering(cn, mn, key); 125 | } 126 | this.key = Objects.requireNonNull(key); 127 | this.events = new LinkedList<>(); 128 | if (this.logger.isLoggable(Level.FINER)) { 129 | this.logger.exiting(cn, mn); 130 | } 131 | } 132 | 133 | 134 | /* 135 | * Instance methods. 136 | */ 137 | 138 | 139 | /** 140 | * Returns a {@link Logger} for use by this {@link EventQueue}. 141 | * 142 | *

This method never returns {@code null}.

143 | * 144 | *

Overrides of this method must not return {@code null}.

145 | * 146 | * @return a non-{@code null} {@link Logger} 147 | */ 148 | protected Logger createLogger() { 149 | return Logger.getLogger(this.getClass().getName()); 150 | } 151 | 152 | /** 153 | * Returns the key identifying the Kubernetes resource to which all 154 | * of the {@link AbstractEvent}s managed by this {@link EventQueue} 155 | * apply. 156 | * 157 | *

This method never returns {@code null}.

158 | * 159 | * @return a non-{@code null} {@link Object} 160 | * 161 | * @see #EventQueue(Object) 162 | */ 163 | public final Object getKey() { 164 | final String cn = this.getClass().getName(); 165 | final String mn = "getKey"; 166 | if (this.logger.isLoggable(Level.FINER)) { 167 | this.logger.entering(cn, mn); 168 | } 169 | final Object returnValue = this.key; 170 | if (this.logger.isLoggable(Level.FINER)) { 171 | this.logger.entering(cn, mn, returnValue); 172 | } 173 | return returnValue; 174 | } 175 | 176 | /** 177 | * Returns {@code true} if this {@link EventQueue} is empty. 178 | * 179 | * @return {@code true} if this {@link EventQueue} is empty; {@code 180 | * false} otherwise 181 | * 182 | * @see #size() 183 | */ 184 | public synchronized final boolean isEmpty() { 185 | final String cn = this.getClass().getName(); 186 | final String mn = "isEmpty"; 187 | if (this.logger.isLoggable(Level.FINER)) { 188 | this.logger.entering(cn, mn); 189 | } 190 | final boolean returnValue = this.events.isEmpty(); 191 | if (this.logger.isLoggable(Level.FINER)) { 192 | this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 193 | } 194 | return returnValue; 195 | } 196 | 197 | /** 198 | * Returns the size of this {@link EventQueue}. 199 | * 200 | *

This method never returns an {@code int} less than {@code 201 | * 0}.

202 | * 203 | * @return the size of this {@link EventQueue}; never negative 204 | * 205 | * @see #isEmpty() 206 | */ 207 | @Override 208 | public synchronized final int size() { 209 | final String cn = this.getClass().getName(); 210 | final String mn = "size"; 211 | if (this.logger.isLoggable(Level.FINER)) { 212 | this.logger.entering(cn, mn); 213 | } 214 | final int returnValue = this.events.size(); 215 | if (this.logger.isLoggable(Level.FINER)) { 216 | this.logger.exiting(cn, mn, Integer.valueOf(returnValue)); 217 | } 218 | return returnValue; 219 | } 220 | 221 | /** 222 | * Adds the supplied {@link AbstractEvent} to this {@link 223 | * EventQueue} under certain conditions. 224 | * 225 | *

The supplied {@link AbstractEvent} is added to this {@link 226 | * EventQueue} if:

227 | * 228 | *
    229 | * 230 | *
  • its {@linkplain AbstractEvent#getKey() key} is equal to this 231 | * {@link EventQueue}'s {@linkplain #getKey() key}
  • 232 | * 233 | *
  • it is either not a {@linkplain SynchronizationEvent} 234 | * synchronization event}, or it is a {@linkplain 235 | * SynchronizationEvent synchronization event} and this {@link 236 | * EventQueue} does not represent a sequence of events that 237 | * {@linkplain #resultsInDeletion() describes a deletion}, and
  • 238 | * 239 | *
  • optional {@linkplain #compress(Collection) compression} does 240 | * not result in this {@link EventQueue} being empty
  • 241 | * 242 | *
243 | * 244 | * @param event the {@link AbstractEvent} to add; must not be {@code 245 | * null} 246 | * 247 | * @return {@code true} if an addition took place and {@linkplain 248 | * #compress(Collection) optional compression} did not result in 249 | * this {@link EventQueue} {@linkplain #isEmpty() becoming empty}; 250 | * {@code false} otherwise 251 | * 252 | * @exception NullPointerException if {@code event} is {@code null} 253 | * 254 | * @exception IllegalArgumentException if {@code event}'s 255 | * {@linkplain AbstractEvent#getKey() key} is not equal to this 256 | * {@link EventQueue}'s {@linkplain #getKey() key} 257 | * 258 | * @see #compress(Collection) 259 | * 260 | * @see SynchronizationEvent 261 | * 262 | * @see #resultsInDeletion() 263 | */ 264 | final boolean addEvent(final AbstractEvent event) { 265 | final String cn = this.getClass().getName(); 266 | final String mn = "addEvent"; 267 | if (this.logger.isLoggable(Level.FINER)) { 268 | this.logger.entering(cn, mn, event); 269 | } 270 | 271 | Objects.requireNonNull(event); 272 | 273 | final Object key = this.getKey(); 274 | if (!key.equals(event.getKey())) { 275 | throw new IllegalArgumentException("!this.getKey().equals(event.getKey()): " + key + ", " + event.getKey()); 276 | } 277 | 278 | boolean returnValue = false; 279 | 280 | final AbstractEvent.Type eventType = event.getType(); 281 | assert eventType != null; 282 | 283 | synchronized (this) { 284 | if (!(event instanceof SynchronizationEvent) || !this.resultsInDeletion()) { 285 | // If the event is NOT a synchronization event (so it's an 286 | // addition, modification, or deletion)... 287 | // ...OR if it IS a synchronization event AND we are NOT 288 | // already going to delete this queue... 289 | returnValue = this.events.add(event); 290 | if (returnValue) { 291 | this.deduplicate(); 292 | final Collection> readOnlyEvents = Collections.unmodifiableCollection(this.events); 293 | final Collection> newEvents = this.compress(readOnlyEvents); 294 | if (newEvents != readOnlyEvents) { 295 | this.events.clear(); 296 | if (newEvents != null && !newEvents.isEmpty()) { 297 | this.events.addAll(newEvents); 298 | } 299 | } 300 | returnValue = !this.isEmpty(); 301 | } 302 | } 303 | } 304 | 305 | if (this.logger.isLoggable(Level.FINER)) { 306 | this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 307 | } 308 | return returnValue; 309 | } 310 | 311 | /** 312 | * Returns the last (and definitionally newest) {@link 313 | * AbstractEvent} in this {@link EventQueue}. 314 | * 315 | *

This method never returns {@code null}.

316 | * 317 | * @return the last {@link AbstractEvent} in this {@link 318 | * EventQueue}; never {@code null} 319 | * 320 | * @exception NoSuchElementException if this {@link EventQueue} is 321 | * {@linkplain #isEmpty() empty} 322 | */ 323 | synchronized final AbstractEvent getLast() { 324 | final String cn = this.getClass().getName(); 325 | final String mn = "getLast"; 326 | if (this.logger.isLoggable(Level.FINER)) { 327 | this.logger.entering(cn, mn); 328 | } 329 | final AbstractEvent returnValue = this.events.getLast(); 330 | if (this.logger.isLoggable(Level.FINER)) { 331 | this.logger.exiting(cn, mn, returnValue); 332 | } 333 | return returnValue; 334 | } 335 | 336 | /** 337 | * Synchronizes on this {@link EventQueue} and, while holding its 338 | * monitor, invokes the {@link Consumer#accept(Object)} method on 339 | * the supplied {@link Consumer} for every {@link AbstractEvent} in 340 | * this {@link EventQueue}. 341 | * 342 | * @param action the {@link Consumer} in question; must not be 343 | * {@code null} 344 | * 345 | * @exception NullPointerException if {@code action} is {@code null} 346 | */ 347 | @Override 348 | public synchronized final void forEach(final Consumer> action) { 349 | super.forEach(action); 350 | } 351 | 352 | /** 353 | * Synchronizes on this {@link EventQueue} and, while holding its 354 | * monitor, returns an unmodifiable {@link Iterator} over its 355 | * contents. 356 | * 357 | *

This method never returns {@code null}.

358 | * 359 | * @return a non-{@code null} unmodifiable {@link Iterator} of 360 | * {@link AbstractEvent}s 361 | */ 362 | @Override 363 | public synchronized final Iterator> iterator() { 364 | return Collections.unmodifiableCollection(this.events).iterator(); 365 | } 366 | 367 | /** 368 | * If this {@link EventQueue}'s {@linkplain #size() size} is greater 369 | * than {@code 2}, and if its last two {@link AbstractEvent}s are 370 | * {@linkplain AbstractEvent.Type#DELETION deletions}, and if the 371 | * next-to-last deletion {@link AbstractEvent}'s {@linkplain 372 | * AbstractEvent#isFinalStateKnown() state is known}, then this method 373 | * causes that {@link AbstractEvent} to replace the two under consideration. 374 | * 375 | *

This method is called only by the {@link #addEvent(AbstractEvent)} 376 | * method.

377 | * 378 | * @see #addEvent(AbstractEvent) 379 | */ 380 | private synchronized final void deduplicate() { 381 | final String cn = this.getClass().getName(); 382 | final String mn = "deduplicate"; 383 | if (this.logger.isLoggable(Level.FINER)) { 384 | this.logger.entering(cn, mn); 385 | } 386 | final int size = this.size(); 387 | if (size > 2) { 388 | final AbstractEvent lastEvent = this.events.get(size - 1); 389 | final AbstractEvent nextToLastEvent = this.events.get(size - 2); 390 | final AbstractEvent event; 391 | if (lastEvent != null && nextToLastEvent != null && AbstractEvent.Type.DELETION.equals(lastEvent.getType()) && AbstractEvent.Type.DELETION.equals(nextToLastEvent.getType())) { 392 | event = nextToLastEvent.isFinalStateKnown() ? nextToLastEvent : lastEvent; 393 | } else { 394 | event = null; 395 | } 396 | if (event != null) { 397 | this.events.set(size - 2, event); 398 | this.events.remove(size - 1); 399 | } 400 | } 401 | if (this.logger.isLoggable(Level.FINER)) { 402 | this.logger.exiting(cn, mn); 403 | } 404 | } 405 | 406 | /** 407 | * Returns {@code true} if this {@link EventQueue} is {@linkplain 408 | * #isEmpty() not empty} and the {@linkplain #getLast() last 409 | * AbstractEvent in this EventQueue} is a 410 | * {@linkplain AbstractEvent.Type#DELETION deletion event}. 411 | * 412 | * @return {@code true} if this {@link EventQueue} currently 413 | * logically represents the deletion of a resource, {@code false} 414 | * otherwise 415 | */ 416 | synchronized final boolean resultsInDeletion() { 417 | final String cn = this.getClass().getName(); 418 | final String mn = "resultsInDeletion"; 419 | if (this.logger.isLoggable(Level.FINER)) { 420 | this.logger.entering(cn, mn); 421 | } 422 | final boolean returnValue = !this.isEmpty() && this.getLast().getType().equals(AbstractEvent.Type.DELETION); 423 | if (this.logger.isLoggable(Level.FINER)) { 424 | this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 425 | } 426 | return returnValue; 427 | } 428 | 429 | /** 430 | * Performs a compression operation on the supplied {@link 431 | * Collection} of {@link AbstractEvent}s and returns the result of that 432 | * operation. 433 | * 434 | *

This method may return {@code null}, which will result in the 435 | * emptying of this {@link EventQueue}.

436 | * 437 | *

This method is called while holding this {@link EventQueue}'s 438 | * monitor.

439 | * 440 | *

This method is called when an {@link EventQueueCollection} (or 441 | * some other {@link AbstractEvent} producer with access to 442 | * package-protected methods of this class) adds an {@link AbstractEvent} to 443 | * this {@link EventQueue} and provides the {@link EventQueue} 444 | * implementation with the ability to eliminate duplicates or 445 | * otherwise compress the event stream it represents.

446 | * 447 | *

This implementation simply returns the supplied {@code events} 448 | * {@link Collection}; i.e. no compression is performed.

449 | * 450 | * @param events an {@link 451 | * Collections#unmodifiableCollection(Collection) unmodifiable 452 | * Collection} of {@link AbstractEvent}s representing the 453 | * current state of this {@link EventQueue}; will never be {@code 454 | * null} 455 | * 456 | * @return the new state that this {@link EventQueue} should assume; 457 | * may be {@code null}; may simply be the supplied {@code events} 458 | * {@link Collection} if compression is not desired or implemented 459 | */ 460 | protected Collection> compress(final Collection> events) { 461 | return events; 462 | } 463 | 464 | /** 465 | * Returns a hashcode for this {@link EventQueue}. 466 | * 467 | * @return a hashcode for this {@link EventQueue} 468 | * 469 | * @see #equals(Object) 470 | */ 471 | @Override 472 | public final int hashCode() { 473 | int hashCode = 17; 474 | 475 | Object value = this.getKey(); 476 | int c = value == null ? 0 : value.hashCode(); 477 | hashCode = 37 * hashCode + c; 478 | 479 | synchronized (this) { 480 | value = this.events; 481 | c = value == null ? 0 : value.hashCode(); 482 | } 483 | hashCode = 37 * hashCode + c; 484 | 485 | return hashCode; 486 | } 487 | 488 | /** 489 | * Returns {@code true} if the supplied {@link Object} is also an 490 | * {@link EventQueue} and is equal in all respects to this one. 491 | * 492 | * @param other the {@link Object} to test; may be {@code null} in 493 | * which case {@code null} will be returned 494 | * 495 | * @return {@code true} if the supplied {@link Object} is also an 496 | * {@link EventQueue} and is equal in all respects to this one; 497 | * {@code false} otherwise 498 | * 499 | * @see #hashCode() 500 | */ 501 | @Override 502 | public final boolean equals(final Object other) { 503 | if (other == this) { 504 | return true; 505 | } else if (other instanceof EventQueue) { 506 | final EventQueue her = (EventQueue)other; 507 | 508 | final Object key = this.getKey(); 509 | if (key == null) { 510 | if (her.getKey() != null) { 511 | return false; 512 | } 513 | } else if (!key.equals(her.getKey())) { 514 | return false; 515 | } 516 | 517 | synchronized (this) { 518 | final Object events = this.events; 519 | if (events == null) { 520 | synchronized (her) { 521 | if (her.events != null) { 522 | return false; 523 | } 524 | } 525 | } else { 526 | synchronized (her) { 527 | if (!events.equals(her.events)) { 528 | return false; 529 | } 530 | } 531 | } 532 | } 533 | 534 | return true; 535 | } else { 536 | return false; 537 | } 538 | } 539 | 540 | /** 541 | * Returns a {@link String} representation of this {@link 542 | * EventQueue}. 543 | * 544 | *

This method never returns {@code null}.

545 | * 546 | * @return a non-{@code null} {@link String} representation of this 547 | * {@link EventQueue} 548 | */ 549 | @Override 550 | public synchronized final String toString() { 551 | return new StringBuilder().append(this.getKey()).append(": ").append(this.events).toString(); 552 | } 553 | 554 | } 555 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/HasMetadatas.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | 22 | import io.fabric8.kubernetes.api.model.HasMetadata; 23 | import io.fabric8.kubernetes.api.model.ObjectMeta; 24 | 25 | /** 26 | * A utility class for working with {@link HasMetadata} resources. 27 | * 28 | * @author Laird Nelson 30 | * 31 | * @see #getKey(HasMetadata) 32 | * 33 | * @see HasMetadata 34 | */ 35 | public final class HasMetadatas { 36 | 37 | 38 | /* 39 | * Constructors. 40 | */ 41 | 42 | 43 | /** 44 | * Creates a new {@link HasMetadatas}. 45 | */ 46 | private HasMetadatas() { 47 | super(); 48 | } 49 | 50 | 51 | /* 52 | * Static methods. 53 | */ 54 | 55 | 56 | /** 57 | * Returns a key for the supplied {@link HasMetadata} 58 | * derived from its {@linkplain ObjectMeta#getName() name} and 59 | * {@linkplain ObjectMeta#getNamespace() namespace}. 60 | * 61 | *

This method may return {@code null}.

62 | * 63 | * @param resource the {@link HasMetadata} for which a key should be 64 | * returned; may be {@code null} in which case {@code null} will be 65 | * returned 66 | * 67 | * @return a key for the supplied {@link HasMetadata} 68 | * 69 | * @exception IllegalStateException if the supplied {@link 70 | * HasMetadata}'s {@linkplain ObjectMeta metadata}'s {@link 71 | * ObjectMeta#getName()} method returns {@code null} or an 72 | * {@linkplain String#isEmpty() empty} {@link String} 73 | * 74 | * @see HasMetadata#getMetadata() 75 | * 76 | * @see ObjectMeta#getName() 77 | * 78 | * @see ObjectMeta#getNamespace() 79 | */ 80 | public static final Object getKey(final HasMetadata resource) { 81 | final String cn = HasMetadatas.class.getName(); 82 | final String mn = "getKey"; 83 | final Logger logger = Logger.getLogger(cn); 84 | assert logger != null; 85 | if (logger.isLoggable(Level.FINER)) { 86 | logger.entering(cn, mn, resource); 87 | } 88 | 89 | final Object returnValue; 90 | if (resource == null) { 91 | returnValue = null; 92 | } else { 93 | final ObjectMeta metadata = resource.getMetadata(); 94 | if (metadata == null) { 95 | returnValue = null; 96 | } else { 97 | String name = metadata.getName(); 98 | if (name == null) { 99 | throw new IllegalStateException("metadata.getName() == null"); 100 | } else if (name.isEmpty()) { 101 | throw new IllegalStateException("metadata.getName().isEmpty()"); 102 | } 103 | final String namespace = metadata.getNamespace(); 104 | if (namespace == null || namespace.isEmpty()) { 105 | returnValue = name; 106 | } else { 107 | returnValue = new StringBuilder(namespace).append("/").append(name).toString(); 108 | } 109 | } 110 | } 111 | 112 | if (logger.isLoggable(Level.FINER)) { 113 | logger.exiting(cn, mn, returnValue); 114 | } 115 | return returnValue; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/Reflector.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.Closeable; 20 | import java.io.IOException; 21 | 22 | import java.lang.reflect.Field; 23 | import java.lang.reflect.Method; 24 | 25 | import java.time.Duration; 26 | 27 | import java.time.temporal.ChronoUnit; 28 | 29 | import java.util.ArrayList; 30 | import java.util.Collection; 31 | import java.util.Collections; 32 | import java.util.Objects; 33 | import java.util.Map; 34 | 35 | import java.util.concurrent.ExecutionException; 36 | import java.util.concurrent.Executors; 37 | import java.util.concurrent.Future; 38 | import java.util.concurrent.FutureTask; 39 | import java.util.concurrent.ScheduledExecutorService; 40 | import java.util.concurrent.ScheduledFuture; 41 | import java.util.concurrent.ScheduledThreadPoolExecutor; 42 | import java.util.concurrent.TimeUnit; 43 | 44 | import java.util.function.Function; 45 | 46 | import java.util.logging.Level; 47 | import java.util.logging.Logger; 48 | 49 | import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only 50 | import io.fabric8.kubernetes.client.KubernetesClientException; 51 | import io.fabric8.kubernetes.client.Watch; // for javadoc only 52 | import io.fabric8.kubernetes.client.Watcher; 53 | 54 | import io.fabric8.kubernetes.client.dsl.base.BaseOperation; 55 | import io.fabric8.kubernetes.client.dsl.base.OperationSupport; 56 | 57 | import io.fabric8.kubernetes.client.dsl.Listable; 58 | import io.fabric8.kubernetes.client.dsl.Versionable; 59 | import io.fabric8.kubernetes.client.dsl.VersionWatchable; 60 | import io.fabric8.kubernetes.client.dsl.Watchable; 61 | 62 | import io.fabric8.kubernetes.client.dsl.internal.CustomResourceOperationsImpl; 63 | 64 | import io.fabric8.kubernetes.api.model.HasMetadata; 65 | import io.fabric8.kubernetes.api.model.ObjectMeta; 66 | import io.fabric8.kubernetes.api.model.KubernetesResourceList; 67 | import io.fabric8.kubernetes.api.model.ListMeta; 68 | 69 | import net.jcip.annotations.GuardedBy; 70 | import net.jcip.annotations.ThreadSafe; 71 | 72 | import okhttp3.OkHttpClient; 73 | 74 | import org.microbean.development.annotation.Hack; 75 | import org.microbean.development.annotation.Issue; 76 | import org.microbean.development.annotation.NonBlocking; 77 | 78 | /** 79 | * A pump of sorts that continuously "pulls" logical events out of 80 | * Kubernetes and {@linkplain EventCache#add(Object, AbstractEvent.Type, 81 | * HasMetadata) adds them} to an {@link EventCache} so as to logically 82 | * "reflect" the contents of Kubernetes into the cache. 83 | * 84 | *

Thread Safety

85 | * 86 | *

Instances of this class are safe for concurrent use by multiple 87 | * {@link Thread}s.

88 | * 89 | *

Design Notes

90 | * 91 | *

This class loosely models the {@code 93 | * Reflector} type in the {@code tools/cache} package of the {@code 94 | * client-go} subproject of Kubernetes.

95 | * 96 | * @param a type of Kubernetes resource 97 | * 98 | * @author Laird Nelson 100 | * 101 | * @see EventCache 102 | */ 103 | @ThreadSafe 104 | public class Reflector implements Closeable { 105 | 106 | 107 | /* 108 | * Instance fields. 109 | */ 110 | 111 | 112 | /** 113 | * The operation that was supplied at construction time. 114 | * 115 | *

This field is never {@code null}.

116 | * 117 | *

It is guaranteed that the value of this field may be 118 | * assignable to a reference of type {@link Listable Listable<? 119 | * extends KubernetesResourceList>} or to a reference of type 120 | * {@link VersionWatchable VersionWatchable<? extends Closeable, 121 | * Watcher<T>>}.

122 | * 123 | * @see Listable 124 | * 125 | * @see VersionWatchable 126 | */ 127 | private final Object operation; 128 | 129 | /** 130 | * The resource version that a successful watch operation processed. 131 | * 132 | * @see #setLastSynchronizationResourceVersion(Object) 133 | * 134 | * @see WatchHandler#eventReceived(Watcher.Action, HasMetadata) 135 | */ 136 | private volatile Object lastSynchronizationResourceVersion; 137 | 138 | /** 139 | * The {@link ScheduledExecutorService} in charge of scheduling 140 | * repeated invocations of the {@link #synchronize()} method. 141 | * 142 | *

This field may be {@code null}.

143 | * 144 | *

Thread Safety

145 | * 146 | *

This field is not safe for concurrent use by multiple threads 147 | * without explicit synchronization on it.

148 | * 149 | * @see #synchronize() 150 | */ 151 | @GuardedBy("this") 152 | private ScheduledExecutorService synchronizationExecutorService; 153 | 154 | /** 155 | * A {@link Function} that consumes a {@link Throwable} and returns 156 | * {@code true} if the error represented by that {@link Throwable} 157 | * was handled in some way. 158 | * 159 | *

This field may be {@code null}.

160 | */ 161 | private final Function synchronizationErrorHandler; 162 | 163 | /** 164 | * A {@link ScheduledFuture} representing the task that is scheduled 165 | * to repeatedly invoke the {@link #synchronize()} method. 166 | * 167 | *

This field may be {@code null}.

168 | * 169 | *

Thread Safety

170 | * 171 | *

This field is not safe for concurrent use by multiple threads 172 | * without explicit synchronization on it.

173 | * 174 | * @see #synchronize() 175 | */ 176 | @GuardedBy("this") 177 | private ScheduledFuture synchronizationTask; 178 | 179 | /** 180 | * A flag tracking whether the {@link 181 | * #synchronizationExecutorService} should be shut down when this 182 | * {@link Reflector} is {@linkplain #close() closed}. If the 183 | * creator of this {@link Reflector} supplied an explicit {@link 184 | * ScheduledExecutorService} at construction time, then it will not 185 | * be shut down. 186 | */ 187 | private final boolean shutdownSynchronizationExecutorServiceOnClose; 188 | 189 | /** 190 | * How many seconds to wait in between scheduled invocations of the 191 | * {@link #synchronize()} method. If the value of this field is 192 | * less than or equal to zero then no synchronization will take 193 | * place. 194 | */ 195 | private final long synchronizationIntervalInSeconds; 196 | 197 | /** 198 | * The watch operation currently in effect. 199 | * 200 | *

This field may be {@code null} at any point.

201 | * 202 | *

Thread Safety

203 | * 204 | *

This field is not safe for concurrent use by multiple threads 205 | * without explicit synchronization on it.

206 | */ 207 | @GuardedBy("this") 208 | private Closeable watch; 209 | 210 | /** 211 | * An {@link EventCache} (often an {@link EventQueueCollection}) 212 | * whose contents will be added to to reflect the current state of 213 | * Kubernetes. 214 | * 215 | *

This field is never {@code null}.

216 | */ 217 | @GuardedBy("itself") 218 | private final EventCache eventCache; 219 | 220 | /** 221 | * A {@link Logger} for use by this {@link Reflector}. 222 | * 223 | *

This field is never {@code null}.

224 | * 225 | * @see #createLogger() 226 | */ 227 | protected final Logger logger; 228 | 229 | 230 | /* 231 | * Constructors. 232 | */ 233 | 234 | 235 | /** 236 | * Creates a new {@link Reflector}. 237 | * 238 | * @param a type that is both an appropriate kind of {@link 239 | * Listable} and {@link VersionWatchable}, such as the kind of 240 | * operation returned by {@link 241 | * DefaultKubernetesClient#configMaps()} and the like 242 | * 243 | * @param operation a {@link Listable} and a {@link 244 | * VersionWatchable} that can report information from a Kubernetes 245 | * cluster; must not be {@code null} 246 | * 247 | * @param eventCache an {@link EventCache} that will be 248 | * synchronized on and into which {@link Event}s will be 249 | * logically "reflected"; must not be {@code null} 250 | * 251 | * @exception NullPointerException if {@code operation} or {@code 252 | * eventCache} is {@code null} 253 | * 254 | * @exception IllegalStateException if the {@link #createLogger()} 255 | * method returns {@code null} 256 | * 257 | * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 258 | * Duration, Function) 259 | * 260 | * @see #start() 261 | */ 262 | @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 263 | public & VersionWatchable>> Reflector(final X operation, 264 | final EventCache eventCache) { 265 | this(operation, eventCache, null, null, null); 266 | } 267 | 268 | /** 269 | * Creates a new {@link Reflector}. 270 | * 271 | * @param a type that is both an appropriate kind of {@link 272 | * Listable} and {@link VersionWatchable}, such as the kind of 273 | * operation returned by {@link 274 | * DefaultKubernetesClient#configMaps()} and the like 275 | * 276 | * @param operation a {@link Listable} and a {@link 277 | * VersionWatchable} that can report information from a Kubernetes 278 | * cluster; must not be {@code null} 279 | * 280 | * @param eventCache an {@link EventCache} that will be 281 | * synchronized on and into which {@link Event}s will be 282 | * logically "reflected"; must not be {@code null} 283 | * 284 | * @param synchronizationInterval a {@link Duration} representing 285 | * the time in between one {@linkplain EventCache#synchronize() 286 | * synchronization operation} and another; interpreted with a 287 | * granularity of seconds; may be {@code null} or semantically equal 288 | * to {@code 0} seconds in which case no synchronization will occur 289 | * 290 | * @exception NullPointerException if {@code operation} or {@code 291 | * eventCache} is {@code null} 292 | * 293 | * @exception IllegalStateException if the {@link #createLogger()} 294 | * method returns {@code null} 295 | * 296 | * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 297 | * Duration, Function) 298 | * 299 | * @see #start() 300 | */ 301 | @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 302 | public & VersionWatchable>> Reflector(final X operation, 303 | final EventCache eventCache, 304 | final Duration synchronizationInterval) { 305 | this(operation, eventCache, null, synchronizationInterval, null); 306 | } 307 | 308 | /** 309 | * Creates a new {@link Reflector}. 310 | * 311 | * @param a type that is both an appropriate kind of {@link 312 | * Listable} and {@link VersionWatchable}, such as the kind of 313 | * operation returned by {@link 314 | * DefaultKubernetesClient#configMaps()} and the like 315 | * 316 | * @param operation a {@link Listable} and a {@link 317 | * VersionWatchable} that can report information from a Kubernetes 318 | * cluster; must not be {@code null} 319 | * 320 | * @param eventCache an {@link EventCache} that will be 321 | * synchronized on and into which {@link Event}s will be 322 | * logically "reflected"; must not be {@code null} 323 | * 324 | * @param synchronizationExecutorService a {@link 325 | * ScheduledExecutorService} to be used to tell the supplied {@link 326 | * EventCache} to {@linkplain EventCache#synchronize() synchronize} 327 | * on a schedule; may be {@code null} in which case no 328 | * synchronization will occur 329 | * 330 | * @param synchronizationInterval a {@link Duration} representing 331 | * the time in between one {@linkplain EventCache#synchronize() 332 | * synchronization operation} and another; may be {@code null} in 333 | * which case no synchronization will occur 334 | * 335 | * @exception NullPointerException if {@code operation} or {@code 336 | * eventCache} is {@code null} 337 | * 338 | * @exception IllegalStateException if the {@link #createLogger()} 339 | * method returns {@code null} 340 | * 341 | * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 342 | * Duration, Function) 343 | * 344 | * @see #start() 345 | */ 346 | @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 347 | public & VersionWatchable>> Reflector(final X operation, 348 | final EventCache eventCache, 349 | final ScheduledExecutorService synchronizationExecutorService, 350 | final Duration synchronizationInterval) { 351 | this(operation, eventCache, synchronizationExecutorService, synchronizationInterval, null); 352 | } 353 | 354 | /** 355 | * Creates a new {@link Reflector}. 356 | * 357 | * @param a type that is both an appropriate kind of {@link 358 | * Listable} and {@link VersionWatchable}, such as the kind of 359 | * operation returned by {@link 360 | * DefaultKubernetesClient#configMaps()} and the like 361 | * 362 | * @param operation a {@link Listable} and a {@link 363 | * VersionWatchable} that can report information from a Kubernetes 364 | * cluster; must not be {@code null} 365 | * 366 | * @param eventCache an {@link EventCache} that will be 367 | * synchronized on and into which {@link Event}s will be 368 | * logically "reflected"; must not be {@code null} 369 | * 370 | * @param synchronizationExecutorService a {@link 371 | * ScheduledExecutorService} to be used to tell the supplied {@link 372 | * EventCache} to {@linkplain EventCache#synchronize() synchronize} 373 | * on a schedule; may be {@code null} in which case no 374 | * synchronization will occur 375 | * 376 | * @param synchronizationInterval a {@link Duration} representing 377 | * the time in between one {@linkplain EventCache#synchronize() 378 | * synchronization operation} and another; may be {@code null} in 379 | * which case no synchronization will occur 380 | * 381 | * @param synchronizationErrorHandler a {@link Function} that 382 | * consumes a {@link Throwable} and returns a {@link Boolean} 383 | * indicating whether the error represented by the {@link Throwable} 384 | * in question was handled or not; may be {@code null} 385 | * 386 | * @exception NullPointerException if {@code operation} or {@code 387 | * eventCache} is {@code null} 388 | * 389 | * @exception IllegalStateException if the {@link #createLogger()} 390 | * method returns {@code null} 391 | * 392 | * @see #start() 393 | */ 394 | @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 395 | public & VersionWatchable>> Reflector(final X operation, 396 | final EventCache eventCache, 397 | final ScheduledExecutorService synchronizationExecutorService, 398 | final Duration synchronizationInterval, 399 | final Function synchronizationErrorHandler) { 400 | super(); 401 | this.logger = this.createLogger(); 402 | if (this.logger == null) { 403 | throw new IllegalStateException("createLogger() == null"); 404 | } 405 | final String cn = this.getClass().getName(); 406 | final String mn = ""; 407 | if (this.logger.isLoggable(Level.FINER)) { 408 | this.logger.entering(cn, mn, new Object[] { operation, eventCache, synchronizationExecutorService, synchronizationInterval }); 409 | } 410 | Objects.requireNonNull(operation); 411 | this.eventCache = Objects.requireNonNull(eventCache); 412 | // TODO: research: maybe: operation.withField("metadata.resourceVersion", "0")? 413 | this.operation = operation.withResourceVersion("0"); 414 | 415 | if (synchronizationInterval == null) { 416 | this.synchronizationIntervalInSeconds = 0L; 417 | } else { 418 | this.synchronizationIntervalInSeconds = synchronizationInterval.get(ChronoUnit.SECONDS); 419 | } 420 | if (this.synchronizationIntervalInSeconds <= 0L) { 421 | this.synchronizationExecutorService = null; 422 | this.shutdownSynchronizationExecutorServiceOnClose = false; 423 | this.synchronizationErrorHandler = null; 424 | } else { 425 | this.synchronizationExecutorService = synchronizationExecutorService; 426 | this.shutdownSynchronizationExecutorServiceOnClose = synchronizationExecutorService == null; 427 | if (synchronizationErrorHandler == null) { 428 | this.synchronizationErrorHandler = t -> { 429 | if (this.logger.isLoggable(Level.SEVERE)) { 430 | this.logger.logp(Level.SEVERE, 431 | this.getClass().getName(), "", 432 | t.getMessage(), t); 433 | } 434 | return true; 435 | }; 436 | } else { 437 | this.synchronizationErrorHandler = synchronizationErrorHandler; 438 | } 439 | } 440 | 441 | if (this.logger.isLoggable(Level.FINER)) { 442 | this.logger.exiting(cn, mn); 443 | } 444 | } 445 | 446 | 447 | /* 448 | * Instance methods. 449 | */ 450 | 451 | 452 | /** 453 | * Returns a {@link Logger} that will be used for this {@link 454 | * Reflector}. 455 | * 456 | *

This method never returns {@code null}.

457 | * 458 | *

Overrides of this method must not return {@code null}.

459 | * 460 | * @return a non-{@code null} {@link Logger} 461 | */ 462 | protected Logger createLogger() { 463 | return Logger.getLogger(this.getClass().getName()); 464 | } 465 | 466 | /** 467 | * Notionally closes this {@link Reflector} by terminating any 468 | * {@link Thread}s that it has started and invoking the {@link 469 | * #onClose()} method while holding this {@link Reflector}'s 470 | * monitor. 471 | * 472 | * @exception IOException if an error occurs 473 | * 474 | * @see #onClose() 475 | */ 476 | @Override 477 | public synchronized final void close() throws IOException { 478 | final String cn = this.getClass().getName(); 479 | final String mn = "close"; 480 | if (this.logger.isLoggable(Level.FINER)) { 481 | this.logger.entering(cn, mn); 482 | } 483 | 484 | try { 485 | this.closeSynchronizationExecutorService(); 486 | if (this.watch != null) { 487 | this.watch.close(); 488 | } 489 | } finally { 490 | this.onClose(); 491 | } 492 | 493 | if (this.logger.isLoggable(Level.FINER)) { 494 | this.logger.exiting(cn, mn); 495 | } 496 | } 497 | 498 | /** 499 | * {@linkplain Future#cancel(boolean) Cancels} scheduled invocations 500 | * of the {@link #synchronize()} method. 501 | * 502 | *

This method is invoked by the {@link 503 | * #closeSynchronizationExecutorService()} method.

504 | * 505 | * @see #setUpSynchronization() 506 | * 507 | * @see #closeSynchronizationExecutorService() 508 | */ 509 | private synchronized final void cancelSynchronization() { 510 | final String cn = this.getClass().getName(); 511 | final String mn = "cancelSynchronization"; 512 | if (this.logger.isLoggable(Level.FINER)) { 513 | this.logger.entering(cn, mn); 514 | } 515 | 516 | if (this.synchronizationTask != null) { 517 | this.synchronizationTask.cancel(true /* interrupt the task */); 518 | this.synchronizationTask = null; // very important; see setUpSynchronization() 519 | } 520 | 521 | if (this.logger.isLoggable(Level.FINER)) { 522 | this.logger.exiting(cn, mn); 523 | } 524 | } 525 | 526 | /** 527 | * {@linkplain #cancelSynchronization Cancels scheduled invocations 528 | * of the synchronize() method} and, when appropriate, 529 | * shuts down the {@link ScheduledExecutorService} responsible for 530 | * the scheduling. 531 | * 532 | * @see #cancelSynchronization() 533 | * 534 | * @see #setUpSynchronization() 535 | */ 536 | private synchronized final void closeSynchronizationExecutorService() { 537 | final String cn = this.getClass().getName(); 538 | final String mn = "closeSynchronizationExecutorService"; 539 | if (this.logger.isLoggable(Level.FINER)) { 540 | this.logger.entering(cn, mn); 541 | } 542 | 543 | this.cancelSynchronization(); 544 | 545 | if (this.synchronizationExecutorService != null && this.shutdownSynchronizationExecutorServiceOnClose) { 546 | 547 | // Stop accepting new tasks. Not that any will be showing up 548 | // anyway, but it's the right thing to do. 549 | this.synchronizationExecutorService.shutdown(); 550 | 551 | try { 552 | if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { 553 | this.synchronizationExecutorService.shutdownNow(); 554 | if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { 555 | if (this.logger.isLoggable(Level.WARNING)) { 556 | this.logger.logp(Level.WARNING, 557 | cn, mn, 558 | "synchronizationExecutorService did not terminate cleanly after 60 seconds"); 559 | } 560 | } 561 | } 562 | } catch (final InterruptedException interruptedException) { 563 | this.synchronizationExecutorService.shutdownNow(); 564 | Thread.currentThread().interrupt(); 565 | } 566 | 567 | } 568 | 569 | if (this.logger.isLoggable(Level.FINER)) { 570 | this.logger.exiting(cn, mn); 571 | } 572 | } 573 | 574 | /** 575 | * As the name implies, sets up synchronization, which is 576 | * the act of the downstream event cache telling its associated 577 | * event listeners that there are items remaining to be processed, 578 | * and returns a {@link Future} reprsenting the scheduled, repeating 579 | * task. 580 | * 581 | *

This method schedules repeated invocations of the {@link 582 | * #synchronize()} method.

583 | * 584 | *

This method may return {@code null}.

585 | * 586 | * @return a {@link Future} representing the scheduled repeating 587 | * synchronization task, or {@code null} if no such task was 588 | * scheduled 589 | * 590 | * @see #synchronize() 591 | * 592 | * @see EventCache#synchronize() 593 | */ 594 | private synchronized final Future setUpSynchronization() { 595 | final String cn = this.getClass().getName(); 596 | final String mn = "setUpSynchronization"; 597 | if (this.logger.isLoggable(Level.FINER)) { 598 | this.logger.entering(cn, mn); 599 | } 600 | 601 | if (this.synchronizationIntervalInSeconds > 0L) { 602 | if (this.synchronizationExecutorService == null || this.synchronizationExecutorService.isTerminated()) { 603 | this.synchronizationExecutorService = Executors.newScheduledThreadPool(1); 604 | if (this.synchronizationExecutorService instanceof ScheduledThreadPoolExecutor) { 605 | ((ScheduledThreadPoolExecutor)this.synchronizationExecutorService).setRemoveOnCancelPolicy(true); 606 | } 607 | } 608 | if (this.synchronizationTask == null) { 609 | if (this.logger.isLoggable(Level.INFO)) { 610 | this.logger.logp(Level.INFO, 611 | cn, mn, 612 | "Scheduling downstream synchronization every {0} seconds", 613 | Long.valueOf(this.synchronizationIntervalInSeconds)); 614 | } 615 | this.synchronizationTask = this.synchronizationExecutorService.scheduleWithFixedDelay(this::synchronize, 0L, this.synchronizationIntervalInSeconds, TimeUnit.SECONDS); 616 | } 617 | assert this.synchronizationExecutorService != null; 618 | assert this.synchronizationTask != null; 619 | } 620 | 621 | if (this.logger.isLoggable(Level.FINER)) { 622 | this.logger.exiting(cn, mn, this.synchronizationTask); 623 | } 624 | return this.synchronizationTask; 625 | } 626 | 627 | /** 628 | * Calls {@link EventCache#synchronize()} on this {@link 629 | * Reflector}'s {@linkplain #eventCache affiliated 630 | * EventCache}. 631 | * 632 | *

This method is normally invoked on a schedule by this {@link 633 | * Reflector}'s {@linkplain #synchronizationExecutorService 634 | * affiliated ScheduledExecutorService}.

635 | * 636 | * @see #setUpSynchronization() 637 | * 638 | * @see #shouldSynchronize() 639 | */ 640 | private final void synchronize() { 641 | final String cn = this.getClass().getName(); 642 | final String mn = "synchronize"; 643 | if (this.logger.isLoggable(Level.FINER)) { 644 | this.logger.entering(cn, mn); 645 | } 646 | 647 | if (this.shouldSynchronize()) { 648 | if (this.logger.isLoggable(Level.FINE)) { 649 | this.logger.logp(Level.FINE, 650 | cn, mn, 651 | "Synchronizing event cache with its downstream consumers"); 652 | } 653 | Throwable throwable = null; 654 | synchronized (this.eventCache) { 655 | try { 656 | 657 | // Tell the EventCache to run a synchronization operation. 658 | // This will have the effect of adding SynchronizationEvents 659 | // of type MODIFICATION to the EventCache. 660 | this.eventCache.synchronize(); 661 | 662 | } catch (final Throwable e) { 663 | assert e instanceof RuntimeException || e instanceof Error; 664 | throwable = e; 665 | } 666 | } 667 | if (throwable != null && !this.synchronizationErrorHandler.apply(throwable)) { 668 | if (throwable instanceof RuntimeException) { 669 | throw (RuntimeException)throwable; 670 | } else if (throwable instanceof Error) { 671 | throw (Error)throwable; 672 | } else { 673 | assert !(throwable instanceof Exception) : "Signature changed for EventCache#synchronize()"; 674 | } 675 | } 676 | } 677 | 678 | if (this.logger.isLoggable(Level.FINER)) { 679 | this.logger.exiting(cn, mn); 680 | } 681 | } 682 | 683 | /** 684 | * Returns whether, at any given moment, this {@link Reflector} 685 | * should cause its {@link EventCache} to {@linkplain 686 | * EventCache#synchronize() synchronize}. 687 | * 688 | *

The default implementation of this method returns {@code true} 689 | * if this {@link Reflector} was constructed with an explicit 690 | * synchronization interval or {@link ScheduledExecutorService} or 691 | * both.

692 | * 693 | *

Design Notes

694 | * 695 | *

This code follows the Go code in the Kubernetes {@code 696 | * client-go/tools/cache} package. One thing that becomes clear 697 | * when looking at all of this through an object-oriented lens is 698 | * that it is the {@link EventCache} (the {@code delta_fifo}, in the 699 | * Go code) that is ultimately in charge of synchronizing. It is 700 | * not clear why in the Go code this is a function of a reflector. 701 | * In an object-oriented world, perhaps the {@link EventCache} 702 | * itself should be in charge of resynchronization schedules, but we 703 | * choose to follow the Go code's division of responsibilities 704 | * here.

705 | * 706 | * @return {@code true} if this {@link Reflector} should cause its 707 | * {@link EventCache} to {@linkplain EventCache#synchronize() 708 | * synchronize}; {@code false} otherwise 709 | */ 710 | protected boolean shouldSynchronize() { 711 | final String cn = this.getClass().getName(); 712 | final String mn = "shouldSynchronize"; 713 | if (this.logger.isLoggable(Level.FINER)) { 714 | this.logger.entering(cn, mn); 715 | } 716 | final boolean returnValue; 717 | synchronized (this) { 718 | returnValue = this.synchronizationExecutorService != null; 719 | } 720 | if (this.logger.isLoggable(Level.FINER)) { 721 | this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 722 | } 723 | return returnValue; 724 | } 725 | 726 | // Not used; not used in the Go code either?! 727 | private final Object getLastSynchronizationResourceVersion() { 728 | return this.lastSynchronizationResourceVersion; 729 | } 730 | 731 | /** 732 | * Records the last resource version processed by a successful watch 733 | * operation. 734 | * 735 | * @param resourceVersion the resource version in question; may be 736 | * {@code null} 737 | * 738 | * @see WatchHandler#eventReceived(Watcher.Action, HasMetadata) 739 | */ 740 | private final void setLastSynchronizationResourceVersion(final Object resourceVersion) { 741 | // lastSynchronizationResourceVersion is volatile; this is an 742 | // atomic assignment 743 | this.lastSynchronizationResourceVersion = resourceVersion; 744 | } 745 | 746 | /** 747 | * Using the {@code operation} supplied at construction time, 748 | * {@linkplain Listable#list() lists} appropriate Kubernetes 749 | * resources, and then, on a separate {@link Thread}, {@linkplain 750 | * VersionWatchable sets up a watch} on them, calling {@link 751 | * EventCache#replace(Collection, Object)} and {@link 752 | * EventCache#add(Object, AbstractEvent.Type, HasMetadata)} methods 753 | * as appropriate. 754 | * 755 | *

For convenience only, this method returns a 756 | * {@link Future} representing any scheduled synchronization task 757 | * created as a result of the user's having supplied a {@link 758 | * Duration} at construction time. The return value may be (and 759 | * usually is) safely ignored. Invoking {@link 760 | * Future#cancel(boolean)} on the returned {@link Future} will 761 | * result in the scheduled synchronization task being cancelled 762 | * irrevocably. Notably, invoking {@link 763 | * Future#cancel(boolean)} on the returned {@link Future} will 764 | * not {@linkplain #close() close} this {@link 765 | * Reflector}. 766 | * 767 | *

This method never returns {@code null}.

768 | * 769 | *

The calling {@link Thread} is not blocked by invocations of 770 | * this method.

771 | * 772 | *

Implementation Notes

773 | * 774 | *

This method loosely models the {@code 776 | * Run} function in {@code reflector.go} together with the {@code 777 | * ListAndWatch} function in the same file.

778 | * 779 | * @return a {@link Future} representing a scheduled synchronization 780 | * operation; never {@code null} 781 | * 782 | * @exception IOException if a watch has previously been established 783 | * and could not be {@linkplain Watch#close() closed} 784 | * 785 | * @exception KubernetesClientException if the initial attempt to 786 | * {@linkplain Listable#list() list} Kubernetes resources fails 787 | * 788 | * @see #close() 789 | */ 790 | @NonBlocking 791 | public final Future start() throws IOException { 792 | final String cn = this.getClass().getName(); 793 | final String mn = "start"; 794 | if (this.logger.isLoggable(Level.FINER)) { 795 | this.logger.entering(cn, mn); 796 | } 797 | 798 | Future returnValue = null; 799 | synchronized (this) { 800 | 801 | try { 802 | 803 | // If somehow we got called while a watch already exists, then 804 | // close the old watch (we'll replace it). Note that, 805 | // critically, the onClose() method of our watch handler sets 806 | // this reference to null, so if the watch is in the process 807 | // of being closed, this little block won't be executed. 808 | if (this.watch != null) { 809 | final Closeable watch = this.watch; 810 | this.watch = null; 811 | if (logger.isLoggable(Level.FINE)) { 812 | logger.logp(Level.FINE, 813 | cn, mn, 814 | "Closing pre-existing watch"); 815 | } 816 | watch.close(); 817 | if (logger.isLoggable(Level.FINE)) { 818 | logger.logp(Level.FINE, 819 | cn, mn, 820 | "Closed pre-existing watch"); 821 | } 822 | } 823 | 824 | // Run a list operation, and get the resourceVersion of that list. 825 | if (logger.isLoggable(Level.FINE)) { 826 | logger.logp(Level.FINE, 827 | cn, mn, 828 | "Listing Kubernetes resources using {0}", this.operation); 829 | } 830 | @Issue(id = "13", uri = "https://github.com/microbean/microbean-kubernetes-controller/issues/13") 831 | @SuppressWarnings("unchecked") 832 | final KubernetesResourceList list = ((Listable>)this.operation).list(); 833 | assert list != null; 834 | 835 | final ListMeta metadata = list.getMetadata(); 836 | assert metadata != null; 837 | 838 | final String resourceVersion = metadata.getResourceVersion(); 839 | assert resourceVersion != null; 840 | 841 | // Using the results of that list operation, do a full replace 842 | // on the EventCache with them. 843 | final Collection replacementItems; 844 | final Collection items = list.getItems(); 845 | if (items == null || items.isEmpty()) { 846 | replacementItems = Collections.emptySet(); 847 | } else { 848 | replacementItems = Collections.unmodifiableCollection(new ArrayList<>(items)); 849 | } 850 | 851 | if (logger.isLoggable(Level.FINE)) { 852 | logger.logp(Level.FINE, cn, mn, "Replacing resources in the event cache"); 853 | } 854 | synchronized (this.eventCache) { 855 | this.eventCache.replace(replacementItems, resourceVersion); 856 | } 857 | if (logger.isLoggable(Level.FINE)) { 858 | logger.logp(Level.FINE, cn, mn, "Done replacing resources in the event cache"); 859 | } 860 | 861 | // Record the resource version we captured during our list 862 | // operation. 863 | this.setLastSynchronizationResourceVersion(resourceVersion); 864 | 865 | // Now that we've vetted that our list operation works (i.e. no 866 | // syntax errors, no connectivity problems) we can schedule 867 | // synchronizations if necessary. 868 | // 869 | // A synchronization is an operation where, if allowed, our 870 | // eventCache goes through its set of known objects and--for 871 | // any that are not enqueued for further processing 872 | // already--fires a *synchronization* event of type 873 | // MODIFICATION. This happens on a schedule, not in reaction 874 | // to an event. This allows its downstream processors a 875 | // chance to try to bring system state in line with desired 876 | // state, even if no events have occurred (kind of like a 877 | // heartbeat). See 878 | // https://engineering.bitnami.com/articles/a-deep-dive-into-kubernetes-controllers.html#resyncperiod. 879 | this.setUpSynchronization(); 880 | returnValue = this.synchronizationTask; 881 | 882 | // If there wasn't a synchronizationTask, then that means the 883 | // user who created this Reflector didn't want any 884 | // synchronization to happen. We return a "dummy" Future that 885 | // is already "completed" (isDone() returns true) to avoid 886 | // having to return null. The returned Future can be 887 | // cancelled with no effect. 888 | if (returnValue == null) { 889 | final FutureTask futureTask = new FutureTask(() -> {}, null); 890 | futureTask.run(); // just sets "doneness" 891 | assert futureTask.isDone(); 892 | assert !futureTask.isCancelled(); 893 | returnValue = futureTask; 894 | } 895 | 896 | assert returnValue != null; 897 | 898 | // Now that we've taken care of our list() operation, set up our 899 | // watch() operation. 900 | if (logger.isLoggable(Level.FINE)) { 901 | logger.logp(Level.FINE, 902 | cn, mn, 903 | "Watching Kubernetes resources with resource version {0} using {1}", 904 | new Object[] { resourceVersion, this.operation }); 905 | } 906 | @SuppressWarnings("unchecked") 907 | final Versionable>> versionableOperation = 908 | (Versionable>>)this.operation; 909 | this.watch = versionableOperation.withResourceVersion(resourceVersion).watch(new WatchHandler()); 910 | if (logger.isLoggable(Level.FINE)) { 911 | logger.logp(Level.FINE, 912 | cn, mn, 913 | "Established watch: {0}", this.watch); 914 | } 915 | 916 | } catch (final IOException | RuntimeException | Error exception) { 917 | this.cancelSynchronization(); 918 | if (this.watch != null) { 919 | try { 920 | // TODO: haven't seen it, but reason hard about deadlock 921 | // here; see 922 | // WatchHandler#onClose(KubernetesClientException) which 923 | // *can* call start() (this method) with the monitor. I 924 | // *think* we're in the clear here: 925 | // onClose(KubernetesClientException) will only (re-)call 926 | // start() if the supplied KubernetesClientException is 927 | // non-null. In this case, it should be, because this is 928 | // an ordinary close() call. 929 | this.watch.close(); 930 | } catch (final Throwable suppressMe) { 931 | exception.addSuppressed(suppressMe); 932 | } 933 | this.watch = null; 934 | } 935 | throw exception; 936 | } 937 | } 938 | 939 | if (this.logger.isLoggable(Level.FINER)) { 940 | this.logger.exiting(cn, mn, returnValue); 941 | } 942 | return returnValue; 943 | } 944 | 945 | /** 946 | * Invoked when {@link #close()} is invoked. 947 | * 948 | *

The default implementation of this method does nothing.

949 | * 950 | *

Overrides of this method must consider that they will be 951 | * invoked with this {@link Reflector}'s monitor held.

952 | * 953 | *

Overrides of this method must not call the {@link #close()} 954 | * method.

955 | * 956 | * @see #close() 957 | */ 958 | protected synchronized void onClose() { 959 | 960 | } 961 | 962 | 963 | /* 964 | * Inner and nested classes. 965 | */ 966 | 967 | 968 | /** 969 | * A {@link Watcher} of Kubernetes resources. 970 | * 971 | * @author Laird Nelson 973 | * 974 | * @see Watcher 975 | */ 976 | private final class WatchHandler implements Watcher { 977 | 978 | 979 | /* 980 | * Constructors. 981 | */ 982 | 983 | 984 | /** 985 | * Creates a new {@link WatchHandler}. 986 | */ 987 | private WatchHandler() { 988 | super(); 989 | final String cn = this.getClass().getName(); 990 | final String mn = ""; 991 | if (logger.isLoggable(Level.FINER)) { 992 | logger.entering(cn, mn); 993 | logger.exiting(cn, mn); 994 | } 995 | } 996 | 997 | 998 | /* 999 | * Instance methods. 1000 | */ 1001 | 1002 | 1003 | /** 1004 | * Calls the {@link EventCache#add(Object, AbstractEvent.Type, 1005 | * HasMetadata)} method on the enclosing {@link Reflector}'s 1006 | * associated {@link EventCache} with information harvested from 1007 | * the supplied {@code resource}, and using an {@link 1008 | * AbstractEvent.Type} selected appropriately given the supplied 1009 | * {@link Watcher.Action}. 1010 | * 1011 | * @param action the kind of Kubernetes event that happened; must 1012 | * not be {@code null} 1013 | * 1014 | * @param resource the {@link HasMetadata} object that was 1015 | * affected; must not be {@code null} 1016 | * 1017 | * @exception NullPointerException if {@code action} or {@code 1018 | * resource} was {@code null} 1019 | * 1020 | * @exception IllegalStateException if another error occurred 1021 | */ 1022 | @Override 1023 | public final void eventReceived(final Watcher.Action action, final T resource) { 1024 | final String cn = this.getClass().getName(); 1025 | final String mn = "eventReceived"; 1026 | if (logger.isLoggable(Level.FINER)) { 1027 | logger.entering(cn, mn, new Object[] { action, resource }); 1028 | } 1029 | Objects.requireNonNull(action); 1030 | Objects.requireNonNull(resource); 1031 | 1032 | final ObjectMeta metadata = resource.getMetadata(); 1033 | assert metadata != null; 1034 | 1035 | final Event.Type eventType; 1036 | switch (action) { 1037 | case ADDED: 1038 | eventType = Event.Type.ADDITION; 1039 | break; 1040 | case MODIFIED: 1041 | eventType = Event.Type.MODIFICATION; 1042 | break; 1043 | case DELETED: 1044 | eventType = Event.Type.DELETION; 1045 | break; 1046 | case ERROR: 1047 | // Uh...the Go code has: 1048 | // 1049 | // if event.Type == watch.Error { 1050 | // return apierrs.FromObject(event.Object) 1051 | // } 1052 | // 1053 | // Now, apierrs.FromObject is here: 1054 | // https://github.com/kubernetes/apimachinery/blob/kubernetes-1.9.2/pkg/api/errors/errors.go#L80-L88 1055 | // This is looking for a Status object. But 1056 | // WatchConnectionHandler will never forward on such a thing: 1057 | // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L246-L258 1058 | // 1059 | // So it follows that if by some chance we get here, resource 1060 | // will definitely be a HasMetadata. We go back to the Go 1061 | // code again, and remember that if the type is Error, the 1062 | // equivalent of this watch handler simply returns and goes home. 1063 | // 1064 | // Now, if we were to throw a RuntimeException here, which is 1065 | // the idiomatic equivalent of returning and going home, this 1066 | // would cause a watch reconnect: 1067 | // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L159-L205 1068 | // ...up to the reconnect limit. 1069 | // 1070 | // ...which is fine, but I'm not sure that in an error case a 1071 | // WatchEvent will ever HAVE a HasMetadata as its payload. 1072 | // Which means MAYBE we'll never get here. But if we do, all 1073 | // we can do is throw a RuntimeException...which ends up 1074 | // reducing to the same case as the default case below, so we 1075 | // fall through. 1076 | default: 1077 | eventType = null; 1078 | throw new IllegalStateException(); 1079 | } 1080 | assert eventType != null; 1081 | 1082 | // Add an Event of the proper kind to our EventCache. This is 1083 | // the heart of this method. 1084 | if (logger.isLoggable(Level.FINE)) { 1085 | logger.logp(Level.FINE, 1086 | cn, mn, 1087 | "Adding event to cache: {0} {1}", new Object[] { eventType, resource }); 1088 | } 1089 | synchronized (eventCache) { 1090 | eventCache.add(Reflector.this, eventType, resource); 1091 | } 1092 | 1093 | // Record the most recent resource version we're tracking to be 1094 | // that of this last successful watch() operation. We set it 1095 | // earlier during a list() operation. 1096 | setLastSynchronizationResourceVersion(metadata.getResourceVersion()); 1097 | 1098 | if (logger.isLoggable(Level.FINER)) { 1099 | logger.exiting(cn, mn); 1100 | } 1101 | } 1102 | 1103 | /** 1104 | * Invoked when the Kubernetes client connection closes. 1105 | * 1106 | * @param exception any {@link KubernetesClientException} that 1107 | * caused this closing to happen; may be {@code null} 1108 | */ 1109 | @Override 1110 | public final void onClose(final KubernetesClientException exception) { 1111 | final String cn = this.getClass().getName(); 1112 | final String mn = "onClose"; 1113 | if (logger.isLoggable(Level.FINER)) { 1114 | logger.entering(cn, mn, exception); 1115 | } 1116 | 1117 | synchronized (Reflector.this) { 1118 | // Don't close Reflector.this.watch before setting it to null 1119 | // here; after all we're being called because it's in the 1120 | // process of closing already! 1121 | Reflector.this.watch = null; 1122 | } 1123 | 1124 | if (exception != null) { 1125 | if (logger.isLoggable(Level.WARNING)) { 1126 | logger.logp(Level.WARNING, 1127 | cn, mn, 1128 | exception.getMessage(), exception); 1129 | } 1130 | // See 1131 | // https://github.com/kubernetes/client-go/blob/5f85fe426e7aa3c1df401a7ae6c1ba837bd76be9/tools/cache/reflector.go#L204. 1132 | if (logger.isLoggable(Level.INFO)) { 1133 | logger.logp(Level.INFO, cn, mn, "Restarting Reflector"); 1134 | } 1135 | try { 1136 | Reflector.this.start(); 1137 | } catch (final Throwable suppressMe) { 1138 | if (logger.isLoggable(Level.SEVERE)) { 1139 | logger.logp(Level.SEVERE, 1140 | cn, mn, 1141 | "Failed to restart Reflector", suppressMe); 1142 | } 1143 | exception.addSuppressed(suppressMe); 1144 | } 1145 | } 1146 | 1147 | if (logger.isLoggable(Level.FINER)) { 1148 | logger.exiting(cn, mn, exception); 1149 | } 1150 | } 1151 | 1152 | } 1153 | 1154 | } 1155 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/ResourceTrackingEventQueueConsumer.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.util.Map; 20 | import java.util.Objects; 21 | 22 | import java.util.function.Consumer; 23 | 24 | import java.util.logging.Level; 25 | import java.util.logging.Logger; 26 | 27 | import io.fabric8.kubernetes.api.model.HasMetadata; 28 | 29 | import net.jcip.annotations.GuardedBy; 30 | 31 | /** 32 | * A {@link Consumer} of {@link EventQueue}s that tracks the 33 | * Kubernetes resources they contain before allowing subclasses to 34 | * process their individual {@link Event}s. 35 | * 36 | *

Typically you would supply an implementation of this class to a 37 | * {@link Controller}.

38 | * 39 | * @param a Kubernetes resource type 40 | * 41 | * @author Laird Nelson 43 | * 44 | * @see #accept(AbstractEvent) 45 | * 46 | * @see Controller 47 | */ 48 | public abstract class ResourceTrackingEventQueueConsumer implements Consumer> { 49 | 50 | 51 | /* 52 | * Instance fields. 53 | */ 54 | 55 | 56 | /** 57 | * A mutable {@link Map} of {@link HasMetadata} objects indexed by 58 | * their keys (often a pairing of namespace and name). 59 | * 60 | *

This field may be {@code null} in which case no resource 61 | * tracking will take place.

62 | * 63 | *

The value of this field is {@linkplain 64 | * #ResourceTrackingEventQueueConsumer(Map) supplied at construction 65 | * time} and is synchronized on and written to, if 66 | * non-{@code null}, by the {@link #accept(EventQueue)} method.

67 | * 68 | *

This class synchronizes on this field's 69 | * value, if it is non-{@code null}, when mutating its 70 | * contents.

71 | */ 72 | @GuardedBy("itself") 73 | private final Map knownObjects; 74 | 75 | /** 76 | * A {@link Logger} for use by this {@link 77 | * ResourceTrackingEventQueueConsumer} implementation. 78 | * 79 | *

This field is never {@code null}.

80 | * 81 | * @see #createLogger() 82 | */ 83 | protected final Logger logger; 84 | 85 | 86 | /* 87 | * Constructors. 88 | */ 89 | 90 | 91 | /** 92 | * Creates a new {@link ResourceTrackingEventQueueConsumer}. 93 | * 94 | * @param knownObjects a mutable {@link Map} of {@link HasMetadata} 95 | * objects indexed by their keys (often a pairing of namespace and 96 | * name); may be {@code null} if deletion tracking is not needed; 97 | * will have its contents changed by this {@link 98 | * ResourceTrackingEventQueueConsumer}'s {@link #accept(EventQueue)} 99 | * method; will be synchronized on by this {@link 100 | * ResourceTrackingEventQueueConsumer}'s {@link #accept(EventQueue)} 101 | * method 102 | * 103 | * @see #accept(EventQueue) 104 | */ 105 | protected ResourceTrackingEventQueueConsumer(final Map knownObjects) { 106 | super(); 107 | this.logger = this.createLogger(); 108 | if (this.logger == null) { 109 | throw new IllegalStateException("createLogger() == null"); 110 | } 111 | final String cn = this.getClass().getName(); 112 | final String mn = ""; 113 | if (this.logger.isLoggable(Level.FINER)) { 114 | final String knownObjectsString; 115 | if (knownObjects == null) { 116 | knownObjectsString = null; 117 | } else { 118 | synchronized (knownObjects) { 119 | knownObjectsString = knownObjects.toString(); 120 | } 121 | } 122 | this.logger.entering(cn, mn, knownObjectsString); 123 | } 124 | this.knownObjects = knownObjects; 125 | if (this.logger.isLoggable(Level.FINER)) { 126 | this.logger.exiting(cn, mn); 127 | } 128 | } 129 | 130 | 131 | /* 132 | * Instance methods. 133 | */ 134 | 135 | 136 | /** 137 | * Returns a {@link Logger} for use with this {@link 138 | * ResourceTrackingEventQueueConsumer}. 139 | * 140 | *

This method never returns {@code null}.

141 | * 142 | *

Overrides of this method must not return {@code null}.

143 | * 144 | * @return a non-{@code null} {@link Logger} 145 | */ 146 | protected Logger createLogger() { 147 | return Logger.getLogger(this.getClass().getName()); 148 | } 149 | 150 | 151 | /** 152 | * {@linkplain EventQueue#iterator() Loops through} all the {@link 153 | * AbstractEvent}s in the supplied {@link EventQueue}, keeping track 154 | * of the {@link HasMetadata} it concerns along the way by 155 | * synchronizing on and writing to the {@link Map} 156 | * {@linkplain #ResourceTrackingEventQueueConsumer(Map) supplied at 157 | * construction time}. 158 | * 159 | *

Individual {@link AbstractEvent}s are forwarded on to the 160 | * {@link #accept(AbstractEvent)} method.

161 | * 162 | *

Implementation Notes

163 | * 164 | *

This loosely models the {@code 166 | * HandleDeltas} function in {@code 167 | * tools/cache/shared_informer.go}. The final distribution step 168 | * is left unimplemented on purpose.

169 | * 170 | * @param eventQueue the {@link EventQueue} to process; may be 171 | * {@code null} in which case no action will be taken 172 | * 173 | * @see #accept(AbstractEvent) 174 | */ 175 | @Override 176 | public final void accept(final EventQueue eventQueue) { 177 | final String cn = this.getClass().getName(); 178 | final String mn = "accept"; 179 | if (eventQueue == null) { 180 | if (this.logger.isLoggable(Level.FINER)) { 181 | this.logger.entering(cn, mn, null); 182 | } 183 | } else { 184 | synchronized (eventQueue) { 185 | if (this.logger.isLoggable(Level.FINER)) { 186 | this.logger.entering(cn, mn, eventQueue); 187 | } 188 | 189 | final Object key = eventQueue.getKey(); 190 | if (key == null) { 191 | throw new IllegalStateException("eventQueue.getKey() == null; eventQueue: " + eventQueue); 192 | } 193 | 194 | for (final AbstractEvent event : eventQueue) { 195 | if (event != null) { 196 | 197 | assert key.equals(event.getKey()); 198 | 199 | final Event.Type eventType = event.getType(); 200 | assert eventType != null; 201 | 202 | final T newResource = event.getResource(); 203 | 204 | if (event.getPriorResource() != null && this.logger.isLoggable(Level.FINE)) { 205 | this.logger.logp(Level.FINE, cn, mn, "Unexpected state; event has a priorResource: {0}", event.getPriorResource()); 206 | } 207 | 208 | final T priorResource; 209 | final AbstractEvent newEvent; 210 | 211 | if (this.knownObjects == null) { 212 | priorResource = null; 213 | newEvent = event; 214 | } else if (Event.Type.DELETION.equals(eventType)) { 215 | 216 | // "Forget" (untrack) the object in question. 217 | synchronized (this.knownObjects) { 218 | priorResource = this.knownObjects.remove(key); 219 | } 220 | 221 | newEvent = event; 222 | } else { 223 | assert eventType.equals(Event.Type.ADDITION) || eventType.equals(Event.Type.MODIFICATION); 224 | 225 | // "Learn" (track) the resource in question. 226 | synchronized (this.knownObjects) { 227 | priorResource = this.knownObjects.put(key, newResource); 228 | } 229 | 230 | if (event instanceof SynchronizationEvent) { 231 | if (priorResource == null) { 232 | assert Event.Type.ADDITION.equals(eventType) : "!Event.Type.ADDITION.equals(eventType): " + eventType; 233 | newEvent = event; 234 | } else { 235 | assert Event.Type.MODIFICATION.equals(eventType) : "!Event.Type.MODIFICATION.equals(eventType): " + eventType; 236 | newEvent = this.createSynchronizationEvent(Event.Type.MODIFICATION, priorResource, newResource); 237 | } 238 | } else if (priorResource == null) { 239 | if (Event.Type.ADDITION.equals(eventType)) { 240 | newEvent = event; 241 | } else { 242 | newEvent = this.createEvent(Event.Type.ADDITION, null, newResource); 243 | } 244 | } else { 245 | newEvent = this.createEvent(Event.Type.MODIFICATION, priorResource, newResource); 246 | } 247 | } 248 | 249 | assert newEvent != null; 250 | assert newEvent instanceof SynchronizationEvent || newEvent instanceof Event; 251 | 252 | // This is the final consumption/distribution step; it is 253 | // an abstract method in this class. 254 | this.accept(newEvent); 255 | 256 | } 257 | } 258 | 259 | } 260 | } 261 | if (this.logger.isLoggable(Level.FINER)) { 262 | this.logger.exiting(cn, mn); 263 | } 264 | } 265 | 266 | /** 267 | * Creates and returns a new {@link Event}. 268 | * 269 | *

This method never returns {@code null}.

270 | * 271 | *

Overrides of this method must not return {@code null}.

272 | * 273 | * @param eventType the {@link AbstractEvent.Type} for the new 274 | * {@link Event}; must not be {@code null}; when supplied by the 275 | * {@link #accept(EventQueue)} method's internals, will always be 276 | * either {@link AbstractEvent.Type#ADDITION} or {@link 277 | * AbstractEvent.Type#MODIFICATION} 278 | * 279 | * @param priorResource the prior state of the resource the new 280 | * {@link Event} will represent; may be (and often is) {@code null} 281 | * 282 | * @param resource the latest state of the resource the new {@link 283 | * Event} will represent; must not be {@code null} 284 | * 285 | * @return a new, non-{@code null} {@link Event} with each 286 | * invocation 287 | * 288 | * @exception NullPointerException if {@code eventType} or {@code 289 | * resource} is {@code null} 290 | */ 291 | protected Event createEvent(final Event.Type eventType, final T priorResource, final T resource) { 292 | final String cn = this.getClass().getName(); 293 | final String mn = "createEvent"; 294 | if (this.logger.isLoggable(Level.FINER)) { 295 | this.logger.entering(cn, mn, new Object[] { eventType, priorResource, resource }); 296 | } 297 | Objects.requireNonNull(eventType); 298 | final Event returnValue = new Event<>(this, eventType, priorResource, resource); 299 | if (this.logger.isLoggable(Level.FINER)) { 300 | this.logger.exiting(cn, mn, returnValue); 301 | } 302 | return returnValue; 303 | } 304 | 305 | /** 306 | * Creates and returns a new {@link SynchronizationEvent}. 307 | * 308 | *

This method never returns {@code null}.

309 | * 310 | *

Overrides of this method must not return {@code null}.

311 | * 312 | * @param eventType the {@link AbstractEvent.Type} for the new 313 | * {@link SynchronizationEvent}; must not be {@code null}; when 314 | * supplied by the {@link #accept(EventQueue)} method's internals, 315 | * will always be {@link AbstractEvent.Type#MODIFICATION} 316 | * 317 | * @param priorResource the prior state of the resource the new 318 | * {@link SynchronizationEvent} will represent; may be (and often 319 | * is) {@code null} 320 | * 321 | * @param resource the latest state of the resource the new {@link 322 | * SynchronizationEvent} will represent; must not be {@code null} 323 | * 324 | * @return a new, non-{@code null} {@link SynchronizationEvent} with 325 | * each invocation 326 | * 327 | * @exception NullPointerException if {@code eventType} or {@code 328 | * resource} is {@code null} 329 | */ 330 | protected SynchronizationEvent createSynchronizationEvent(final Event.Type eventType, final T priorResource, final T resource) { 331 | final String cn = this.getClass().getName(); 332 | final String mn = "createSynchronizationEvent"; 333 | if (this.logger.isLoggable(Level.FINER)) { 334 | this.logger.entering(cn, mn, new Object[] { eventType, priorResource, resource }); 335 | } 336 | Objects.requireNonNull(eventType); 337 | final SynchronizationEvent returnValue = new SynchronizationEvent<>(this, eventType, priorResource, resource); 338 | if (this.logger.isLoggable(Level.FINER)) { 339 | this.logger.exiting(cn, mn, returnValue); 340 | } 341 | return returnValue; 342 | } 343 | 344 | /** 345 | * Called to process a given {@link AbstractEvent} from the {@link 346 | * EventQueue} supplied to the {@link #accept(EventQueue)} method, 347 | * with that {@link EventQueue}'s monitor held. 348 | * 349 | *

Implementations of this method should be relatively fast as 350 | * this method dictates the speed of {@link EventQueue} 351 | * processing.

352 | * 353 | * @param event the {@link AbstractEvent} encountered in the {@link 354 | * EventQueue}; must not be {@code null} 355 | * 356 | * @exception NullPointerException if {@code event} is {@code null} 357 | * 358 | * @see #accept(EventQueue) 359 | */ 360 | protected abstract void accept(final AbstractEvent event); 361 | 362 | } 363 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/SynchronizationEvent.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.Serializable; // for javadoc only 20 | 21 | import java.util.EventObject; 22 | 23 | import io.fabric8.kubernetes.api.model.HasMetadata; 24 | 25 | /** 26 | * An {@link AbstractEvent} that describes an {@link EventCache} 27 | * synchronization event. 28 | * 29 | * @param a type of Kubernetes resource 30 | * 31 | * @author Laird Nelson 33 | * 34 | * @see EventCache 35 | */ 36 | public class SynchronizationEvent extends AbstractEvent { 37 | 38 | 39 | /* 40 | * Static fields. 41 | */ 42 | 43 | 44 | /** 45 | * The version of this class for {@linkplain Serializable 46 | * serialization purposes}. 47 | * 48 | * @see Serializable 49 | */ 50 | private static final long serialVersionUID = 1L; 51 | 52 | 53 | /* 54 | * Constructors. 55 | */ 56 | 57 | 58 | /** 59 | * Creates a new {@link SynchronizationEvent}. 60 | * 61 | * @param source the creator; must not be {@code null} 62 | * 63 | * @param type the {@link Type} of this {@link 64 | * SynchronizationEvent}; must not be {@code null}; must not be 65 | * {@link Type#DELETION} 66 | * 67 | * @param priorResource a {@link HasMetadata} representing the 68 | * prior state of the {@linkplain #getResource() Kubernetes 69 | * resource this Event primarily concerns}; may 70 | * be—and often is—null 71 | * 72 | * @param resource a {@link HasMetadata} representing a Kubernetes 73 | * resource; must not be {@code null} 74 | * 75 | * @exception NullPointerException if {@code source}, {@code type} 76 | * or {@code resource} is {@code null} 77 | * 78 | * @exception IllegalArgumentException if {@link Type#DELETION} is 79 | * equal to {@code type} 80 | * 81 | * @see Type 82 | * 83 | * @see EventObject#getSource() 84 | */ 85 | public SynchronizationEvent(final Object source, final Type type, final T priorResource, final T resource) { 86 | super(source, type, priorResource, resource); 87 | if (Type.DELETION.equals(type)) { 88 | throw new IllegalArgumentException("DELETION.equals(type): " + type); 89 | } 90 | } 91 | 92 | 93 | /* 94 | * Instance methods. 95 | */ 96 | 97 | 98 | /** 99 | * Returns {@code true} if the supplied {@link Object} is also a 100 | * {@link SynchronizationEvent} and is equal in every respect to 101 | * this one. 102 | * 103 | * @param other the {@link Object} to test; may be {@code null} in 104 | * which case {@code false} will be returned 105 | * 106 | * @return {@code true} if the supplied {@link Object} is also a 107 | * {@link SynchronizationEvent} and is equal in every respect to 108 | * this one; {@code false} otherwise 109 | */ 110 | @Override 111 | public boolean equals(final Object other) { 112 | if (other == this) { 113 | return true; 114 | } else if (other instanceof SynchronizationEvent) { 115 | 116 | final boolean superEquals = super.equals(other); 117 | if (!superEquals) { 118 | return false; 119 | } 120 | 121 | return true; 122 | } else { 123 | return false; 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/org/microbean/kubernetes/controller/package-info.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | 18 | /** 19 | * Provides classes and interfaces assisting in the writing of 20 | * Kubernetes controllers. 21 | * 22 | * @author Laird Nelson 24 | * 25 | * @see org.microbean.kubernetes.controller.Controller 26 | * 27 | * @see org.microbean.kubernetes.controller.EventCache 28 | * 29 | * @see org.microbean.kubernetes.controller.EventQueueCollection 30 | * 31 | * @see org.microbean.kubernetes.controller.EventQueue 32 | * 33 | * @see org.microbean.kubernetes.controller.Reflector 34 | */ 35 | @org.microbean.development.annotation.License( 36 | name = "Apache License 2.0", 37 | uri = "https://www.apache.org/licenses/LICENSE-2.0" 38 | ) 39 | package org.microbean.kubernetes.controller; 40 | -------------------------------------------------------------------------------- /src/main/javadoc/css/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Javadoc style sheet */ 2 | /* 3 | Overall document style 4 | */ 5 | 6 | @import url('https://fonts.googleapis.com/css?family=Lobster|Roboto'); 7 | 8 | body { 9 | background-color:#ffffff; 10 | color:#353833; 11 | font-family: Arial, Helvetica, sans-serif; 12 | font-size:14px; 13 | margin:0; 14 | } 15 | a:link, a:visited { 16 | text-decoration:none; 17 | color:#4A6782; 18 | } 19 | a:hover, a:focus { 20 | text-decoration:none; 21 | color:#bb7a2a; 22 | } 23 | a:active { 24 | text-decoration:none; 25 | color:#4A6782; 26 | } 27 | a[name] { 28 | color:#353833; 29 | } 30 | a[name]:hover { 31 | text-decoration:none; 32 | color:#353833; 33 | } 34 | pre { 35 | font-family: monospace; 36 | font-size:14px; 37 | } 38 | h1 { 39 | font-size:20px; 40 | } 41 | h2 { 42 | font-size:18px; 43 | } 44 | h3 { 45 | font-size:16px; 46 | font-style:italic; 47 | } 48 | h4 { 49 | font-size:13px; 50 | } 51 | h5 { 52 | font-size:12px; 53 | } 54 | h6 { 55 | font-size:11px; 56 | } 57 | ul { 58 | list-style-type:disc; 59 | } 60 | code, tt { 61 | font-family:monospace; 62 | font-size:14px; 63 | padding-top:4px; 64 | margin-top:8px; 65 | line-height:1.4em; 66 | } 67 | dt code { 68 | font-family:monospace; 69 | font-size:14px; 70 | padding-top:4px; 71 | } 72 | table tr td dt code { 73 | font-family:monospace; 74 | font-size:14px; 75 | vertical-align:top; 76 | padding-top:4px; 77 | } 78 | sup { 79 | font-size:8px; 80 | } 81 | /* 82 | Document title and Copyright styles 83 | */ 84 | .clear { 85 | clear:both; 86 | height:0px; 87 | overflow:hidden; 88 | } 89 | .aboutLanguage { 90 | float:right; 91 | padding:0px 21px; 92 | font-size:11px; 93 | z-index:200; 94 | margin-top:-9px; 95 | } 96 | .legalCopy { 97 | margin-left:.5em; 98 | } 99 | .bar a, .bar a:link, .bar a:visited, .bar a:active { 100 | color:#FFFFFF; 101 | text-decoration:none; 102 | } 103 | .bar a:hover, .bar a:focus { 104 | color:#bb7a2a; 105 | } 106 | .tab { 107 | background-color:#0066FF; 108 | color:#ffffff; 109 | padding:8px; 110 | width:5em; 111 | font-weight:bold; 112 | } 113 | /* 114 | Navigation bar styles 115 | */ 116 | .bar { 117 | background-color:#4D7A97; 118 | color:#FFFFFF; 119 | padding:.8em .5em .4em .8em; 120 | height:auto;/*height:1.8em;*/ 121 | font-size:11px; 122 | margin:0; 123 | } 124 | .topNav { 125 | background-color:#4D7A97; 126 | color:#FFFFFF; 127 | float:left; 128 | padding:0; 129 | width:100%; 130 | clear:right; 131 | height:2.8em; 132 | padding-top:10px; 133 | overflow:hidden; 134 | font-size:12px; 135 | } 136 | .bottomNav { 137 | margin-top:10px; 138 | background-color:#4D7A97; 139 | color:#FFFFFF; 140 | float:left; 141 | padding:0; 142 | width:100%; 143 | clear:right; 144 | height:2.8em; 145 | padding-top:10px; 146 | overflow:hidden; 147 | font-size:12px; 148 | } 149 | .subNav { 150 | background-color:#dee3e9; 151 | float:left; 152 | width:100%; 153 | overflow:hidden; 154 | font-size:12px; 155 | } 156 | .subNav div { 157 | clear:left; 158 | float:left; 159 | padding:0 0 5px 6px; 160 | text-transform:uppercase; 161 | } 162 | ul.navList, ul.subNavList { 163 | float:left; 164 | margin:0 25px 0 0; 165 | padding:0; 166 | } 167 | ul.navList li{ 168 | list-style:none; 169 | float:left; 170 | padding: 5px 6px; 171 | text-transform:uppercase; 172 | } 173 | ul.subNavList li{ 174 | list-style:none; 175 | float:left; 176 | } 177 | .topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { 178 | color:#FFFFFF; 179 | text-decoration:none; 180 | text-transform:uppercase; 181 | } 182 | .topNav a:hover, .bottomNav a:hover { 183 | text-decoration:none; 184 | color:#bb7a2a; 185 | text-transform:uppercase; 186 | } 187 | .navBarCell1Rev { 188 | background-color:#F8981D; 189 | color:#253441; 190 | margin: auto 5px; 191 | } 192 | .skipNav { 193 | position:absolute; 194 | top:auto; 195 | left:-9999px; 196 | overflow:hidden; 197 | } 198 | /* 199 | Page header and footer styles 200 | */ 201 | .header, .footer { 202 | clear:both; 203 | margin:0 20px; 204 | padding:5px 0 0 0; 205 | } 206 | .indexHeader { 207 | margin:10px; 208 | position:relative; 209 | } 210 | .indexHeader span{ 211 | margin-right:15px; 212 | } 213 | .indexHeader h1 { 214 | font-size:13px; 215 | } 216 | .title { 217 | color:#2c4557; 218 | margin:10px 0; 219 | } 220 | .subTitle { 221 | margin:5px 0 0 0; 222 | } 223 | .header ul { 224 | margin:0 0 15px 0; 225 | padding:0; 226 | } 227 | .footer ul { 228 | margin:20px 0 5px 0; 229 | } 230 | .header ul li, .footer ul li { 231 | list-style:none; 232 | font-size:13px; 233 | } 234 | /* 235 | Heading styles 236 | */ 237 | div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { 238 | background-color:#dee3e9; 239 | border:1px solid #d0d9e0; 240 | margin:0 0 6px -8px; 241 | padding:7px 5px; 242 | } 243 | ul.blockList ul.blockList ul.blockList li.blockList h3 { 244 | background-color:#dee3e9; 245 | border:1px solid #d0d9e0; 246 | margin:0 0 6px -8px; 247 | padding:7px 5px; 248 | } 249 | ul.blockList ul.blockList li.blockList h3 { 250 | padding:0; 251 | margin:15px 0; 252 | } 253 | ul.blockList li.blockList h2 { 254 | padding:0px 0 20px 0; 255 | } 256 | /* 257 | Page layout container styles 258 | */ 259 | .contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { 260 | clear:both; 261 | padding:10px 20px; 262 | position:relative; 263 | } 264 | .indexContainer { 265 | margin:10px; 266 | position:relative; 267 | font-size:12px; 268 | } 269 | .indexContainer h2 { 270 | font-size:13px; 271 | padding:0 0 3px 0; 272 | } 273 | .indexContainer ul { 274 | margin:0; 275 | padding:0; 276 | } 277 | .indexContainer ul li { 278 | list-style:none; 279 | padding-top:2px; 280 | } 281 | .contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { 282 | font-size:12px; 283 | font-weight:bold; 284 | margin:10px 0 0 0; 285 | color:#4E4E4E; 286 | } 287 | .contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { 288 | margin:5px 0 10px 0px; 289 | font-size:14px; 290 | font-family:monospace; 291 | } 292 | .serializedFormContainer dl.nameValue dt { 293 | margin-left:1px; 294 | font-size:1.1em; 295 | display:inline; 296 | font-weight:bold; 297 | } 298 | .serializedFormContainer dl.nameValue dd { 299 | margin:0 0 0 1px; 300 | font-size:1.1em; 301 | display:inline; 302 | } 303 | /* 304 | List styles 305 | */ 306 | ul.horizontal li { 307 | display:inline; 308 | font-size:0.9em; 309 | } 310 | ul.inheritance { 311 | margin:0; 312 | padding:0; 313 | } 314 | ul.inheritance li { 315 | display:inline; 316 | list-style:none; 317 | } 318 | ul.inheritance li ul.inheritance { 319 | margin-left:15px; 320 | padding-left:15px; 321 | padding-top:1px; 322 | } 323 | ul.blockList, ul.blockListLast { 324 | margin:10px 0 10px 0; 325 | padding:0; 326 | } 327 | ul.blockList li.blockList, ul.blockListLast li.blockList { 328 | list-style:none; 329 | margin-bottom:15px; 330 | line-height:1.4; 331 | } 332 | ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { 333 | padding:0px 20px 5px 10px; 334 | border:1px solid #ededed; 335 | background-color:#f8f8f8; 336 | } 337 | ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { 338 | padding:0 0 5px 8px; 339 | background-color:#ffffff; 340 | border:none; 341 | } 342 | ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { 343 | margin-left:0; 344 | padding-left:0; 345 | padding-bottom:15px; 346 | border:none; 347 | } 348 | ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { 349 | list-style:none; 350 | border-bottom:none; 351 | padding-bottom:0; 352 | } 353 | table tr td dl, table tr td dl dt, table tr td dl dd { 354 | margin-top:0; 355 | margin-bottom:1px; 356 | } 357 | /* 358 | Table styles 359 | */ 360 | .overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary { 361 | width:100%; 362 | border-left:1px solid #EEE; 363 | border-right:1px solid #EEE; 364 | border-bottom:1px solid #EEE; 365 | } 366 | .overviewSummary, .memberSummary { 367 | padding:0px; 368 | } 369 | .overviewSummary caption, .memberSummary caption, .typeSummary caption, 370 | .useSummary caption, .constantsSummary caption, .deprecatedSummary caption { 371 | position:relative; 372 | text-align:left; 373 | background-repeat:no-repeat; 374 | color:#253441; 375 | font-weight:bold; 376 | clear:none; 377 | overflow:hidden; 378 | padding:0px; 379 | padding-top:10px; 380 | padding-left:1px; 381 | margin:0px; 382 | white-space:pre; 383 | } 384 | .overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link, 385 | .useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link, 386 | .overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover, 387 | .useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover, 388 | .overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active, 389 | .useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active, 390 | .overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited, 391 | .useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited { 392 | color:#FFFFFF; 393 | } 394 | .overviewSummary caption span, .memberSummary caption span, .typeSummary caption span, 395 | .useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span { 396 | white-space:nowrap; 397 | padding-top:5px; 398 | padding-left:12px; 399 | padding-right:12px; 400 | padding-bottom:7px; 401 | display:inline-block; 402 | float:left; 403 | background-color:#F8981D; 404 | border: none; 405 | height:16px; 406 | } 407 | .memberSummary caption span.activeTableTab span { 408 | white-space:nowrap; 409 | padding-top:5px; 410 | padding-left:12px; 411 | padding-right:12px; 412 | margin-right:3px; 413 | display:inline-block; 414 | float:left; 415 | background-color:#F8981D; 416 | height:16px; 417 | } 418 | .memberSummary caption span.tableTab span { 419 | white-space:nowrap; 420 | padding-top:5px; 421 | padding-left:12px; 422 | padding-right:12px; 423 | margin-right:3px; 424 | display:inline-block; 425 | float:left; 426 | background-color:#4D7A97; 427 | height:16px; 428 | } 429 | .memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab { 430 | padding-top:0px; 431 | padding-left:0px; 432 | padding-right:0px; 433 | background-image:none; 434 | float:none; 435 | display:inline; 436 | } 437 | .overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd, 438 | .useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd { 439 | display:none; 440 | width:5px; 441 | position:relative; 442 | float:left; 443 | background-color:#F8981D; 444 | } 445 | .memberSummary .activeTableTab .tabEnd { 446 | display:none; 447 | width:5px; 448 | margin-right:3px; 449 | position:relative; 450 | float:left; 451 | background-color:#F8981D; 452 | } 453 | .memberSummary .tableTab .tabEnd { 454 | display:none; 455 | width:5px; 456 | margin-right:3px; 457 | position:relative; 458 | background-color:#4D7A97; 459 | float:left; 460 | 461 | } 462 | .overviewSummary td, .memberSummary td, .typeSummary td, 463 | .useSummary td, .constantsSummary td, .deprecatedSummary td { 464 | text-align:left; 465 | padding:0px 0px 12px 10px; 466 | } 467 | th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th, 468 | td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{ 469 | vertical-align:top; 470 | padding-right:0px; 471 | padding-top:8px; 472 | padding-bottom:3px; 473 | } 474 | th.colFirst, th.colLast, th.colOne, .constantsSummary th { 475 | background:#dee3e9; 476 | text-align:left; 477 | padding:8px 3px 3px 7px; 478 | } 479 | td.colFirst, th.colFirst { 480 | white-space:nowrap; 481 | font-size:13px; 482 | } 483 | td.colLast, th.colLast { 484 | font-size:13px; 485 | } 486 | td.colOne, th.colOne { 487 | font-size:13px; 488 | } 489 | .overviewSummary td.colFirst, .overviewSummary th.colFirst, 490 | .useSummary td.colFirst, .useSummary th.colFirst, 491 | .overviewSummary td.colOne, .overviewSummary th.colOne, 492 | .memberSummary td.colFirst, .memberSummary th.colFirst, 493 | .memberSummary td.colOne, .memberSummary th.colOne, 494 | .typeSummary td.colFirst{ 495 | width:25%; 496 | vertical-align:top; 497 | } 498 | td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { 499 | font-weight:bold; 500 | } 501 | .tableSubHeadingColor { 502 | background-color:#EEEEFF; 503 | } 504 | .altColor { 505 | background-color:#FFFFFF; 506 | } 507 | .rowColor { 508 | background-color:#EEEEEF; 509 | } 510 | /* 511 | Content styles 512 | */ 513 | .description pre { 514 | margin-top:0; 515 | } 516 | .deprecatedContent { 517 | margin:0; 518 | padding:10px 0; 519 | } 520 | .docSummary { 521 | padding:0; 522 | } 523 | 524 | ul.blockList ul.blockList ul.blockList li.blockList h3 { 525 | font-style:normal; 526 | } 527 | 528 | div.block { 529 | font-size:14px; 530 | font-family:Georgia, "Times New Roman", Times, serif; 531 | } 532 | 533 | td.colLast div { 534 | padding-top:0px; 535 | } 536 | 537 | 538 | td.colLast a { 539 | padding-bottom:3px; 540 | } 541 | /* 542 | Formatting effect styles 543 | */ 544 | .sourceLineNo { 545 | color:green; 546 | padding:0 30px 0 0; 547 | } 548 | h1.hidden { 549 | visibility:hidden; 550 | overflow:hidden; 551 | font-size:10px; 552 | } 553 | .block { 554 | display:block; 555 | margin:3px 10px 2px 0px; 556 | color:#474747; 557 | } 558 | .deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink, 559 | .overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel, 560 | .seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink { 561 | font-weight:bold; 562 | } 563 | .deprecationComment, .emphasizedPhrase, .interfaceName { 564 | font-style:italic; 565 | } 566 | 567 | div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase, 568 | div.block div.block span.interfaceName { 569 | font-style:normal; 570 | } 571 | 572 | div.contentContainer ul.blockList li.blockList h2{ 573 | padding-bottom:0px; 574 | } 575 | -------------------------------------------------------------------------------- /src/main/javadoc/overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Provides classes and interfaces for assisting in the writing of 4 | Kubernetes controllers.

5 | 6 | @author Laird Nelson 8 | 9 | @see org.microbean.kubernetes.controller.EventCache 10 | 11 | @see org.microbean.kubernetes.controller.EventQueueCollection 12 | 13 | @see org.microbean.kubernetes.controller.EventQueue 14 | 15 | @see org.microbean.kubernetes.controller.Reflector 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/site/markdown/index.md.vm: -------------------------------------------------------------------------------- 1 | #include("../../../README.md") 2 | -------------------------------------------------------------------------------- /src/site/resources/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lobster|Roboto'); 2 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | μb ${project.artifactId} 11 | https://avatars0.githubusercontent.com/u/25515632?s=60 12 | ${project.url} 13 | 14 | 15 | 16 | org.apache.maven.skins 17 | maven-fluido-skin 18 | 1.6 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | false 34 | true 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/spotbugs/exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/test/java/org/microbean/kubernetes/controller/TestInterruptionBehavior.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.util.concurrent.Future; 20 | import java.util.concurrent.ScheduledThreadPoolExecutor; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | import org.junit.Test; 24 | 25 | import static org.junit.Assert.assertEquals; 26 | import static org.junit.Assert.assertFalse; 27 | import static org.junit.Assert.assertTrue; 28 | 29 | public class TestInterruptionBehavior { 30 | 31 | public TestInterruptionBehavior() { 32 | super(); 33 | } 34 | 35 | @Test 36 | public void testInterruptionAndScheduledThreadPoolExecutorInteraction() throws Exception { 37 | final ScheduledThreadPoolExecutor e = new ScheduledThreadPoolExecutor(1); 38 | final Future task = e.scheduleWithFixedDelay(() -> { 39 | assertFalse(Thread.currentThread().isInterrupted()); 40 | while (!Thread.currentThread().isInterrupted()) { 41 | try { 42 | synchronized (TestInterruptionBehavior.this) { 43 | TestInterruptionBehavior.this.wait(); 44 | } 45 | assertFalse(Thread.currentThread().isInterrupted()); 46 | } catch (final InterruptedException interruptedException) { 47 | Thread.currentThread().interrupt(); 48 | assertTrue(Thread.currentThread().isInterrupted()); 49 | } catch (final RuntimeException runtimeException) { 50 | runtimeException.printStackTrace(); 51 | throw runtimeException; 52 | } 53 | } 54 | assertTrue(Thread.currentThread().isInterrupted()); 55 | }, 0L, 1L, TimeUnit.MILLISECONDS); 56 | assertTrue(task.cancel(true)); // should interrupt 57 | e.shutdown(); 58 | assertTrue(e.awaitTermination(2L, TimeUnit.SECONDS)); 59 | e.shutdownNow(); 60 | assertTrue(e.awaitTermination(2L, TimeUnit.SECONDS)); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/org/microbean/kubernetes/controller/TestReflectorBasics.java: -------------------------------------------------------------------------------- 1 | /* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 2 | * 3 | * Copyright © 2017-2018 microBean. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14 | * implied. See the License for the specific language governing 15 | * permissions and limitations under the License. 16 | */ 17 | package org.microbean.kubernetes.controller; 18 | 19 | import java.io.Closeable; 20 | 21 | import java.time.Duration; 22 | 23 | import java.util.Collection; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Set; 29 | 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.ScheduledExecutorService; 32 | 33 | import java.util.function.Consumer; 34 | 35 | import io.fabric8.kubernetes.api.model.ConfigMap; 36 | import io.fabric8.kubernetes.api.model.ConfigMapList; 37 | import io.fabric8.kubernetes.api.model.HasMetadata; 38 | import io.fabric8.kubernetes.api.model.KubernetesResourceList; 39 | import io.fabric8.kubernetes.api.model.Pod; 40 | 41 | import io.fabric8.kubernetes.client.DefaultKubernetesClient; 42 | 43 | import io.fabric8.kubernetes.client.dsl.Listable; 44 | import io.fabric8.kubernetes.client.dsl.VersionWatchable; 45 | 46 | import org.junit.Ignore; 47 | import org.junit.Test; 48 | 49 | import static org.junit.Assert.assertNotNull; 50 | import static org.junit.Assert.assertFalse; 51 | import static org.junit.Assert.assertTrue; 52 | import static org.junit.Assume.assumeFalse; 53 | 54 | import io.fabric8.kubernetes.client.Watcher; 55 | 56 | public class TestReflectorBasics { 57 | 58 | public TestReflectorBasics() { 59 | super(); 60 | } 61 | 62 | @Test 63 | public void testBasics() throws Exception { 64 | assumeFalse(Boolean.getBoolean("skipClusterTests")); 65 | 66 | // We'll use this as our "known objects". 67 | final Map configMaps = new HashMap<>(); 68 | 69 | // Create a new EventCache implementation that "knows about" our 70 | // known objects. 71 | final EventQueueCollection eventQueues = new EventQueueCollection<>(configMaps, 16, 0.75f); 72 | 73 | // Create a consumer that can remove and process EventQueues from 74 | // our EventCache implementation. It will also update our "known 75 | // objects" as necessary. 76 | final Consumer> siphon = 77 | new ResourceTrackingEventQueueConsumer(configMaps) { 78 | @Override 79 | protected final void accept(final AbstractEvent event) { 80 | assertNotNull(event); 81 | System.out.println("*** received event: " + event); 82 | } 83 | }; 84 | 85 | // Begin sucking EventQueue instances out of the cache on a 86 | // separate Thread. Obviously there aren't any yet. This creates 87 | // a new (daemon) Thread and starts it. It will block 88 | // immediately, waiting for new EventQueues to show up in our 89 | // EventQueueCollection. 90 | eventQueues.start(siphon); 91 | 92 | // Connect to Kubernetes using a combination of system properties, 93 | // environment variables and ~/.kube/config settings as detailed 94 | // here: 95 | // https://github.com/fabric8io/kubernetes-client/blob/v3.2.0/README.md#configuring-the-client. 96 | // We'll use this client when we create a Reflector below. 97 | final DefaultKubernetesClient client = new DefaultKubernetesClient(); 98 | 99 | // Now create a Reflector that we'll then hook up to Kubernetes 100 | // and instruct to "reflect" its events "into" our 101 | // EventQueueCollection, thus making EventQueues available to the 102 | // Consumer we built above. 103 | final Reflector reflector = 104 | new Reflector(client.configMaps(), 105 | eventQueues, 106 | Duration.ofSeconds(10)); 107 | 108 | // Start the reflection process: this effectively puts EventQueue 109 | // instances into the cache. This creates a new (daemon) Thread 110 | // and starts it. 111 | System.out.println("*** starting reflector"); 112 | reflector.start(); 113 | 114 | // Sleep for a bit on the main thread so you can see what's going 115 | // on and try adding some resources to Kubernetes in a terminal 116 | // window. Watch as the consumer we built above will report on 117 | // all the additions, updates, deletions and synchronizations. 118 | Thread.sleep(1L * 60L * 1000L); 119 | 120 | // Close the Reflector. This cancels any scheduled 121 | // synchronization tasks. 122 | System.out.println("*** closing reflector"); 123 | reflector.close(); 124 | 125 | // Close the client, now that no one will be calling it anymore. 126 | System.out.println("*** closing client"); 127 | client.close(); 128 | 129 | // Shut down reception of events now that no one is making any 130 | // more of them. 131 | System.out.println("*** closing eventQueues"); 132 | eventQueues.close(); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/test/resources/kubernetes/configMap00.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | apiVersion: v1 3 | metadata: 4 | name: "00" 5 | data: 6 | foo: barbarbar 7 | --------------------------------------------------------------------------------