001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017-2018 microBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.kubernetes.controller;
018
019import java.beans.PropertyChangeEvent;
020import java.beans.PropertyChangeListener;
021
022import java.io.Closeable;
023import java.io.IOException;
024
025import java.time.Duration;
026
027import java.util.Map;
028import java.util.Objects;
029
030import java.util.concurrent.ScheduledExecutorService;
031
032import java.util.function.Consumer;
033
034import java.util.logging.Level;
035import java.util.logging.Logger;
036
037import io.fabric8.kubernetes.api.model.HasMetadata;
038import io.fabric8.kubernetes.api.model.KubernetesResourceList;
039
040import io.fabric8.kubernetes.client.Watcher;
041
042import io.fabric8.kubernetes.client.dsl.Listable;
043import io.fabric8.kubernetes.client.dsl.VersionWatchable;
044
045import net.jcip.annotations.Immutable;
046import net.jcip.annotations.ThreadSafe;
047
048import org.microbean.development.annotation.NonBlocking;
049
050/**
051 * A convenient combination of a {@link Reflector}, a {@link
052 * VersionWatchable} and {@link Listable} implementation, an
053 * (internal) {@link EventQueueCollection}, a {@link Map} of known
054 * Kubernetes resources and an {@link EventQueue} {@link Consumer}
055 * that {@linkplain Reflector#start() mirrors Kubernetes cluster
056 * events} into a {@linkplain EventQueueCollection collection of
057 * <code>EventQueue</code>s} and {@linkplain
058 * EventQueueCollection#start(Consumer) arranges for their consumption
059 * and processing}.
060 *
061 * <p>{@linkplain #start() Starting} a {@link Controller} {@linkplain
062 * EventQueueCollection#start(Consumer) starts the
063 * <code>Consumer</code>} supplied at construction time, and
064 * {@linkplain Reflector#start() starts the embedded
065 * <code>Reflector</code>}.  {@linkplain #close() Closing} a {@link
066 * Controller} {@linkplain Reflector#close() closes its embedded
067 * <code>Reflector</code>} and {@linkplain
068 * EventQueueCollection#close() causes the <code>Consumer</code>
069 * supplied at construction time to stop receiving
070 * <code>Event</code>s}.</p>
071 *
072 * <p>Several {@code protected} methods in this class exist to make
073 * customization easier; none require overriding and their default
074 * behavior is usually just fine.</p>
075 *
076 * <h2>Thread Safety</h2>
077 *
078 * <p>Instances of this class are safe for concurrent use by multiple
079 * threads.</p>
080 *
081 * @param <T> a Kubernetes resource type
082 *
083 * @author <a href="https://about.me/lairdnelson"
084 * target="_parent">Laird Nelson</a>
085 *
086 * @see Reflector
087 *
088 * @see EventQueueCollection
089 *
090 * @see ResourceTrackingEventQueueConsumer
091 *
092 * @see #start()
093 *
094 * @see #close()
095 */
096@Immutable
097@ThreadSafe
098public class Controller<T extends HasMetadata> implements Closeable {
099
100
101  /*
102   * Instance fields.
103   */
104
105
106  /**
107   * A {@link Logger} used by this {@link Controller}.
108   *
109   * <p>This field is never {@code null}.</p>
110   *
111   * @see #createLogger()
112   */
113  protected final Logger logger;
114
115  /**
116   * The {@link Reflector} used by this {@link Controller} to mirror
117   * Kubernetes events.
118   *
119   * <p>This field is never {@code null}.</p>
120   */
121  private final Reflector<T> reflector;
122
123  /**
124   * The {@link EventQueueCollection} used by the {@link #reflector
125   * Reflector} and by the {@link Consumer} supplied at construction
126   * time.
127   *
128   * <p>This field is never {@code null}.</p>
129   *
130   * @see EventQueueCollection#add(Object, AbstractEvent.Type,
131   * HasMetadata)
132   *
133   * @see EventQueueCollection#replace(Collection, Object)
134   *
135   * @see EventQueueCollection#synchronize()
136   *
137   * @see EventQueueCollection#start(Consumer)
138   */
139  private final EventQueueCollection<T> eventCache;
140
141  /**
142   * A {@link Consumer} of {@link EventQueue}s that processes {@link
143   * Event}s produced, ultimately, by the {@link #reflector
144   * Reflector}.
145   *
146   * <p>This field is never {@code null}.</p>
147   */
148  private final Consumer<? super EventQueue<? extends T>> siphon;
149
150  
151  /*
152   * Constructors.
153   */
154
155
156  /**
157   * Creates a new {@link Controller} but does not {@linkplain
158   * #start() start it}.
159   *
160   * @param <X> a {@link Listable} and {@link VersionWatchable} that
161   * will be used by the embedded {@link Reflector}; must not be
162   * {@code null}
163   *
164   * @param operation a {@link Listable} and a {@link
165   * VersionWatchable} that produces Kubernetes events; must not be
166   * {@code null}
167   *
168   * @param siphon the {@link Consumer} that will process each {@link
169   * EventQueue} as it becomes ready; must not be {@code null}
170   *
171   * @exception NullPointerException if {@code operation} or {@code
172   * siphon} is {@code null}
173   *
174   * @see #Controller(Listable, ScheduledExecutorService, Duration,
175   * Map, Consumer)
176   *
177   * @see #start()
178   */
179  @SuppressWarnings("rawtypes")
180  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation,
181                                                                                                                               final Consumer<? super EventQueue<? extends T>> siphon) {
182    this(operation, null, null, null, siphon);
183  }
184
185  /**
186   * Creates a new {@link Controller} but does not {@linkplain
187   * #start() start it}.
188   *
189   * @param <X> a {@link Listable} and {@link VersionWatchable} that
190   * will be used by the embedded {@link Reflector}; must not be
191   * {@code null}
192   *
193   * @param operation a {@link Listable} and a {@link
194   * VersionWatchable} that produces Kubernetes events; must not be
195   * {@code null}
196   *
197   * @param knownObjects a {@link Map} containing the last known state
198   * of Kubernetes resources the embedded {@link EventQueueCollection}
199   * is caching events for; may be {@code null} if this {@link
200   * Controller} is not interested in tracking deletions of objects;
201   * if non-{@code null} <strong>will be synchronized on by this
202   * class</strong> during retrieval and traversal operations
203   *
204   * @param siphon the {@link Consumer} that will process each {@link
205   * EventQueue} as it becomes ready; must not be {@code null}
206   *
207   * @exception NullPointerException if {@code operation} or {@code
208   * siphon} is {@code null}
209   *
210   * @see #Controller(Listable, ScheduledExecutorService, Duration,
211   * Map, Consumer)
212   *
213   * @see #start()
214   */
215  @SuppressWarnings("rawtypes")
216  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation,
217                                                                                                                               final Map<Object, T> knownObjects,
218                                                                                                                               final Consumer<? super EventQueue<? extends T>> siphon) {
219    this(operation, null, null, knownObjects, siphon);
220  }
221
222  /**
223   * Creates a new {@link Controller} but does not {@linkplain
224   * #start() start it}.
225   *
226   * @param <X> a {@link Listable} and {@link VersionWatchable} that
227   * will be used by the embedded {@link Reflector}; must not be
228   * {@code null}
229   *
230   * @param operation a {@link Listable} and a {@link
231   * VersionWatchable} that produces Kubernetes events; must not be
232   * {@code null}
233   *
234   * @param synchronizationInterval a {@link Duration} representing
235   * the time in between one {@linkplain EventCache#synchronize()
236   * synchronization operation} and another; may be {@code null} in
237   * which case no synchronization will occur
238   *
239   * @param siphon the {@link Consumer} that will process each {@link
240   * EventQueue} as it becomes ready; must not be {@code null}
241   *
242   * @exception NullPointerException if {@code operation} or {@code
243   * siphon} is {@code null}
244   *
245   * @see #Controller(Listable, ScheduledExecutorService, Duration,
246   * Map, Consumer)
247   *
248   * @see #start()
249   */
250  @SuppressWarnings("rawtypes")
251  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation,
252                                                                                                                               final Duration synchronizationInterval,
253                                                                                                                               final Consumer<? super EventQueue<? extends T>> siphon) {
254    this(operation, null, synchronizationInterval, null, siphon);
255  }
256
257  /**
258   * Creates a new {@link Controller} but does not {@linkplain
259   * #start() start it}.
260   *
261   * @param <X> a {@link Listable} and {@link VersionWatchable} that
262   * will be used by the embedded {@link Reflector}; must not be
263   * {@code null}
264   *
265   * @param operation a {@link Listable} and a {@link
266   * VersionWatchable} that produces Kubernetes events; must not be
267   * {@code null}
268   *
269   * @param synchronizationInterval a {@link Duration} representing
270   * the time in between one {@linkplain EventCache#synchronize()
271   * synchronization operation} and another; may be {@code null} in
272   * which case no synchronization will occur
273   *
274   * @param knownObjects a {@link Map} containing the last known state
275   * of Kubernetes resources the embedded {@link EventQueueCollection}
276   * is caching events for; may be {@code null} if this {@link
277   * Controller} is not interested in tracking deletions of objects;
278   * if non-{@code null} <strong>will be synchronized on by this
279   * class</strong> during retrieval and traversal operations
280   *
281   * @param siphon the {@link Consumer} that will process each {@link
282   * EventQueue} as it becomes ready; must not be {@code null}
283   *
284   * @exception NullPointerException if {@code operation} or {@code
285   * siphon} is {@code null}
286   *
287   * @see #Controller(Listable, ScheduledExecutorService, Duration,
288   * Map, Consumer)
289   *
290   * @see #start()
291   */
292  @SuppressWarnings("rawtypes")
293  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation,
294                                                                                                                               final Duration synchronizationInterval,
295                                                                                                                               final Map<Object, T> knownObjects,
296                                                                                                                               final Consumer<? super EventQueue<? extends T>> siphon) {
297    this(operation, null, synchronizationInterval, knownObjects, siphon);
298  }
299
300  /**
301   * Creates a new {@link Controller} but does not {@linkplain
302   * #start() start it}.
303   *
304   * @param <X> a {@link Listable} and {@link VersionWatchable} that
305   * will be used by the embedded {@link Reflector}; must not be
306   * {@code null}
307   *
308   * @param operation a {@link Listable} and a {@link
309   * VersionWatchable} that produces Kubernetes events; must not be
310   * {@code null}
311   *
312   * @param synchronizationExecutorService the {@link
313   * ScheduledExecutorService} that will be passed to the {@link
314   * Reflector} constructor; may be {@code null} in which case a
315   * default {@link ScheduledExecutorService} may be used instead
316   *
317   * @param synchronizationInterval a {@link Duration} representing
318   * the time in between one {@linkplain EventCache#synchronize()
319   * synchronization operation} and another; may be {@code null} in
320   * which case no synchronization will occur
321   *
322   * @param knownObjects a {@link Map} containing the last known state
323   * of Kubernetes resources the embedded {@link EventQueueCollection}
324   * is caching events for; may be {@code null} if this {@link
325   * Controller} is not interested in tracking deletions of objects;
326   * if non-{@code null} <strong>will be synchronized on by this
327   * class</strong> during retrieval and traversal operations
328   *
329   * @param siphon the {@link Consumer} that will process each {@link
330   * EventQueue} as it becomes ready; must not be {@code null}
331   *
332   * @exception NullPointerException if {@code operation} or {@code
333   * siphon} is {@code null}
334   *
335   * @see #start()
336   */
337  @SuppressWarnings("rawtypes")
338  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation,
339                                                                                                                               final ScheduledExecutorService synchronizationExecutorService,
340                                                                                                                               final Duration synchronizationInterval,
341                                                                                                                               final Map<Object, T> knownObjects,
342                                                                                                                               final Consumer<? super EventQueue<? extends T>> siphon) {
343    super();
344    this.logger = this.createLogger();
345    if (this.logger == null) {
346      throw new IllegalStateException("createLogger() == null");
347    }
348    final String cn = this.getClass().getName();
349    final String mn = "<init>";
350    if (this.logger.isLoggable(Level.FINER)) {
351      this.logger.entering(cn, mn, new Object[] { operation, synchronizationExecutorService, synchronizationInterval, knownObjects, siphon });
352    }
353    this.siphon = Objects.requireNonNull(siphon);
354    this.eventCache = new ControllerEventQueueCollection(knownObjects);    
355    this.reflector = new ControllerReflector(operation, synchronizationExecutorService, synchronizationInterval);
356    if (this.logger.isLoggable(Level.FINER)) {
357      this.logger.exiting(cn, mn);
358    }
359  }
360
361
362  /*
363   * Instance methods.
364   */
365
366
367  /**
368   * Returns a {@link Logger} for use by this {@link Controller}.
369   *
370   * <p>This method never returns {@code null}.</p>
371   *
372   * <p>Overrides of this method must not return {@code null}.</p>
373   *
374   * @return a non-{@code null} {@link Logger}
375   */
376  protected Logger createLogger() {
377    return Logger.getLogger(this.getClass().getName());
378  }
379
380  /**
381   * {@linkplain EventQueueCollection#start(Consumer) Starts the
382   * embedded <code>EventQueueCollection</code> consumption machinery}
383   * and then {@linkplain Reflector#start() starts the embedded
384   * <code>Reflector</code>}.
385   *
386   * @see EventQueueCollection#start(Consumer)
387   *
388   * @see Reflector#start()
389   */
390  @NonBlocking
391  public final void start() {
392    final String cn = this.getClass().getName();
393    final String mn = "start";
394    if (this.logger.isLoggable(Level.FINER)) {
395      this.logger.entering(cn, mn);
396    }
397    if (this.logger.isLoggable(Level.INFO)) {
398      this.logger.logp(Level.INFO, cn, mn, "Starting {0}", this.siphon);
399    }
400    this.eventCache.start(this.siphon);
401    if (this.logger.isLoggable(Level.INFO)) {
402      this.logger.logp(Level.INFO, cn, mn, "Starting {0}", this.reflector);
403    }
404    this.reflector.start();
405    if (this.logger.isLoggable(Level.FINER)) {
406      this.logger.exiting(cn, mn);
407    }
408  }
409
410  /**
411   * {@linkplain Reflector#close() Closes the embedded
412   * <code>Reflector</code>} and then {@linkplain
413   * EventQueueCollection#close() closes the embedded
414   * <code>EventQueueCollection</code>}, handling exceptions
415   * appropriately.
416   *
417   * @exception IOException if the {@link Reflector} could not
418   * {@linkplain Reflector#close() close} properly
419   *
420   * @see Reflector#close()
421   *
422   * @see EventQueueCollection#close()
423   */
424  @Override
425  public final void close() throws IOException {
426    final String cn = this.getClass().getName();
427    final String mn = "close";
428    if (this.logger.isLoggable(Level.FINER)) {
429      this.logger.entering(cn, mn);
430    }
431    Exception throwMe = null;    
432    try {
433      if (this.logger.isLoggable(Level.INFO)) {
434        this.logger.logp(Level.INFO, cn, mn, "Closing {0}", this.reflector);
435      }
436      this.reflector.close();
437    } catch (final Exception everything) {
438      throwMe = everything;
439    }
440
441    try {
442      if (this.logger.isLoggable(Level.INFO)) {
443        this.logger.logp(Level.INFO, cn, mn, "Closing {0}", this.eventCache);
444      }
445      this.eventCache.close();
446    } catch (final RuntimeException runtimeException) {
447      if (throwMe == null) {
448        throw runtimeException;
449      }
450      throwMe.addSuppressed(runtimeException);
451    }
452
453    if (throwMe instanceof IOException) {
454      throw (IOException)throwMe;
455    } else if (throwMe instanceof RuntimeException) {
456      throw (RuntimeException)throwMe;
457    } else if (throwMe != null) {
458      throw new IllegalStateException(throwMe.getMessage(), throwMe);
459    }
460
461    if (this.logger.isLoggable(Level.FINER)) {
462      this.logger.exiting(cn, mn);
463    }
464  }
465
466  /**
467   * Returns if the embedded {@link Reflector} should {@linkplain
468   * Reflector#shouldSynchronize() synchronize}.
469   *
470   * <p>This implementation returns {@code true}.</p>
471   *
472   * @return {@code true} if the embedded {@link Reflector} should
473   * {@linkplain Reflector#shouldSynchronize() synchronize}; {@code
474   * false} otherwise
475   */
476  protected boolean shouldSynchronize() {
477    final String cn = this.getClass().getName();
478    final String mn = "shouldSynchronize";
479    if (this.logger.isLoggable(Level.FINER)) {
480      this.logger.entering(cn, mn);
481    }
482    final boolean returnValue = true;
483    if (this.logger.isLoggable(Level.FINER)) {
484      this.logger.exiting(cn, mn, Boolean.valueOf(returnValue));
485    }
486    return returnValue;
487  }
488
489  /**
490   * Invoked after the embedded {@link Reflector} {@linkplain
491   * Reflector#onClose() closes}.
492   *
493   * <p>This implementation does nothing.</p>
494   *
495   * @see Reflector#close()
496   *
497   * @see Reflector#onClose()
498   */
499  protected void onClose() {
500
501  }
502
503  /**
504   * Returns a key that can be used to identify the supplied {@link
505   * HasMetadata}.
506   *
507   * <p>This method never returns {@code null}.</p>
508   *
509   * <p>Overrides of this method must not return {@code null}.</p>
510   *
511   * <p>The default implementation of this method returns the return
512   * value of invoking the {@link HasMetadatas#getKey(HasMetadata)}
513   * method.</p>
514   *
515   * @param resource the Kubernetes resource for which a key is
516   * desired; must not be {@code null}
517   *
518   * @return a non-{@code null} key for the supplied {@link
519   * HasMetadata}
520   *
521   * @exception NullPointerException if {@code resource} is {@code
522   * null}
523   */
524  protected Object getKey(final T resource) {
525    final String cn = this.getClass().getName();
526    final String mn = "getKey";
527    if (this.logger.isLoggable(Level.FINER)) {
528      this.logger.entering(cn, mn, resource);
529    }
530    final Object returnValue = HasMetadatas.getKey(Objects.requireNonNull(resource));
531    if (this.logger.isLoggable(Level.FINER)) {
532      this.logger.exiting(cn, mn, returnValue);
533    }
534    return returnValue;
535  }
536
537  /**
538   * Creates a new {@link Event} when invoked.
539   *
540   * <p>This method never returns {@code null}.</p>
541   *
542   * <p>Overrides of this method must not return {@code null}.</p>
543   *
544   * <p>Overrides of this method must return a new {@link Event} or
545   * subclass with each invocation.</p>
546   *
547   * @param source the source of the new {@link Event}; must not be
548   * {@code null}
549   *
550   * @param eventType the {@link Event.Type} for the new {@link
551   * Event}; must not be {@code null}
552   *
553   * @param resource the {@link HasMetadata} that the new {@link
554   * Event} concerns; must not be {@code null}
555   *
556   * @return a new, non-{@code null} {@link Event}
557   *
558   * @exception NullPointerException if any of the parameters is
559   * {@code null}
560   */
561  protected Event<T> createEvent(final Object source, final Event.Type eventType, final T resource) {
562    final String cn = this.getClass().getName();
563    final String mn = "createEvent";
564    if (this.logger.isLoggable(Level.FINER)) {
565      this.logger.entering(cn, mn, new Object[] { source, eventType, resource });
566    }
567    final Event<T> returnValue = new Event<>(Objects.requireNonNull(source), Objects.requireNonNull(eventType), null, Objects.requireNonNull(resource));
568    if (this.logger.isLoggable(Level.FINER)) {
569      this.logger.exiting(cn, mn, returnValue);
570    }
571    return returnValue;
572  }
573
574  /**
575   * Creates a new {@link EventQueue} when invoked.
576   *
577   * <p>This method never returns {@code null}.</p>
578   *
579   * <p>Overrides of this method must not return {@code null}.</p>
580   *
581   * <p>Overrides of this method must return a new {@link EventQueue}
582   * or subclass with each invocation.</p>
583   *
584   * @param key the key to create the new {@link EventQueue} with;
585   * must not be {@code null}
586   *
587   * @return a new, non-{@code null} {@link EventQueue}
588   *
589   * @exception NullPointerException if {@code key} is {@code null}
590   */
591  protected EventQueue<T> createEventQueue(final Object key) {
592    final String cn = this.getClass().getName();
593    final String mn = "createEventQueue";
594    if (this.logger.isLoggable(Level.FINER)) {
595      this.logger.entering(cn, mn, key);
596    }
597    final EventQueue<T> returnValue = new EventQueue<>(key);
598    if (this.logger.isLoggable(Level.FINER)) {
599      this.logger.exiting(cn, mn, returnValue);
600    }
601    return returnValue;
602  }
603
604
605  /*
606   * Inner and nested classes.
607   */
608
609
610  /**
611   * An {@link EventQueueCollection} that delegates its overridable
612   * methods to their equivalents in the {@link Controller} class.
613   *
614   * @author <a href="https://about.me/lairdnelson"
615   * target="_parent">Laird Nelson</a>
616   *
617   * @see EventQueueCollection
618   *
619   * @see EventCache
620   */
621  private final class ControllerEventQueueCollection extends EventQueueCollection<T> {
622
623
624    /*
625     * Constructors.
626     */
627
628    
629    private ControllerEventQueueCollection(final Map<?, ? extends T> knownObjects) {
630      super(knownObjects);
631    }
632
633
634    /*
635     * Instance methods.
636     */
637
638    
639    @Override
640    protected final Event<T> createEvent(final Object source, final Event.Type eventType, final T resource) {
641      return Controller.this.createEvent(source, eventType, resource);
642    }
643    
644    @Override
645    protected final EventQueue<T> createEventQueue(final Object key) {
646      return Controller.this.createEventQueue(key);
647    }
648    
649    @Override
650    protected final Object getKey(final T resource) {
651      return Controller.this.getKey(resource);
652    }
653    
654  }
655
656  
657  /**
658   * A {@link Reflector} that delegates its overridable
659   * methods to their equivalents in the {@link Controller} class.
660   *
661   * @author <a href="https://about.me/lairdnelson"
662   * target="_parent">Laird Nelson</a>
663   *
664   * @see Reflector
665   */
666  private final class ControllerReflector extends Reflector<T> {
667
668
669    /*
670     * Constructors.
671     */
672
673    
674    @SuppressWarnings("rawtypes")
675    private <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> ControllerReflector(final X operation,
676                                                                                                                                           final ScheduledExecutorService synchronizationExecutorService,
677                                                                                                                                           final Duration synchronizationInterval) {
678      super(operation, Controller.this.eventCache, synchronizationExecutorService, synchronizationInterval);
679    }
680
681
682    /*
683     * Instance methods.
684     */
685    
686    
687    @Override
688    protected final boolean shouldSynchronize() {
689      return Controller.this.shouldSynchronize();
690    }
691
692    @Override
693    protected final void onClose() {
694      Controller.this.onClose();
695    }
696  }
697  
698}