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.IOException;
020
021import java.time.Duration;
022import java.time.Instant;
023
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Iterator;
027import java.util.Map;
028import java.util.Objects;
029
030import java.util.concurrent.BlockingQueue;
031import java.util.concurrent.Executor;
032import java.util.concurrent.Executors;
033import java.util.concurrent.ExecutorService;
034import java.util.concurrent.Future;
035import java.util.concurrent.CopyOnWriteArrayList;
036import java.util.concurrent.LinkedBlockingQueue;
037import java.util.concurrent.ScheduledExecutorService;
038import java.util.concurrent.ScheduledThreadPoolExecutor;
039import java.util.concurrent.TimeUnit;
040
041import java.util.concurrent.locks.Lock;
042import java.util.concurrent.locks.ReadWriteLock;
043import java.util.concurrent.locks.ReentrantReadWriteLock;
044
045import java.util.function.Consumer;
046
047import io.fabric8.kubernetes.api.model.HasMetadata;
048
049import net.jcip.annotations.Immutable;
050import net.jcip.annotations.GuardedBy;
051import net.jcip.annotations.ThreadSafe;
052
053/**
054 * A {@link ResourceTrackingEventQueueConsumer} that {@linkplain
055 * ResourceTrackingEventQueueConsumer#accept(EventQueue) consumes
056 * <tt>EventQueue</tt> instances} by feeding each {@link
057 * AbstractEvent} in the {@link EventQueue} being consumed to {@link
058 * Consumer}s of {@link AbstractEvent}s that have been {@linkplain
059 * #addConsumer(Consumer) registered}.
060 *
061 * <p>{@link EventDistributor} instances must be {@linkplain #close()
062 * closed} and discarded after use.</p>
063 *
064 * @param <T> a type of Kubernetes resource
065 *
066 * @author <a href="https://about.me/lairdnelson"
067 * target="_parent">Laird Nelson</a>
068 *
069 * @see #addConsumer(Consumer)
070 *
071 * @see #removeConsumer(Consumer)
072 */
073@Immutable
074@ThreadSafe
075public final class EventDistributor<T extends HasMetadata> extends ResourceTrackingEventQueueConsumer<T> implements AutoCloseable {
076
077
078  /*
079   * Instance fields.
080   */
081  
082
083  @GuardedBy("readLock && writeLock")
084  private final Collection<Pump<T>> distributors;
085
086  @GuardedBy("readLock && writeLock")
087  private final Collection<Pump<T>> synchronizingDistributors;
088
089  private final Duration synchronizationInterval;
090
091  private final Lock readLock;
092
093  private final Lock writeLock;
094
095
096  /*
097   * Constructors.
098   */
099
100
101  /**
102   * Creates a new {@link EventDistributor}.
103   *
104   * @param knownObjects a mutable {@link Map} of Kubernetes resources
105   * that contains or will contain Kubernetes resources known to this
106   * {@link EventDistributor} and whatever mechanism (such as a {@link
107   * Controller}) is feeding it; may be {@code null}
108   *
109   * @see #EventDistributor(Map, Duration)
110   */
111  public EventDistributor(final Map<Object, T> knownObjects) {
112    this(knownObjects, null);
113  }
114
115  /**
116   * Creates a new {@link EventDistributor}.
117   *
118   * @param knownObjects a mutable {@link Map} of Kubernetes resources
119   * that contains or will contain Kubernetes resources known to this
120   * {@link EventDistributor} and whatever mechanism (such as a {@link
121   * Controller}) is feeding it; may be {@code null}
122   *
123   * @param synchronizationInterval a {@link Duration} representing
124   * the interval after which an attempt to synchronize might happen;
125   * may be {@code null} in which case no synchronization will occur
126   *
127   * @see
128   * ResourceTrackingEventQueueConsumer#ResourceTrackingEventQueueConsumer(Map)
129   */
130  public EventDistributor(final Map<Object, T> knownObjects, final Duration synchronizationInterval) {
131    super(knownObjects);
132    final ReadWriteLock lock = new ReentrantReadWriteLock();
133    this.readLock = lock.readLock();
134    this.writeLock = lock.writeLock();
135    this.distributors = new ArrayList<>();    
136    this.synchronizingDistributors = new ArrayList<>();
137    this.synchronizationInterval = synchronizationInterval;
138  }
139
140
141  /*
142   * Instance methods.
143   */
144  
145
146  /**
147   * Adds the supplied {@link Consumer} to this {@link
148   * EventDistributor} as a listener that will be notified of each
149   * {@link AbstractEvent} this {@link EventDistributor} receives.
150   *
151   * <p>The supplied {@link Consumer}'s {@link
152   * Consumer#accept(Object)} method may be called later on a separate
153   * thread of execution.</p>
154   *
155   * @param consumer a {@link Consumer} of {@link AbstractEvent}s; may
156   * be {@code null} in which case no action will be taken
157   *
158   * @see #removeConsumer(Consumer)
159   */
160  public final void addConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) {
161    if (consumer != null) {
162      this.writeLock.lock();
163      try {
164        final Pump<T> distributor = new Pump<>(this.synchronizationInterval, consumer);
165        this.distributors.add(distributor);
166        this.synchronizingDistributors.add(distributor);
167      } finally {
168        this.writeLock.unlock();
169      }
170    }
171  }
172
173  /**
174   * Removes any {@link Consumer} {@linkplain Object#equals(Object)
175   * equal to} a {@link Consumer} previously {@linkplain
176   * #addConsumer(Consumer) added} to this {@link EventDistributor}.
177   *
178   * @param consumer the {@link Consumer} to remove; may be {@code
179   * null} in which case no action will be taken
180   *
181   * @see #addConsumer(Consumer)
182   */
183  public final void removeConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) {
184    if (consumer != null) {
185      this.writeLock.lock();
186      try {
187        final Iterator<? extends Pump<?>> iterator = this.distributors.iterator();
188        assert iterator != null;
189        while (iterator.hasNext()) {
190          final Pump<?> distributor = iterator.next();
191          if (distributor != null && consumer.equals(distributor.getEventConsumer())) {
192            iterator.remove();
193            break;
194          }
195        }
196      } finally {
197        this.writeLock.unlock();
198      }
199    }
200  }
201
202  /**
203   * Releases resources held by this {@link EventDistributor} during
204   * its execution.
205   */
206  @Override
207  public final void close() {
208    this.writeLock.lock();
209    try {
210      this.distributors.parallelStream()
211        .forEach(distributor -> {
212            distributor.close();
213          });
214      this.synchronizingDistributors.clear();
215      this.distributors.clear();
216    } finally {
217      this.writeLock.unlock();
218    }
219  }
220
221  /**
222   * Returns {@code true} if this {@link EventDistributor} should
223   * <em>synchronize</em> with its upstream source.
224   *
225   * <h2>Design Notes</h2>
226   *
227   * <p>The Kubernetes {@code tools/cache} package spreads
228   * synchronization out among the reflector, controller, event cache
229   * and event processor constructs for no seemingly good reason.
230   * They should probably be consolidated, particularly in an
231   * object-oriented environment such as Java.</p>
232   *
233   * @return {@code true} if synchronization should occur; {@code
234   * false} otherwise
235   *
236   * @see EventCache#synchronize()
237   */
238  public final boolean shouldSynchronize() {
239    boolean returnValue = false;
240    this.writeLock.lock();
241    try {
242      this.synchronizingDistributors.clear();
243      final Instant now = Instant.now();
244      this.distributors.parallelStream()
245        .filter(distributor -> distributor.shouldSynchronize(now))
246        .forEach(distributor -> {
247            this.synchronizingDistributors.add(distributor);
248            distributor.determineNextSynchronizationInterval(now);
249          });
250      returnValue = !this.synchronizingDistributors.isEmpty();
251    } finally {
252      this.writeLock.unlock();
253    }
254    return returnValue;
255  }
256
257  /**
258   * Consumes the supplied {@link AbstractEvent} by forwarding it to
259   * the {@link Consumer#accept(Object)} method of each {@link
260   * Consumer} {@linkplain #addConsumer(Consumer) registered} with
261   * this {@link EventDistributor}.
262   *
263   * @param event the {@link AbstractEvent} to forward; may be {@code
264   * null} in which case no action is taken
265   *
266   * @see #addConsumer(Consumer)
267   */
268  @Override
269  protected final void accept(final AbstractEvent<? extends T> event) {
270    if (event != null) {
271      if (event instanceof SynchronizationEvent) {
272        this.accept((SynchronizationEvent<? extends T>)event);
273      } else if (event instanceof Event) {
274        this.accept((Event<? extends T>)event);
275      } else {
276        assert false : "Unexpected event type: " + event.getClass();
277      }
278    }
279  }
280
281  private final void accept(final SynchronizationEvent<? extends T> event) {
282    this.readLock.lock();
283    try {
284      if (!this.synchronizingDistributors.isEmpty()) {
285        this.synchronizingDistributors.parallelStream()
286          .forEach(distributor -> distributor.accept(event));
287      }
288    } finally {
289      this.readLock.unlock();
290    }
291  }
292
293  private final void accept(final Event<? extends T> event) {
294    this.readLock.lock();
295    try {
296      if (!this.distributors.isEmpty()) {
297        this.distributors.parallelStream()
298          .forEach(distributor -> distributor.accept(event));
299      }
300    } finally {
301      this.readLock.unlock();
302    }
303  }
304
305
306  /*
307   * Inner and nested classes.
308   */
309  
310
311  /**
312   * A {@link Consumer} of {@link AbstractEvent} instances that puts
313   * them on an internal queue and, in a separate thread, removes them
314   * from the queue and forwards them to the "real" {@link Consumer}
315   * supplied at construction time.
316   *
317   * @author <a href="https://about.me/lairdnelson"
318   * target="_parent">Laird Nelson</a>
319   */
320  private static final class Pump<T extends HasMetadata> implements Consumer<AbstractEvent<? extends T>>, AutoCloseable {
321
322    private volatile boolean closing;
323    
324    private volatile Instant nextSynchronizationInstant;
325    
326    private volatile Duration synchronizationInterval;
327    
328    final BlockingQueue<AbstractEvent<? extends T>> queue;
329    
330    private final ScheduledExecutorService executor;
331
332    private final Future<?> task;
333    
334    private final Consumer<? super AbstractEvent<? extends T>> eventConsumer;
335    
336    private Pump(final Duration synchronizationInterval, final Consumer<? super AbstractEvent<? extends T>> eventConsumer) {
337      super();
338      Objects.requireNonNull(eventConsumer);
339      this.eventConsumer = eventConsumer;
340      this.executor = this.createScheduledThreadPoolExecutor();
341      if (this.executor == null) {
342        throw new IllegalStateException("createScheduledThreadPoolExecutor() == null");
343      }
344      this.queue = new LinkedBlockingQueue<>();
345
346      // Schedule a hopefully never-ending task to pump events from
347      // our queue to the supplied eventConsumer.  We schedule this
348      // instead of simply executing it so that if for any reason it
349      // exits it will get restarted.  Cancelling a scheduled task
350      // will also cancel all resubmissions of it, so this is the most
351      // robust thing to do.  The delay of one second is arbitrary.
352      this.task = this.executor.scheduleWithFixedDelay(() -> {
353          try {
354            while (!Thread.currentThread().isInterrupted()) {
355              this.eventConsumer.accept(this.queue.take());
356            }
357          } catch (final InterruptedException interruptedException) {
358            Thread.currentThread().interrupt();
359          }
360        }, 0L, 1L, TimeUnit.SECONDS);
361      
362      this.setSynchronizationInterval(synchronizationInterval);
363    }
364
365    private final ScheduledExecutorService createScheduledThreadPoolExecutor() {
366      final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
367      executor.setRemoveOnCancelPolicy(true);
368      return executor;
369    }
370    
371    private final Consumer<? super AbstractEvent<? extends T>> getEventConsumer() {
372      return this.eventConsumer;
373    }
374    
375    /**
376     * Adds the supplied {@link AbstractEvent} to an internal {@link
377     * BlockingQueue} and schedules a task to consume it.
378     *
379     * @param event the {@link AbstractEvent} to add; may be {@code
380     * null} in which case no action is taken
381     */
382    @Override
383    public final void accept(final AbstractEvent<? extends T> event) {
384      if (this.closing) {
385        throw new IllegalStateException();
386      }
387      if (event != null) {
388        final boolean added = this.queue.add(event);
389        assert added;
390      }
391    }
392    
393    @Override
394    public final void close() {
395      this.closing = true;
396      this.executor.shutdown();
397      this.task.cancel(true);
398      try {
399        if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) {
400          this.executor.shutdownNow();
401          if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) {
402            // TODO: log
403          }
404        }
405      } catch (final InterruptedException interruptedException) {
406        this.executor.shutdownNow();
407        Thread.currentThread().interrupt();
408      }
409    }
410    
411    
412    /*
413     * Synchronization-related methods.  It seems odd that one of these
414     * listeners would need to report details about synchronization, but
415     * that's what the Go code does.  Maybe this functionality could be
416     * relocated "higher up".
417     */
418    
419    
420    private final boolean shouldSynchronize(Instant now) {
421      final Duration interval = this.getSynchronizationInterval();
422      final boolean returnValue;
423      if (interval == null || interval.isZero()) {
424        returnValue = false;
425      } else if (now == null) {
426        returnValue = Instant.now().compareTo(this.nextSynchronizationInstant) >= 0;
427      } else {
428        returnValue = now.compareTo(this.nextSynchronizationInstant) >= 0;
429      }
430      return returnValue;
431    }
432    
433    private final void determineNextSynchronizationInterval(Instant now) {
434      final Duration synchronizationInterval = this.getSynchronizationInterval();
435      if (synchronizationInterval == null) {
436        if (now == null) {
437          this.nextSynchronizationInstant = Instant.now();
438        } else {
439          this.nextSynchronizationInstant = now;
440        }
441      } else if (now == null) {
442        this.nextSynchronizationInstant = Instant.now().plus(synchronizationInterval);
443      } else {
444        this.nextSynchronizationInstant = now.plus(synchronizationInterval);
445      }
446    }
447    
448    public final void setSynchronizationInterval(final Duration synchronizationInterval) {
449      this.synchronizationInterval = synchronizationInterval;
450    }
451    
452    public final Duration getSynchronizationInterval() {
453      return this.synchronizationInterval;
454    }
455  }
456  
457}