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.time.temporal.ChronoUnit;
025
026import java.util.ArrayList;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.Objects;
030import java.util.Map;
031
032import java.util.concurrent.Executors;
033import java.util.concurrent.ScheduledExecutorService;
034import java.util.concurrent.ScheduledFuture;
035import java.util.concurrent.TimeUnit;
036
037import java.util.logging.Level;
038import java.util.logging.Logger;
039
040import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only
041import io.fabric8.kubernetes.client.KubernetesClientException;
042import io.fabric8.kubernetes.client.Watcher;
043
044import io.fabric8.kubernetes.client.dsl.Listable;
045import io.fabric8.kubernetes.client.dsl.VersionWatchable;
046
047import io.fabric8.kubernetes.api.model.HasMetadata;
048import io.fabric8.kubernetes.api.model.ObjectMeta;
049import io.fabric8.kubernetes.api.model.KubernetesResourceList;
050import io.fabric8.kubernetes.api.model.ListMeta;
051
052import net.jcip.annotations.GuardedBy;
053import net.jcip.annotations.ThreadSafe;
054
055import org.microbean.development.annotation.NonBlocking;
056
057/**
058 * A pump of sorts that continuously "pulls" logical events out of
059 * Kubernetes and {@linkplain EventCache#add(Object, AbstractEvent.Type,
060 * HasMetadata) adds them} to an {@link EventCache} so as to logically
061 * "reflect" the contents of Kubernetes into the cache.
062 *
063 * <h2>Thread Safety</h2>
064 *
065 * <p>Instances of this class are safe for concurrent use by multiple
066 * {@link Thread}s.</p>
067 *
068 * @param <T> a type of Kubernetes resource
069 *
070 * @author <a href="https://about.me/lairdnelson"
071 * target="_parent">Laird Nelson</a>
072 *
073 * @see EventCache
074 */
075@ThreadSafe
076public class Reflector<T extends HasMetadata> implements Closeable {
077
078
079  /*
080   * Instance fields.
081   */
082
083
084  /**
085   * The operation that was supplied at construction time.
086   *
087   * <p>This field is never {@code null}.</p>
088   *
089   * <p>It is guaranteed that the value of this field may be
090   * assignable to a reference of type {@link Listable Listable&lt;?
091   * extends KubernetesResourceList&gt;} or to a reference of type
092   * {@link VersionWatchable VersionWatchable&lt;? extends Closeable,
093   * Watcher&lt;T&gt;&gt;}.</p>
094   *
095   * @see Listable
096   *
097   * @see VersionWatchable
098   */
099  private final Object operation;
100
101  /**
102   * The resource version 
103   */
104  private volatile Object lastSynchronizationResourceVersion;
105
106  private final ScheduledExecutorService synchronizationExecutorService;
107
108  @GuardedBy("this")
109  private ScheduledFuture<?> synchronizationTask;
110
111  private final boolean shutdownSynchronizationExecutorServiceOnClose;
112
113  private final Duration synchronizationInterval;
114
115  @GuardedBy("this")
116  private Closeable watch;
117
118  @GuardedBy("itself")
119  private final EventCache<T> eventCache;
120
121  /**
122   * A {@link Logger} for use by this {@link Reflector}.
123   *
124   * <p>This field is never {@code null}.</p>
125   *
126   * @see #createLogger()
127   */
128  protected final Logger logger;
129  
130
131  /*
132   * Constructors.
133   */
134
135
136  /**
137   * Creates a new {@link Reflector}.
138   *
139   * @param <X> a type that is both an appropriate kind of {@link
140   * Listable} and {@link VersionWatchable}, such as the kind of
141   * operation returned by {@link
142   * DefaultKubernetesClient#configMaps()} and the like
143   *
144   * @param operation a {@link Listable} and a {@link
145   * VersionWatchable} that can report information from a Kubernetes
146   * cluster; must not be {@code null}
147   *
148   * @param eventCache an {@link EventCache} <strong>that will be
149   * synchronized on</strong> and into which {@link Event}s will be
150   * logically "reflected"; must not be {@code null}
151   *
152   * @exception NullPointerException if {@code operation} or {@code
153   * eventCache} is {@code null}
154   *
155   * @exception IllegalStateException if the {@link #createLogger()}
156   * method returns {@code null}
157   *
158   * @see #Reflector(Listable, EventCache, ScheduledExecutorService,
159   * Duration)
160   *
161   * @see #start()
162   */
163  @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types
164  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation,
165                                                                                                                              final EventCache<T> eventCache) {
166    this(operation, eventCache, null, null);
167  }
168
169  /**
170   * Creates a new {@link Reflector}.
171   *
172   * @param <X> a type that is both an appropriate kind of {@link
173   * Listable} and {@link VersionWatchable}, such as the kind of
174   * operation returned by {@link
175   * DefaultKubernetesClient#configMaps()} and the like
176   *
177   * @param operation a {@link Listable} and a {@link
178   * VersionWatchable} that can report information from a Kubernetes
179   * cluster; must not be {@code null}
180   *
181   * @param eventCache an {@link EventCache} <strong>that will be
182   * synchronized on</strong> and into which {@link Event}s will be
183   * logically "reflected"; must not be {@code null}
184   *
185   * @param synchronizationInterval a {@link Duration} representing
186   * the time in between one {@linkplain EventCache#synchronize()
187   * synchronization operation} and another; may be {@code null} in
188   * which case no synchronization will occur
189   *
190   * @exception NullPointerException if {@code operation} or {@code
191   * eventCache} is {@code null}
192   *
193   * @exception IllegalStateException if the {@link #createLogger()}
194   * method returns {@code null}
195   *
196   * @see #Reflector(Listable, EventCache, ScheduledExecutorService,
197   * Duration)
198   *
199   * @see #start()
200   */
201  @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types
202  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation,
203                                                                                                                              final EventCache<T> eventCache,
204                                                                                                                              final Duration synchronizationInterval) {
205    this(operation, eventCache, null, synchronizationInterval);
206  }
207
208  /**
209   * Creates a new {@link Reflector}.
210   *
211   * @param <X> a type that is both an appropriate kind of {@link
212   * Listable} and {@link VersionWatchable}, such as the kind of
213   * operation returned by {@link
214   * DefaultKubernetesClient#configMaps()} and the like
215   *
216   * @param operation a {@link Listable} and a {@link
217   * VersionWatchable} that can report information from a Kubernetes
218   * cluster; must not be {@code null}
219   *
220   * @param eventCache an {@link EventCache} <strong>that will be
221   * synchronized on</strong> and into which {@link Event}s will be
222   * logically "reflected"; must not be {@code null}
223   *
224   * @param synchronizationExecutorService a {@link
225   * ScheduledExecutorService} to be used to tell the supplied {@link
226   * EventCache} to {@linkplain EventCache#synchronize() synchronize}
227   * on a schedule; may be {@code null} in which case no
228   * synchronization will occur
229   *
230   * @param synchronizationInterval a {@link Duration} representing
231   * the time in between one {@linkplain EventCache#synchronize()
232   * synchronization operation} and another; may be {@code null} in
233   * which case no synchronization will occur
234   *
235   * @exception NullPointerException if {@code operation} or {@code
236   * eventCache} is {@code null}
237   *
238   * @exception IllegalStateException if the {@link #createLogger()}
239   * method returns {@code null}
240   *
241   * @see #start()
242   */
243  @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types
244  public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation,
245                                                                                                                              final EventCache<T> eventCache,
246                                                                                                                              final ScheduledExecutorService synchronizationExecutorService,
247                                                                                                                              final Duration synchronizationInterval) {
248    super();
249    this.logger = this.createLogger();
250    if (this.logger == null) {
251      throw new IllegalStateException("createLogger() == null");
252    }
253    final String cn = this.getClass().getName();
254    final String mn = "<init>";
255    if (this.logger.isLoggable(Level.FINER)) {
256      this.logger.entering(cn, mn, new Object[] { operation, eventCache, synchronizationExecutorService, synchronizationInterval });
257    }
258    Objects.requireNonNull(operation);
259    this.eventCache = Objects.requireNonNull(eventCache);
260    // TODO: research: maybe: operation.withField("metadata.resourceVersion", "0")?    
261    this.operation = operation.withResourceVersion("0");
262    this.synchronizationInterval = synchronizationInterval;
263    if (synchronizationExecutorService == null) {
264      if (synchronizationInterval == null) {
265        this.synchronizationExecutorService = null;
266        this.shutdownSynchronizationExecutorServiceOnClose = false;
267      } else {
268        this.synchronizationExecutorService = Executors.newScheduledThreadPool(1);
269        this.shutdownSynchronizationExecutorServiceOnClose = true;
270      }
271    } else {
272      this.synchronizationExecutorService = synchronizationExecutorService;
273      this.shutdownSynchronizationExecutorServiceOnClose = false;
274    }
275    if (this.logger.isLoggable(Level.FINER)) {
276      this.logger.exiting(cn, mn);
277    }
278  }
279
280
281  /*
282   * Instance methods.
283   */
284  
285
286  /**
287   * Returns a {@link Logger} that will be used for this {@link
288   * Reflector}.
289   *
290   * <p>This method never returns {@code null}.</p>
291   *
292   * <p>Overrides of this method must not return {@code null}.</p>
293   *
294   * @return a non-{@code null} {@link Logger}
295   */
296  protected Logger createLogger() {
297    return Logger.getLogger(this.getClass().getName());
298  }
299  
300  /**
301   * Notionally closes this {@link Reflector} by terminating any
302   * {@link Thread}s that it has started and invoking the {@link
303   * #onClose()} method while holding this {@link Reflector}'s
304   * monitor.
305   *
306   * @exception IOException if an error occurs
307   *
308   * @see #onClose()
309   */
310  @Override
311  public synchronized final void close() throws IOException {
312    final String cn = this.getClass().getName();
313    final String mn = "close";
314    if (this.logger.isLoggable(Level.FINER)) {
315      this.logger.entering(cn, mn);
316    }
317    try {
318      this.closeSynchronizationExecutorService();
319      final ScheduledFuture<?> synchronizationTask = this.synchronizationTask;
320      if (synchronizationTask != null) {
321        synchronizationTask.cancel(false);
322      }
323      this.closeSynchronizationExecutorService();
324      if (this.watch != null) {
325        this.watch.close();
326      }
327    } finally {
328      this.onClose();
329    }
330    if (this.logger.isLoggable(Level.FINER)) {
331      this.logger.exiting(cn, mn);
332    }
333  }
334
335  private synchronized final void closeSynchronizationExecutorService() {
336    final String cn = this.getClass().getName();
337    final String mn = "closeSynchronizationExecutorService";
338    if (this.logger.isLoggable(Level.FINER)) {
339      this.logger.entering(cn, mn);
340    }
341    if (this.synchronizationExecutorService != null && this.shutdownSynchronizationExecutorServiceOnClose) {
342      this.synchronizationExecutorService.shutdown();
343      try {
344        if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) {
345          this.synchronizationExecutorService.shutdownNow();
346          if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) {
347            this.synchronizationExecutorService.shutdownNow();
348          }
349        }
350      } catch (final InterruptedException interruptedException) {
351        this.synchronizationExecutorService.shutdownNow();
352        Thread.currentThread().interrupt();
353      }
354    }
355    if (this.logger.isLoggable(Level.FINER)) {
356      this.logger.exiting(cn, mn);
357    }
358  }
359
360  private synchronized final void setUpSynchronization() {
361    final String cn = this.getClass().getName();
362    final String mn = "setUpSynchronization";
363    if (this.logger.isLoggable(Level.FINER)) {
364      this.logger.entering(cn, mn);
365    }
366    if (this.synchronizationExecutorService != null) {
367      
368      final Duration synchronizationDuration = this.getSynchronizationInterval();
369      final long seconds;
370      if (synchronizationDuration == null) {
371        seconds = 0L;
372      } else {
373        seconds = synchronizationDuration.get(ChronoUnit.SECONDS);
374      }
375        
376      if (seconds > 0L) {
377        if (this.logger.isLoggable(Level.INFO)) {
378          this.logger.logp(Level.INFO, cn, mn, "Scheduling downstream synchronization every {0} seconds", Long.valueOf(seconds));
379        }
380        final ScheduledFuture<?> job = this.synchronizationExecutorService.scheduleWithFixedDelay(() -> {
381            if (shouldSynchronize()) {
382              if (logger.isLoggable(Level.FINE)) {
383                logger.logp(Level.FINE, cn, mn, "Synchronizing event cache with its downstream consumers");
384              }
385              synchronized (eventCache) {
386                eventCache.synchronize();
387              }
388            }
389          }, 0L, seconds, TimeUnit.SECONDS);
390        assert job != null;
391        this.synchronizationTask = job;
392      }
393      
394    }
395    if (this.logger.isLoggable(Level.FINER)) {
396      this.logger.exiting(cn, mn);
397    }
398  }
399
400  /**
401   * Returns whether, at any given moment, this {@link Reflector}
402   * should cause its {@link EventCache} to {@linkplain
403   * EventCache#synchronize() synchronize}.
404   *
405   * <h2>Design Notes</h2>
406   *
407   * <p>This code follows the Go code in the Kubernetes {@code
408   * client-go/tools/cache} package.  One thing that becomes clear
409   * when looking at all of this through an object-oriented lens is
410   * that it is the {@link EventCache} (the {@code delta_fifo}, in the
411   * Go code) that is ultimately in charge of synchronizing.  It is
412   * not clear why this is a function of a reflector.  In an
413   * object-oriented world, perhaps the {@link EventCache} itself
414   * should be in charge of resynchronization schedules.</p>
415   *
416   * @return {@code true} if this {@link Reflector} should cause its
417   * {@link EventCache} to {@linkplain EventCache#synchronize()
418   * synchronize}; {@code false} otherwise
419   */
420  protected boolean shouldSynchronize() {
421    final String cn = this.getClass().getName();
422    final String mn = "shouldSynchronize";
423    if (this.logger.isLoggable(Level.FINER)) {
424      this.logger.entering(cn, mn);
425    }
426    final boolean returnValue = this.synchronizationExecutorService != null;
427    if (this.logger.isLoggable(Level.FINER)) {
428      this.logger.exiting(cn, mn, Boolean.valueOf(returnValue));
429    }
430    return returnValue;
431  }
432  
433  private final Duration getSynchronizationInterval() {
434    return this.synchronizationInterval;
435  }
436
437  private final Object getLastSynchronizationResourceVersion() {
438    return this.lastSynchronizationResourceVersion;
439  }
440  
441  private final void setLastSynchronizationResourceVersion(final Object resourceVersion) {
442    this.lastSynchronizationResourceVersion = resourceVersion;
443  }
444
445  /**
446   * Using the {@code operation} supplied at construction time,
447   * {@linkplain Listable#list() lists} appropriate Kubernetes
448   * resources, and then, on a separate {@link Thread}, {@linkplain
449   * VersionWatchable sets up a watch} on them, calling {@link
450   * EventCache#replace(Collection, Object)} and {@link
451   * EventCache#add(Object, AbstractEvent.Type, HasMetadata)} methods
452   * as appropriate.
453   *
454   * <p>The calling {@link Thread} is not blocked by invocations of
455   * this method.</p>
456   *
457   * @see #close()
458   */
459  @NonBlocking
460  public synchronized final void start() {
461    final String cn = this.getClass().getName();
462    final String mn = "start";
463    if (this.logger.isLoggable(Level.FINER)) {
464      this.logger.entering(cn, mn);
465    }
466    if (this.watch == null) {
467
468      // Run a list operation, and get the resourceVersion of that list.
469      @SuppressWarnings("unchecked")
470      final KubernetesResourceList<? extends T> list = ((Listable<? extends KubernetesResourceList<? extends T>>)this.operation).list();
471      assert list != null;
472      final ListMeta metadata = list.getMetadata();
473      assert metadata != null;      
474      final String resourceVersion = metadata.getResourceVersion();
475      assert resourceVersion != null;
476
477      // Using the results of that list operation, do a full replace
478      // on the EventCache with them.
479      final Collection<? extends T> replacementItems;
480      final Collection<? extends T> items = list.getItems();
481      if (items == null || items.isEmpty()) {
482        replacementItems = Collections.emptySet();
483      } else {
484        replacementItems = Collections.unmodifiableCollection(new ArrayList<>(items));
485      }
486      synchronized (eventCache) {
487        this.eventCache.replace(replacementItems, resourceVersion);
488      }
489
490      // Record the resource version we captured during our list
491      // operation.
492      this.setLastSynchronizationResourceVersion(resourceVersion);
493
494      // Now that we've vetted that our list operation works (i.e. no
495      // syntax errors, no connectivity problems) we can schedule
496      // resynchronizations if necessary.
497      this.setUpSynchronization();
498
499      // Now that we've taken care of our list() operation, set up our
500      // watch() operation.
501      @SuppressWarnings("unchecked")
502      final Closeable temp = ((VersionWatchable<? extends Closeable, Watcher<T>>)operation).withResourceVersion(resourceVersion).watch(new WatchHandler());
503      assert temp != null;
504      this.watch = temp;
505
506    }
507    if (this.logger.isLoggable(Level.FINER)) {
508      this.logger.exiting(cn, mn);
509    }
510  }
511
512  /**
513   * Invoked when {@link #close()} is invoked.
514   *
515   * <p>The default implementation of this method does nothing.</p>
516   */
517  protected synchronized void onClose() {
518
519  }
520  
521
522  /*
523   * Inner and nested classes.
524   */
525  
526
527  /**
528   * A {@link Watcher} of Kubernetes resources.
529   *
530   * @author <a href="https://about.me/lairdnelson"
531   * target="_parent">Laird Nelson</a>
532   *
533   * @see Watcher
534   */
535  private final class WatchHandler implements Watcher<T> {
536
537
538    /*
539     * Constructors.
540     */
541
542    
543    /**
544     * Creates a new {@link WatchHandler}.
545     */
546    private WatchHandler() {
547      super();
548      final String cn = this.getClass().getName();
549      final String mn = "<init>";
550      if (logger.isLoggable(Level.FINER)) {
551        logger.entering(cn, mn);
552        logger.exiting(cn, mn);
553      }
554    }
555
556
557    /*
558     * Instance methods.
559     */
560    
561
562    /**
563     * Calls the {@link EventCache#add(Object, AbstractEvent.Type,
564     * HasMetadata)} method on the enclosing {@link Reflector}'s
565     * associated {@link EventCache} with information harvested from
566     * the supplied {@code resource}, and using an {@link Event.Type}
567     * selected appropriately given the supplied {@link
568     * Watcher.Action}.
569     *
570     * @param action the kind of Kubernetes event that happened; must
571     * not be {@code null}
572     *
573     * @param resource the {@link HasMetadata} object that was
574     * affected; must not be {@code null}
575     *
576     * @exception NullPointerException if {@code action} or {@code
577     * resource} was {@code null}
578     *
579     * @exception IllegalStateException if another error occurred
580     */
581    @Override
582    public final void eventReceived(final Watcher.Action action, final T resource) {
583      final String cn = this.getClass().getName();
584      final String mn = "eventReceived";
585      if (logger.isLoggable(Level.FINER)) {
586        logger.entering(cn, mn, new Object[] { action, resource });
587      }
588      Objects.requireNonNull(action);
589      Objects.requireNonNull(resource);
590
591      final ObjectMeta metadata = resource.getMetadata();
592      assert metadata != null;
593
594      final Event.Type eventType;
595
596      switch (action) {
597      case ADDED:
598        eventType = Event.Type.ADDITION;
599        break;
600      case MODIFIED:
601        eventType = Event.Type.MODIFICATION;
602        break;
603      case DELETED:
604        eventType = Event.Type.DELETION;
605        break;
606      case ERROR:        
607        // Uh...the Go code has:
608        //
609        //   if event.Type == watch.Error {
610        //     return apierrs.FromObject(event.Object)
611        //   }
612        //
613        // Now, apierrs.FromObject is here:
614        // https://github.com/kubernetes/apimachinery/blob/kubernetes-1.9.2/pkg/api/errors/errors.go#L80-L88
615        // This is looking for a Status object.  But
616        // WatchConnectionHandler will never forward on such a thing:
617        // 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
618        //
619        // So it follows that if by some chance we get here, resource
620        // will definitely be a HasMetadata.  We go back to the Go
621        // code again, and remember that if the type is Error, the
622        // equivalent of this watch handler simply returns and goes home.
623        //
624        // Now, if we were to throw a RuntimeException here, which is
625        // the idiomatic equivalent of returning and going home, this
626        // would cause a watch reconnect:
627        // 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
628        // ...up to the reconnect limit.
629        //
630        // ...which is fine, but I'm not sure that in an error case a
631        // WatchEvent will ever HAVE a HasMetadata as its payload.
632        // Which means MAYBE we'll never get here.  But if we do, all
633        // we can do is throw a RuntimeException...which ends up
634        // reducing to the same case as the default case below, so we
635        // fall through.
636      default:
637        eventType = null;
638        throw new IllegalStateException();
639      }
640
641      // Add an Event of the proper kind to our EventCache.
642      if (eventType != null) {
643        if (logger.isLoggable(Level.FINE)) {
644          logger.logp(Level.FINE, cn, mn, "Adding event to cache: {0} {1}", new Object[] { eventType, resource });
645        }
646        synchronized (eventCache) {
647          eventCache.add(Reflector.this, eventType, resource);
648        }
649      }
650
651      // Record the most recent resource version we're tracking to be
652      // that of this last successful watch() operation.  We set it
653      // earlier during a list() operation.
654      setLastSynchronizationResourceVersion(metadata.getResourceVersion());
655
656      if (logger.isLoggable(Level.FINER)) {
657        logger.exiting(cn, mn);
658      }
659    }
660
661    /**
662     * Invoked when the Kubernetes client connection closes.
663     *
664     * @param exception any {@link KubernetesClientException} that
665     * caused this closing to happen; may be {@code null}
666     */
667    @Override
668    public final void onClose(final KubernetesClientException exception) {
669      final String cn = this.getClass().getName();
670      final String mn = "onClose";
671      if (logger.isLoggable(Level.FINER)) {
672        logger.entering(cn, mn, exception);
673      }
674      if (exception != null && logger.isLoggable(Level.WARNING)) {
675        logger.logp(Level.WARNING, cn, mn, exception.getMessage(), exception);
676      }
677      if (logger.isLoggable(Level.FINER)) {
678        logger.exiting(cn, mn, exception);
679      }      
680    }
681    
682  }
683  
684}