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 | * EventQueue
s} 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 | * Event
s}.
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 super EventQueue extends T>> 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 extends Closeable,
199 | Watcher>> Controller(final X operation,
200 | final Consumer super EventQueue extends T>> 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 extends Closeable,
238 | Watcher>> Controller(final X operation,
239 | final Map knownObjects,
240 | final Consumer super EventQueue extends T>> 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 extends Closeable,
276 | Watcher>> Controller(final X operation,
277 | final Duration synchronizationInterval,
278 | final Consumer super EventQueue extends T>> 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 extends Closeable,
321 | Watcher>> Controller(final X operation,
322 | final Duration synchronizationInterval,
323 | final Map knownObjects,
324 | final Consumer super EventQueue extends T>> 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 extends Closeable,
369 | Watcher>> Controller(final X operation,
370 | final ScheduledExecutorService synchronizationExecutorService,
371 | final Duration synchronizationInterval,
372 | final Map knownObjects,
373 | final Consumer super EventQueue extends T>> 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 extends Closeable,
425 | Watcher>> Controller(final X operation,
426 | final ScheduledExecutorService synchronizationExecutorService,
427 | final Duration synchronizationInterval,
428 | final Function super Throwable, Boolean> errorHandler,
429 | final Map knownObjects,
430 | final Consumer super EventQueue extends T>> 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, ? extends T> knownObjects,
796 | final Function super Throwable, Boolean> 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 extends Closeable, Watcher>> ControllerReflector(final X operation,
845 | final ScheduledExecutorService synchronizationExecutorService,
846 | final Duration synchronizationInterval, final Function super Throwable, Boolean> 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 extends T> 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 super AbstractEvent extends T>> 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 super AbstractEvent extends T>> consumer, final Function super Throwable, Boolean> 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 super AbstractEvent extends T>> consumer) {
220 | if (consumer != null) {
221 | this.writeLock.lock();
222 | try {
223 | final Iterator extends Pump>> 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 extends T> event) {
309 | if (event != null) {
310 | if (event instanceof SynchronizationEvent) {
311 | this.accept((SynchronizationEvent extends T>)event);
312 | } else if (event instanceof Event) {
313 | this.accept((Event extends T>)event);
314 | } else {
315 | assert false : "Unexpected event type: " + event.getClass();
316 | }
317 | }
318 | }
319 |
320 | private final void accept(final SynchronizationEvent extends T> 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 extends T> 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 super AbstractEvent extends T>> eventConsumer;
370 |
371 | private final Function super Throwable, Boolean> 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 super AbstractEvent extends T>> eventConsumer) {
390 | this(synchronizationInterval, eventConsumer, null);
391 | }
392 |
393 | private Pump(final Duration synchronizationInterval, final Consumer super AbstractEvent extends T>> eventConsumer, final Function super Throwable, Boolean> 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 super AbstractEvent extends T>> 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 extends T> 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 Thread
s} 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 super AbstractEvent> 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 super Throwable, Boolean> 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 extends Closeable, Watcher>> 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 extends Closeable, Watcher>> 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 extends Closeable, Watcher>> 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 extends Closeable, Watcher>> Reflector(final X operation,
396 | final EventCache eventCache,
397 | final ScheduledExecutorService synchronizationExecutorService,
398 | final Duration synchronizationInterval,
399 | final Function super Throwable, Boolean> 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 extends T> list = ((Listable extends KubernetesResourceList extends T>>)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 extends T> replacementItems;
844 | final Collection extends T> 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 extends Watchable extends Closeable, Watcher>> versionableOperation =
908 | (Versionable extends Watchable extends Closeable, Watcher>>)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 extends T> 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 extends T> 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 extends T> 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 extends T> 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 super EventQueue extends ConfigMap>> siphon =
77 | new ResourceTrackingEventQueueConsumer(configMaps) {
78 | @Override
79 | protected final void accept(final AbstractEvent extends ConfigMap> 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 |
--------------------------------------------------------------------------------