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