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
110   * ResourceTrackingEventQueueConsumer#ResourceTrackingEventQueueConsumer(Map)
111   */
112  public EventDistributor(final Map<Object, T> knownObjects) {
113    super(knownObjects);
114    final ReadWriteLock lock = new ReentrantReadWriteLock();
115    this.readLock = lock.readLock();
116    this.writeLock = lock.writeLock();
117    this.distributors = new ArrayList<>();    
118    this.synchronizingDistributors = new ArrayList<>();
119    this.synchronizationInterval = null; // TODO: implement/fix
120  }
121
122
123  /*
124   * Instance methods.
125   */
126  
127
128  /**
129   * Adds the supplied {@link Consumer} to this {@link
130   * EventDistributor} as a listener that will be notified of each
131   * {@link AbstractEvent} this {@link EventDistributor} receives.
132   *
133   * <p>The supplied {@link Consumer}'s {@link
134   * Consumer#accept(Object)} method may be called later on a separate
135   * thread of execution.</p>
136   *
137   * @param consumer a {@link Consumer} of {@link AbstractEvent}s; may
138   * be {@code null} in which case no action will be taken
139   *
140   * @see #removeConsumer(Consumer)
141   */
142  public final void addConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) {
143    if (consumer != null) {
144      this.writeLock.lock();
145      try {
146        final Pump<T> distributor = new Pump<>(this.synchronizationInterval, consumer);
147        this.distributors.add(distributor);
148        this.synchronizingDistributors.add(distributor);
149      } finally {
150        this.writeLock.unlock();
151      }
152    }
153  }
154
155  /**
156   * Removes any {@link Consumer} {@linkplain Object#equals(Object)
157   * equal to} a {@link Consumer} previously {@linkplain
158   * #addConsumer(Consumer) added} to this {@link EventDistributor}.
159   *
160   * @param consumer the {@link Consumer} to remove; may be {@code
161   * null} in which case no action will be taken
162   *
163   * @see #addConsumer(Consumer)
164   */
165  public final void removeConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) {
166    if (consumer != null) {
167      this.writeLock.lock();
168      try {
169        final Iterator<? extends Pump<?>> iterator = this.distributors.iterator();
170        assert iterator != null;
171        while (iterator.hasNext()) {
172          final Pump<?> distributor = iterator.next();
173          if (distributor != null && consumer.equals(distributor.getEventConsumer())) {
174            iterator.remove();
175            break;
176          }
177        }
178      } finally {
179        this.writeLock.unlock();
180      }
181    }
182  }
183
184  /**
185   * Releases resources held by this {@link EventDistributor} during
186   * its execution.
187   */
188  @Override
189  public final void close() {
190    this.writeLock.lock();
191    try {
192      this.distributors.parallelStream()
193        .forEach(distributor -> {
194            distributor.close();
195          });
196      this.synchronizingDistributors.clear();
197      this.distributors.clear();
198    } finally {
199      this.writeLock.unlock();
200    }
201  }
202
203  /**
204   * Returns {@code true} if this {@link EventDistributor} should
205   * <em>synchronize</em> with its upstream source.
206   *
207   * <h2>Design Notes</h2>
208   *
209   * <p>The Kubernetes {@code tools/cache} package spreads
210   * synchronization out among the reflector, controller, event cache
211   * and event processor constructs for no seemingly good reason.
212   * They should probably be consolidated, particularly in an
213   * object-oriented environment such as Java.</p>
214   *
215   * @return {@code true} if synchronization should occur; {@code
216   * false} otherwise
217   *
218   * @see EventCache#synchronize()
219   */
220  public final boolean shouldSynchronize() {
221    boolean returnValue = false;
222    this.writeLock.lock();
223    try {
224      this.synchronizingDistributors.clear();
225      final Instant now = Instant.now();
226      this.distributors.parallelStream()
227        .filter(distributor -> distributor.shouldSynchronize(now))
228        .forEach(distributor -> {
229            this.synchronizingDistributors.add(distributor);
230            distributor.determineNextSynchronizationInterval(now);
231          });
232      returnValue = !this.synchronizingDistributors.isEmpty();
233    } finally {
234      this.writeLock.unlock();
235    }
236    return returnValue;
237  }
238
239  /**
240   * Consumes the supplied {@link AbstractEvent} by forwarding it to
241   * the {@link Consumer#accept(Object)} method of each {@link
242   * Consumer} {@linkplain #addConsumer(Consumer) registered} with
243   * this {@link EventDistributor}.
244   *
245   * @param event the {@link AbstractEvent} to forward; may be {@code
246   * null} in which case no action is taken
247   *
248   * @see #addConsumer(Consumer)
249   */
250  @Override
251  protected final void accept(final AbstractEvent<? extends T> event) {
252    if (event != null) {
253      if (event instanceof SynchronizationEvent) {
254        this.accept((SynchronizationEvent<? extends T>)event);
255      } else if (event instanceof Event) {
256        this.accept((Event<? extends T>)event);
257      } else {
258        assert false : "Unexpected event type: " + event.getClass();
259      }
260    }
261  }
262
263  private final void accept(final SynchronizationEvent<? extends T> event) {
264    this.readLock.lock();
265    try {
266      if (!this.synchronizingDistributors.isEmpty()) {
267        this.synchronizingDistributors.parallelStream()
268          .forEach(distributor -> distributor.accept(event));
269      }
270    } finally {
271      this.readLock.unlock();
272    }
273  }
274
275  private final void accept(final Event<? extends T> event) {
276    this.readLock.lock();
277    try {
278      if (!this.distributors.isEmpty()) {
279        this.distributors.parallelStream()
280          .forEach(distributor -> distributor.accept(event));
281      }
282    } finally {
283      this.readLock.unlock();
284    }
285  }
286
287
288  /*
289   * Inner and nested classes.
290   */
291  
292
293  /**
294   * A {@link Consumer} of {@link AbstractEvent} instances that puts
295   * them on an internal queue and, in a separate thread, removes them
296   * from the queue and forwards them to the "real" {@link Consumer}
297   * supplied at construction time.
298   *
299   * @author <a href="https://about.me/lairdnelson"
300   * target="_parent">Laird Nelson</a>
301   */
302  private static final class Pump<T extends HasMetadata> implements Consumer<AbstractEvent<? extends T>>, AutoCloseable {
303
304    private volatile boolean closing;
305    
306    private volatile Instant nextSynchronizationInstant;
307    
308    private volatile Duration synchronizationInterval;
309    
310    final BlockingQueue<AbstractEvent<? extends T>> queue;
311    
312    private final ScheduledExecutorService executor;
313
314    private final Future<?> task;
315    
316    private final Consumer<? super AbstractEvent<? extends T>> eventConsumer;
317    
318    private Pump(final Duration synchronizationInterval, final Consumer<? super AbstractEvent<? extends T>> eventConsumer) {
319      super();
320      Objects.requireNonNull(eventConsumer);
321      this.eventConsumer = eventConsumer;
322      this.executor = this.createScheduledThreadPoolExecutor();
323      if (this.executor == null) {
324        throw new IllegalStateException("createScheduledThreadPoolExecutor() == null");
325      }
326      this.queue = new LinkedBlockingQueue<>();
327
328      // Schedule a hopefully never-ending task to pump events from
329      // our queue to the supplied eventConsumer.  We schedule this
330      // instead of simply executing it so that if for any reason it
331      // exits it will get restarted.  Cancelling a scheduled task
332      // will also cancel all resubmissions of it, so this is the most
333      // robust thing to do.  The delay of one second is arbitrary.
334      this.task = this.executor.scheduleWithFixedDelay(() -> {
335          try {
336            while (!Thread.currentThread().isInterrupted()) {
337              this.eventConsumer.accept(this.queue.take());
338            }
339          } catch (final InterruptedException interruptedException) {
340            Thread.currentThread().interrupt();
341          }
342        }, 0L, 1L, TimeUnit.SECONDS);
343      
344      this.setSynchronizationInterval(synchronizationInterval);
345    }
346
347    private final ScheduledExecutorService createScheduledThreadPoolExecutor() {
348      final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
349      executor.setRemoveOnCancelPolicy(true);
350      return executor;
351    }
352    
353    private final Consumer<? super AbstractEvent<? extends T>> getEventConsumer() {
354      return this.eventConsumer;
355    }
356    
357    /**
358     * Adds the supplied {@link AbstractEvent} to an internal {@link
359     * BlockingQueue} and schedules a task to consume it.
360     *
361     * @param event the {@link AbstractEvent} to add; may be {@code
362     * null} in which case no action is taken
363     */
364    @Override
365    public final void accept(final AbstractEvent<? extends T> event) {
366      if (this.closing) {
367        throw new IllegalStateException();
368      }
369      if (event != null) {
370        final boolean added = this.queue.add(event);
371        assert added;
372      }
373    }
374    
375    @Override
376    public final void close() {
377      this.closing = true;
378      this.executor.shutdown();
379      this.task.cancel(true);
380      try {
381        if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) {
382          this.executor.shutdownNow();
383          if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) {
384            // TODO: log
385          }
386        }
387      } catch (final InterruptedException interruptedException) {
388        this.executor.shutdownNow();
389        Thread.currentThread().interrupt();
390      }
391    }
392    
393    
394    /*
395     * Synchronization-related methods.  It seems odd that one of these
396     * listeners would need to report details about synchronization, but
397     * that's what the Go code does.  Maybe this functionality could be
398     * relocated "higher up".
399     */
400    
401    
402    private final boolean shouldSynchronize(Instant now) {
403      if (now == null) {
404        now = Instant.now();
405      }
406      final Duration interval = this.getSynchronizationInterval();
407      final boolean returnValue = interval != null && !interval.isZero() && now.compareTo(this.nextSynchronizationInstant) >= 0;
408      return returnValue;
409    }
410    
411    private final void determineNextSynchronizationInterval(Instant now) {
412      if (now == null) {
413        now = Instant.now();
414      }
415      this.nextSynchronizationInstant = now.plus(this.synchronizationInterval);
416    }
417    
418    public final void setSynchronizationInterval(final Duration synchronizationInterval) {
419      this.synchronizationInterval = synchronizationInterval;
420    }
421    
422    public final Duration getSynchronizationInterval() {
423      return this.synchronizationInterval;
424    }
425  }
426  
427}