All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.microbean.kubernetes.controller.EventQueueCollection Maven / Gradle / Ivy

There is a newer version: 0.3.0
Show newest version
/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
 *
 * Copyright © 2017-2018 microBean.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package org.microbean.kubernetes.controller;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

import java.io.Serializable;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import java.util.logging.Level;
import java.util.logging.Logger;

import io.fabric8.kubernetes.api.model.HasMetadata;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

import org.microbean.development.annotation.Blocking;
import org.microbean.development.annotation.NonBlocking;

/**
 * An {@link EventCache} that temporarily stores {@link Event}s in
 * {@link EventQueue}s, one per named Kubernetes resource, and
 * {@linkplain #start(Consumer) provides a means for processing those
 * queues}.
 *
 * 

Thread Safety

* *

This class is safe for concurrent use by multiple {@link * Thread}s.

* *

Design Notes

* *

This class loosely models the {@code * DeltaFIFO} type in the Kubernetes Go client {@code tools/cache} * package.

* * @param a type of Kubernetes resource * * @author Laird Nelson * * @see #add(Object, AbstractEvent.Type, HasMetadata) * * @see #replace(Collection, Object) * * @see #synchronize() * * @see #start(Consumer) * * @see EventQueue */ @ThreadSafe public class EventQueueCollection implements EventCache, Supplier>, AutoCloseable { /* * Instance fields. */ /** * A {@link PropertyChangeSupport} object that manages {@link * PropertyChangeEvent}s on behalf of this {@link * EventQueueCollection}. * *

This field is never {@code null}.

* * @see #addPropertyChangeListener(String, PropertyChangeListener) */ private final PropertyChangeSupport propertyChangeSupport; /** * Whether this {@link EventQueueCollection} is in the process of * {@linkplain #close() closing}. * * @see #close() */ private volatile boolean closing; /** * Whether or not this {@link EventQueueCollection} has been * populated via an invocation of the {@link #replace(Collection, * Object)} method. * *

Mutations of this field must be synchronized on {@code * this}.

* * @see #replace(Collection, Object) */ @GuardedBy("this") private boolean populated; /** * The number of {@link EventQueue}s that this {@link * EventQueueCollection} was initially {@linkplain * #replace(Collection, Object) seeded with}. * *

Mutations of this field must be synchronized on {@code * this}.

* * @see #replace(Collection, Object) */ @GuardedBy("this") private int initialPopulationCount; /** * A {@link LinkedHashMap} of {@link EventQueue} instances, indexed * by {@linkplain EventQueue#getKey() their keys}. * *

This field is never {@code null}.

* *

Mutations of the contents of this {@link LinkedHashMap} must * be synchronized on {@code this}.

* * @see #add(Object, AbstractEvent.Type, HasMetadata) */ @GuardedBy("this") private final LinkedHashMap> eventQueueMap; /** * A {@link Map} containing the last known state of Kubernetes * resources this {@link EventQueueCollection} is caching events * for. This field is used chiefly by the {@link #synchronize()} * method, but by others as well. * *

This field may be {@code null}.

* *

Mutations of this field must be synchronized on this field's * value.

* * @see #getKnownObjects() * * @see #synchronize() */ @GuardedBy("itself") private final Map knownObjects; @GuardedBy("this") private ScheduledExecutorService consumerExecutor; @GuardedBy("this") private Future eventQueueConsumptionTask; private final Function errorHandler; /** * A {@link Logger} used by this {@link EventQueueCollection}. * *

This field is never {@code null}.

* * @see #createLogger() */ protected final Logger logger; /* * Constructors. */ /** * Creates a new {@link EventQueueCollection} with an initial * capacity of {@code 16} and a load factor of {@code 0.75} that is * not interested in tracking Kubernetes resource deletions. * * @see #EventQueueCollection(Map, int, float) */ public EventQueueCollection() { this(null, null, 16, 0.75f); } /** * Creates a new {@link EventQueueCollection} with an initial * capacity of {@code 16} and a load factor of {@code 0.75}. * * @param knownObjects a {@link Map} containing the last known state * of Kubernetes resources this {@link EventQueueCollection} is * caching events for; may be {@code null} if this {@link * EventQueueCollection} is not interested in tracking deletions of * objects; if non-{@code null} will be synchronized on by * this class during retrieval and traversal operations * * @see #EventQueueCollection(Map, int, float) */ public EventQueueCollection(final Map knownObjects) { this(knownObjects, null, 16, 0.75f); } /** * Creates a new {@link EventQueueCollection}. * * @param knownObjects a {@link Map} containing the last known state * of Kubernetes resources this {@link EventQueueCollection} is * caching events for; may be {@code null} if this {@link * EventQueueCollection} is not interested in tracking deletions of * objects; if non-{@code null} will be synchronized on by * this class during retrieval and traversal operations * * @param initialCapacity the initial capacity of the internal data * structure used to house this {@link EventQueueCollection}'s * {@link EventQueue}s; must be an integer greater than {@code 0} * * @param loadFactor the load factor of the internal data structure * used to house this {@link EventQueueCollection}'s {@link * EventQueue}s; must be a positive number between {@code 0} and * {@code 1} */ public EventQueueCollection(final Map knownObjects, final int initialCapacity, final float loadFactor) { this(knownObjects, null, initialCapacity, loadFactor); } /** * Creates a new {@link EventQueueCollection}. * * @param knownObjects a {@link Map} containing the last known state * of Kubernetes resources this {@link EventQueueCollection} is * caching events for; may be {@code null} if this {@link * EventQueueCollection} is not interested in tracking deletions of * objects; if non-{@code null} will be synchronized on by * this class during retrieval and traversal operations * * @param errorHandler a {@link Function} that accepts a {@link * Throwable} and returns a {@link Boolean} indicating whether the * error was handled or not; used to handle truly unanticipated * errors from within a {@link ScheduledThreadPoolExecutor}; may be * {@code null} * * @param initialCapacity the initial capacity of the internal data * structure used to house this {@link EventQueueCollection}'s * {@link EventQueue}s; must be an integer greater than {@code 0} * * @param loadFactor the load factor of the internal data structure * used to house this {@link EventQueueCollection}'s {@link * EventQueue}s; must be a positive number between {@code 0} and * {@code 1} */ public EventQueueCollection(final Map knownObjects, final Function errorHandler, final int initialCapacity, final float loadFactor) { super(); final String cn = this.getClass().getName(); final String mn = ""; this.logger = this.createLogger(); if (logger == null) { throw new IllegalStateException(); } if (this.logger.isLoggable(Level.FINER)) { final String knownObjectsString; if (knownObjects == null) { knownObjectsString = null; } else { synchronized (knownObjects) { knownObjectsString = knownObjects.toString(); } } this.logger.entering(cn, mn, new Object[] { knownObjectsString, Integer.valueOf(initialCapacity), Float.valueOf(loadFactor) }); } this.propertyChangeSupport = new PropertyChangeSupport(this); this.eventQueueMap = new LinkedHashMap<>(initialCapacity, loadFactor); this.knownObjects = knownObjects; if (errorHandler == null) { this.errorHandler = t -> { if (this.logger.isLoggable(Level.SEVERE)) { this.logger.logp(Level.SEVERE, this.getClass().getName(), "", t.getMessage(), t); } return true; }; } else { this.errorHandler = errorHandler; } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /* * Instance methods. */ /** * Returns a {@link Logger} for use by this {@link * EventQueueCollection}. * *

This method never returns {@code null}.

* *

Overrides of this method must not return {@code null}.

* * @return a non-{@code null} {@link Logger} for use by this {@link * EventQueueCollection} */ protected Logger createLogger() { return Logger.getLogger(this.getClass().getName()); } private final Map getKnownObjects() { return this.knownObjects; } /** * Returns {@code true} if this {@link EventQueueCollection} is empty. * * @return {@code true} if this {@link EventQueueCollection} is * empty; {@code false} otherwise */ private synchronized final boolean isEmpty() { return this.eventQueueMap.isEmpty(); } /** * Returns {@code true} if this {@link EventQueueCollection} has * been populated via a call to {@link #add(Object, AbstractEvent.Type, * HasMetadata)} at some point, and if there are no {@link * EventQueue}s remaining to be {@linkplain #start(Consumer) * removed}. * *

This is a bound * property.

* * @return {@code true} if this {@link EventQueueCollection} has * been populated via a call to {@link #add(Object, AbstractEvent.Type, * HasMetadata)} at some point, and if there are no {@link * EventQueue}s remaining to be {@linkplain #start(Consumer) * removed}; {@code false} otherwise * * @see #replace(Collection, Object) * * @see #add(Object, AbstractEvent.Type, HasMetadata) * * @see #synchronize() */ public synchronized final boolean isSynchronized() { return this.populated && this.initialPopulationCount == 0; } /** * Synchronizes on the {@code knownObjects} object * {@linkplain #EventQueueCollection(Map, int, float) supplied at * construction time}, if there is one, and, for every Kubernetes * resource found within at the time of this call, adds a {@link * SynchronizationEvent} for it with an {@link AbstractEvent.Type} * of {@link AbstractEvent.Type#MODIFICATION}. */ @Override public final void synchronize() { final String cn = this.getClass().getName(); final String mn = "synchronize"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } synchronized (this) { final Map knownObjects = this.getKnownObjects(); if (knownObjects != null) { synchronized (knownObjects) { if (!knownObjects.isEmpty()) { final Collection values = knownObjects.values(); if (values != null && !values.isEmpty()) { for (final T knownObject : values) { if (knownObject != null) { // We follow the Go code in that we use *our* key // extraction logic, rather than relying on the // known key in the knownObjects map. See // https://github.com/kubernetes/client-go/blob/37c3c02ec96533daec0dbda1f39a6b1d68505c79/tools/cache/delta_fifo.go#L567. // I'm not sure this is significant as they should // evaluate to the same thing, but there may be an // edge case I'm not thinking of. final Object key = this.getKey(knownObject); if (key != null) { final EventQueue eventQueue = this.eventQueueMap.get(key); if (eventQueue == null || eventQueue.isEmpty()) { // There was an object in our knownObjects map // somehow, but not in one of the queues we // manage. Make sure others know about it. // We make a SynchronizationEvent of type // MODIFICATION. shared_informer.go checks in // its HandleDeltas function to see if oldObj // exists; if so, it's a modification // (https://github.com/kubernetes/client-go/blob/37c3c02ec96533daec0dbda1f39a6b1d68505c79/tools/cache/shared_informer.go#L354-L358). // Here we take action *only if* the equivalent // of oldObj exists, therefore this is a // SynchronizationEvent of type MODIFICATION, // not ADDITION. this.addSynchronizationEvent(this, AbstractEvent.Type.MODIFICATION, knownObject); } } } } } } } } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * At a high level, fully replaces the internal state of this {@link * EventQueueCollection} to reflect only the Kubernetes resources * contained in the supplied {@link Collection}. * *

{@link SynchronizationEvent}s of type {@link * AbstractEvent.Type#ADDITION} are added for every resource present * in the {@code incomingResources} parameter.

* *

{@link Event}s of type {@link AbstractEvent.Type#DELETION} are * added when this {@link EventQueueCollection} can determine that * the lack of a resource's presence in the {@code * incomingResources} parameter indicates that it has been deleted * from Kubernetes.

* *

{@link EventQueue}s managed by this {@link * EventQueueCollection} that have not yet {@linkplain * #start(Consumer) been processed} are not removed by this * operation.

* *

This method synchronizes on the supplied {@code * incomingResources} {@link Collection} while iterating * over it.

* * @param incomingResources the {@link Collection} of Kubernetes * resources with which to replace this {@link * EventQueueCollection}'s internal state; will be * synchronized on; may be {@code null} or {@linkplain * Collection#isEmpty() empty}, which will be taken as an indication * that this {@link EventQueueCollection} should effectively be * emptied * * @param resourceVersion the version of the Kubernetes list * resource that contained the incoming resources; currently * ignored but reserved for future use; may be {@code null} * * @exception IllegalStateException if the {@link * #createEvent(Object, AbstractEvent.Type, HasMetadata)} method returns * {@code null} for any reason * * @see SynchronizationEvent * * @see #createEvent(Object, AbstractEvent.Type, HasMetadata) */ @Override public synchronized final void replace(final Collection incomingResources, final Object resourceVersion) { final String cn = this.getClass().getName(); final String mn = "replace"; if (this.logger.isLoggable(Level.FINER)) { final String incomingResourcesString; if (incomingResources == null) { incomingResourcesString = null; } else { synchronized (incomingResources) { incomingResourcesString = incomingResources.toString(); } } this.logger.entering(cn, mn, new Object[] { incomingResourcesString, resourceVersion }); } final boolean oldSynchronized = this.isSynchronized(); final int size; final Set replacementKeys; // Process all the additions first. if (incomingResources == null) { size = 0; replacementKeys = Collections.emptySet(); } else { synchronized (incomingResources) { if (incomingResources.isEmpty()) { size = 0; replacementKeys = Collections.emptySet(); } else { size = incomingResources.size(); assert size > 0; replacementKeys = new HashSet<>(); for (final T resource : incomingResources) { if (resource != null) { replacementKeys.add(this.getKey(resource)); // The final boolean parameter indicates that we don't // want our populated status to be set by this call. We // do this at the bottom of this method ourselves. this.addSynchronizationEvent(this, AbstractEvent.Type.ADDITION, resource, false); } } } } } // Now process deletions. int queuedDeletions = 0; final Map knownObjects = this.getKnownObjects(); if (knownObjects == null) { // No one is keeping track of known objects. The best we can do // is: if there's an EventQueue currently being processed, then // if we get here the object it concerns itself with in // Kubernetes is gone. We need to synthesize a deletion event // to say, effectively, "The object was deleted but we don't // know what its prior state was". for (final EventQueue eventQueue : this.eventQueueMap.values()) { assert eventQueue != null; final Object key; final AbstractEvent newestEvent; synchronized (eventQueue) { if (eventQueue.isEmpty()) { key = null; newestEvent = null; throw new IllegalStateException("eventQueue.isEmpty(): " + eventQueue); } key = eventQueue.getKey(); if (key == null) { throw new IllegalStateException(); } if (replacementKeys.contains(key)) { newestEvent = null; } else { // We have an EventQueue indexed under a key that // identifies a resource that no longer exists in // Kubernetes. Inform any consumers via a deletion event // that this object was removed at some point from // Kubernetes. The state of the object in question is // indeterminate. newestEvent = eventQueue.getLast(); assert newestEvent != null; } } if (newestEvent != null) { assert key != null; // We grab the last event in the queue in question and get // its resource; this will serve as the state of the // Kubernetes resource in question the last time we knew // about it. This state is not necessarily, but could be, // the true actual last state of the resource in question. // The point is, the true state of the object when it was // deleted is unknown. We build a new event to reflect all // this. // // Astute readers will realize that this could result in two // DELETION events enqueued, back to back, with identical // payloads. See the deduplicate() method in EventQueue, // which takes care of this situation. final T resourceToBeDeleted = newestEvent.getResource(); assert resourceToBeDeleted != null; final Event event = this.createEvent(this, AbstractEvent.Type.DELETION, resourceToBeDeleted); if (event == null) { throw new IllegalStateException("createEvent() == null"); } event.setKey(key); // The final boolean parameter indicates that we don't want // our populated status to be set by this call. We do this // at the bottom of this method ourselves. this.add(event, false); } } } else { assert knownObjects != null; // We're keeping track of known objects, so fire deletion events // if objects were removed from Kubernetes. synchronized (knownObjects) { if (!knownObjects.isEmpty()) { final Collection> entrySet = knownObjects.entrySet(); if (entrySet != null && !entrySet.isEmpty()) { for (final Entry entry : entrySet) { if (entry != null) { final Object knownKey = entry.getKey(); if (!replacementKeys.contains(knownKey)) { final Event event = this.createEvent(this, AbstractEvent.Type.DELETION, entry.getValue()); if (event == null) { throw new IllegalStateException("createEvent() == null"); } event.setKey(knownKey); // The final boolean parameter indicates that we // don't want our populated status to be set by this // call. We do this at the bottom of this method // ourselves. this.add(event, false); queuedDeletions++; } } } } } } } if (!this.populated) { this.populated = true; assert size >= 0; assert queuedDeletions >= 0; final int oldInitialPopulationCount = this.initialPopulationCount; this.initialPopulationCount = size + queuedDeletions; this.firePropertyChange("populated", false, true); this.firePropertyChange("initialPopulationCount", oldInitialPopulationCount, this.initialPopulationCount); if (this.initialPopulationCount == 0) { // We know that we are now synchronized because the definition // of being synchronized is to have an initialPopulationCount // of 0 and a populated status of true. assert this.isSynchronized(); this.firePropertyChange("synchronized", oldSynchronized, true); } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * Returns an {@link Object} which will be used as the key that will * uniquely identify the supplied {@code resource} to this {@link * EventQueueCollection}. * *

This method may return {@code null}, but only if {@code * resource} is {@code null} or is constructed in such a way that * its {@link HasMetadata#getMetadata()} method returns {@code * null}.

* *

Overrides of this method may return {@code null}, but only if * {@code resource} is {@code null}. * *

The default implementation of this method returns the return * value of the {@link HasMetadatas#getKey(HasMetadata)} method.

* * @param resource a {@link HasMetadata} for which a key should be * returned; may be {@code null} in which case {@code null} may be * returned * * @return a non-{@code null} key for the supplied {@code resource}; * or {@code null} if {@code resource} is {@code null} * * @see HasMetadatas#getKey(HasMetadata) */ protected Object getKey(final T resource) { final String cn = this.getClass().getName(); final String mn = "getKey"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, resource); } final Object returnValue = HasMetadatas.getKey(resource); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /** * Creates a new {@link EventQueue} suitable for holding {@link * Event}s {@linkplain Event#getKey() matching} the supplied {@code * key}. * *

This method never returns {@code null}.

* *

Overrides of this method must not return {@code null}.

* * @param key the key {@linkplain EventQueue#getKey() for the new * EventQueue}; must not be {@code null} * * @return the new {@link EventQueue}; never {@code null} * * @exception NullPointerException if {@code key} is {@code null} * * @see EventQueue#EventQueue(Object) */ protected EventQueue createEventQueue(final Object key) { final String cn = this.getClass().getName(); final String mn = "createEventQueue"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, key); } final EventQueue returnValue = new EventQueue(key); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /** * Starts a new {@link Thread} that, until {@link #close()} is * called, removes {@link EventQueue}s from this {@link * EventQueueCollection} and supplies them to the supplied {@link * Consumer}, and returns a {@link Future} representing this task. * *

This method never returns {@code null}.

* *

If this method has been called before, then the existing * {@link Future} representing the task that was scheduled is * returned instead.

* *

Invoking this method does not block the calling {@link * Thread}.

* *

The non-{@code null} {@link Future} that is returned will not * return {@code true} from its {@linkplain Future#isDone()} method * unless {@linkplain Future#cancel(boolean) it has been cancelled} * or an exception has occurred. That is, the task represented by * the returned {@link Future} is never-ending under normal * circumstances.

* * @param eventQueueConsumer the {@link Consumer} that will process * each {@link EventQueue} as it becomes ready; must not be {@code * null} * * @return a {@link Future} representing the task that is feeding * {@link EventQueue}s to the supplied {@link Consumer}; never * {@code null}; suitable only for {@linkplain * Future#cancel(boolean) cancellation} * * @exception NullPointerException if {@code eventQueueConsumer} is * {@code null} */ @NonBlocking public final Future start(final Consumer> eventQueueConsumer) { final String cn = this.getClass().getName(); final String mn = "start"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, eventQueueConsumer); } Objects.requireNonNull(eventQueueConsumer); final Future returnValue; synchronized (this) { if (this.consumerExecutor == null) { this.consumerExecutor = createScheduledThreadPoolExecutor(); assert this.consumerExecutor != null : "createScheduledThreadPoolExecutor() == null"; } if (this.eventQueueConsumptionTask == null) { // This task is scheduled, rather than simply executed, so // that if it terminates exceptionally it will be restarted // after one second. The task could have been written to do // this "scheduling" itself (i.e. it could restart a loop // itself in the presence of an exception) but we follow the // Go code idiom here. this.eventQueueConsumptionTask = this.consumerExecutor.scheduleWithFixedDelay(this.createEventQueueConsumptionTask(eventQueueConsumer), 0L, 1L, TimeUnit.SECONDS); } assert this.eventQueueConsumptionTask != null; returnValue = this.eventQueueConsumptionTask; } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } private static final ScheduledThreadPoolExecutor createScheduledThreadPoolExecutor() { final ScheduledThreadPoolExecutor returnValue = new ScheduledThreadPoolExecutor(1); returnValue.setRemoveOnCancelPolicy(true); return returnValue; } /** * Semantically closes this {@link EventQueueCollection} by * detaching any {@link Consumer} previously attached via the {@link * #start(Consumer)} method. {@linkplain #add(Object, AbstractEvent.Type, * HasMetadata) Additions}, {@linkplain #replace(Collection, Object) * replacements} and {@linkplain #synchronize() synchronizations} * are still possible, but there won't be anything consuming any * events generated by or supplied to these operations. * *

A closed {@link EventQueueCollection} may be {@linkplain * #start(Consumer) started} again.

* * @see #start(Consumer) */ @Override public final void close() { final String cn = this.getClass().getName(); final String mn = "close"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } // (closing is a volatile field.) this.closing = true; try { final ScheduledExecutorService consumerExecutor; synchronized (this) { // We keep this synchronized block as small as we can since in // other areas in the code there are threads holding this // object's monitor. // // Cancel the consumer pump task and begin the lengthy process // of shutting down the pump itself. if (this.eventQueueConsumptionTask != null) { this.eventQueueConsumptionTask.cancel(true); this.eventQueueConsumptionTask = null; } consumerExecutor = this.consumerExecutor; this.consumerExecutor = null; } if (consumerExecutor != null) { // Stop accepting new tasks. consumerExecutor.shutdown(); // Cancel all running tasks firmly (there shouldn't be any, // but it's the right thing to do). consumerExecutor.shutdownNow(); try { // Wait for termination to complete normally. This should // complete instantly because there aren't any running // tasks. if (!consumerExecutor.awaitTermination(60, TimeUnit.SECONDS) && this.logger.isLoggable(Level.WARNING)) { this.logger.logp(Level.WARNING, cn, mn, "this.consumerExecutor.awaitTermination() failed"); } } catch (final InterruptedException interruptedException) { Thread.currentThread().interrupt(); } } } finally { this.closing = false; } assert this.eventQueueConsumptionTask == null; assert this.consumerExecutor == null; if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * Creates and returns a {@link Runnable} that will serve as the * "body" of a never-ending task that {@linkplain #get() removes the * eldest EventQueue} from this {@link * EventQueueCollection} and {@linkplain Consumer#accept(Object) * supplies it to the supplied eventQueueConsumer}. * *

This method never returns {@code null}.

* * @param eventQueueConsumer the {@link Consumer} that will act upon * the eldest {@link EventQueue} in this {@link * EventQueueCollection}; must not be {@code null} * * @return a new {@link Runnable}; never {@code null} * * @exception NullPointerException if {@code eventQueueConsumer} is * {@code null} */ private final Runnable createEventQueueConsumptionTask(final Consumer> eventQueueConsumer) { Objects.requireNonNull(eventQueueConsumer); final Runnable returnValue = () -> { // This Runnable loosely models the processLoop() function in // https://github.com/kubernetes/kubernetes/blob/v1.9.0/staging/src/k8s.io/client-go/tools/cache/controller.go#L139-L161. try { while (!Thread.currentThread().isInterrupted()) { // Note that get() *removes* an EventQueue from this // EventQueueCollection, blocking until one is available. @Blocking final EventQueue eventQueue = this.get(); if (eventQueue != null) { Throwable unhandledThrowable = null; synchronized (eventQueue) { try { eventQueueConsumer.accept(eventQueue); } catch (final TransientException transientException) { this.eventQueueMap.putIfAbsent(eventQueue.getKey(), eventQueue); } catch (final Throwable e) { unhandledThrowable = e; } } if (unhandledThrowable != null && !this.errorHandler.apply(unhandledThrowable)) { if (unhandledThrowable instanceof RuntimeException) { throw (RuntimeException)unhandledThrowable; } else if (unhandledThrowable instanceof Error) { throw (Error)unhandledThrowable; } else { assert !(unhandledThrowable instanceof Exception); } } } } } catch (final RuntimeException unhandledRuntimeException) { // This RuntimeException was almost certainly supplied to our // error handler, who had a chance to process it, but who // rejected it for one reason or another. As a last attempt // to make sure it is noticed, we don't just throw this // RuntimeException but also log it because it is very easy // for a Runnable submitted to an ExecutorService to have its // RuntimeExceptions disappear into the ether. if (logger.isLoggable(Level.SEVERE)) { logger.logp(Level.SEVERE, this.getClass().getName(), "", unhandledRuntimeException.getMessage(), unhandledRuntimeException); } throw unhandledRuntimeException; } }; return returnValue; } /** * Implements the {@link Supplier#get()} contract by * removing and returning an {@link EventQueue} if * one is available, blocking if one is not and * returning {@code null} only if the {@linkplain Thread#interrupt() * current thread is interrupted} or this {@linkplain * EventQueueCollection#close() EventQueueCollection is * closing}. * *

Design Notes

* *

This method calls an internal method that models the {@code * Pop} function in {@code delta_fifo.go}.

* * @return an {@link EventQueue}, or {@code null} */ @Blocking @Override public final EventQueue get() { final String cn = this.getClass().getName(); final String mn = "get"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } EventQueue returnValue = null; try { returnValue = this.take(); } catch (final InterruptedException interruptedException) { Thread.currentThread().interrupt(); returnValue = null; } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /** * Blocks until there is an {@link EventQueue} in this {@link * EventQueueCollection} available for removal whereupon it is * removed and returned. * *

This method may return {@code null} if it is invoked while * this {@linkplain #close() EventQueueCollection is * closing}.

* *

Design Notes

* *

This method models the {@code * Pop} function in {@code delta_fifo.go}.

* *

This method is called internally only by the {@link #get()} * method, which is its {@code public}-facing counterpart. {@link * #get()} cannot, by contract, throw an {@link * InterruptedException}; hence this method.

* * @return the {@link EventQueue} that was removed (taken), or, in * exceptional circumstances, {@code null} * * @exception InterruptedException if the {@link * Thread#currentThread() current thread} was interrupted */ @Blocking private final EventQueue take() throws InterruptedException { final String cn = this.getClass().getName(); final String mn = "take"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } final EventQueue returnValue; synchronized (this) { while (this.isEmpty() && !this.closing) { this.wait(); // blocks; see add() for corresponding notifyAll() } // We can assert this because add() is the method that calls // notifyAll(), and add() sets populated to true before it calls // notifyAll(). The notifyAll() call is the only way that the // blocking wait() call above will exit other than interruption, // and if it exited via interruption, an InterruptedException // would have been thrown and we'd never get here. assert this.populated : "this.populated == false"; if (this.isEmpty()) { assert this.closing : "this.isEmpty() && !this.closing"; returnValue = null; } else { // Pop the first EventQueue off and return it. final Iterator> iterator = this.eventQueueMap.values().iterator(); assert iterator != null; assert iterator.hasNext(); returnValue = iterator.next(); assert returnValue != null; iterator.remove(); if (this.initialPopulationCount > 0) { // We know we're not populated and our // initialPopulationCount is not 0, so therefore we are not // synchronized. See isSynchronized(). assert !this.isSynchronized(); final int oldInitialPopulationCount = this.initialPopulationCount; this.initialPopulationCount--; this.firePropertyChange("initialPopulationCount", oldInitialPopulationCount, this.initialPopulationCount); this.firePropertyChange("synchronized", false, this.isSynchronized()); } this.firePropertyChange("empty", false, this.isEmpty()); } } if (this.logger.isLoggable(Level.FINER)) { final String eventQueueString; synchronized (returnValue) { eventQueueString = returnValue.toString(); } this.logger.exiting(cn, mn, eventQueueString); } return returnValue; } /** * Creates an {@link Event} using the supplied raw materials and * returns it. * *

This method never returns {@code null}.

* *

Implementations of this method must not return {@code * null}.

* *

Implementations of this method must return a new {@link Event} * with every invocation.

* * @param source the {@linkplain Event#getSource() source} of the * {@link Event} that will be created; must not be null * * @param eventType the {@linkplain Event#getType() type} of {@link * Event} that will be created; must not be {@code null} * * @param resource the {@linkplain Event#getResource() resource} of * the {@link Event} that will be created; must not be * {@code null} * * @return the created {@link Event}; never {@code null} * * @exception NullPointerException if any parameter is {@code null} */ protected Event createEvent(final Object source, final AbstractEvent.Type eventType, final T resource) { final String cn = this.getClass().getName(); final String mn = "createEvent"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); } Objects.requireNonNull(source); Objects.requireNonNull(eventType); Objects.requireNonNull(resource); final Event returnValue = new Event<>(source, eventType, null, resource); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /** * Creates a {@link SynchronizationEvent} using the supplied raw * materials and returns it. * *

This method never returns {@code null}.

* *

Implementations of this method must not return {@code * null}.

* *

Implementations of this method must return a new {@link * SynchronizationEvent} with every invocation.

* * @param source the {@linkplain SynchronizationEvent#getSource() * source} of the {@link SynchronizationEvent} that will be created; * must not be null * * @param eventType the {@linkplain Event#getType() type} of {@link * SynchronizationEvent} that will be created; must not be {@code * null} * * @param resource the {@linkplain Event#getResource() resource} of * the {@link SynchronizationEvent} that will be created; must not * be {@code null} * * @return the created {@link SynchronizationEvent}; never {@code * null} * * @exception NullPointerException if any parameter is {@code null} */ protected SynchronizationEvent createSynchronizationEvent(final Object source, final AbstractEvent.Type eventType, final T resource) { final String cn = this.getClass().getName(); final String mn = "createSynchronizationEvent"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); } Objects.requireNonNull(source); Objects.requireNonNull(eventType); Objects.requireNonNull(resource); final SynchronizationEvent returnValue = new SynchronizationEvent<>(source, eventType, null, resource); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } private final SynchronizationEvent addSynchronizationEvent(final Object source, final AbstractEvent.Type eventType, final T resource) { return this.addSynchronizationEvent(source, eventType, resource, true); } private final SynchronizationEvent addSynchronizationEvent(final Object source, final AbstractEvent.Type eventType, final T resource, final boolean populate) { final String cn = this.getClass().getName(); final String mn = "addSynchronizationEvent"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { source, eventType, resource, Boolean.valueOf(populate) }); } Objects.requireNonNull(source); Objects.requireNonNull(eventType); Objects.requireNonNull(resource); if (!(eventType.equals(AbstractEvent.Type.ADDITION) || eventType.equals(AbstractEvent.Type.MODIFICATION))) { throw new IllegalArgumentException("Illegal eventType: " + eventType); } final SynchronizationEvent event = this.createSynchronizationEvent(source, eventType, resource); if (event == null) { throw new IllegalStateException("createSynchronizationEvent() == null"); } final SynchronizationEvent returnValue = this.add(event, populate); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /** * Adds a new {@link Event} constructed out of the parameters * supplied to this method to this {@link EventQueueCollection} and * returns the {@link Event} that was added. * *

This method may return {@code null}.

* *

This implementation {@linkplain #createEventQueue(Object) * creates an EventQueue} if necessary for the {@link * Event} that will be added, and then adds the new {@link Event} to * the queue.

* * @param source the {@linkplain Event#getSource() source} of the * {@link Event} that will be created and added; must not be null * * @param eventType the {@linkplain Event#getType() type} of {@link * Event} that will be created and added; must not be {@code null} * * @param resource the {@linkplain Event#getResource() resource} of * the {@link Event} that will be created and added; must not be * {@code null} * * @return the {@link Event} that was created and added, or {@code * null} if no {@link Event} was actually added as a result of this * method's invocation * * @exception NullPointerException if any of the parameters is * {@code null} * * @see Event */ @Override public final Event add(final Object source, final AbstractEvent.Type eventType, final T resource) { return this.add(source, eventType, resource, true); } /** * Adds a new {@link Event} constructed out of the parameters * supplied to this method to this {@link EventQueueCollection} and * returns the {@link Event} that was added. * *

This method may return {@code null}.

* *

This implementation {@linkplain #createEventQueue(Object) * creates an EventQueue} if necessary for the {@link * Event} that will be added, and then adds the new {@link Event} to * the queue.

* * @param source the {@linkplain Event#getSource() source} of the * {@link Event} that will be created and added; must not be null * * @param eventType the {@linkplain Event#getType() type} of {@link * Event} that will be created and added; must not be {@code null} * * @param resource the {@linkplain Event#getResource() resource} of * the {@link Event} that will be created and added; must not be * {@code null} * * @param populate if {@code true} then this {@link * EventQueueCollection} will be internally marked as initially * populated * * @return the {@link Event} that was created and added, or {@code * null} if no {@link Event} was actually added as a result of this * method's invocation * * @exception NullPointerException if any of the parameters is * {@code null} * * @see Event */ private final Event add(final Object source, final AbstractEvent.Type eventType, final T resource, final boolean populate) { final String cn = this.getClass().getName(); final String mn = "add"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { source, eventType, resource, Boolean.valueOf(populate)}); } final Event event = this.createEvent(source, eventType, resource); if (event == null) { throw new IllegalStateException("createEvent() == null"); } final Event returnValue = this.add(event, populate); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /** * Adds the supplied {@link Event} to this {@link * EventQueueCollection} and returns the {@link Event} that was * added. * *

This method may return {@code null}.

* *

This implementation {@linkplain #createEventQueue(Object) * creates an EventQueue} if necessary for the {@link * Event} that will be added, and then adds the new {@link Event} to * the queue.

* * @param an {@link AbstractEvent} type that is both consumed * and returned * * @param event the {@link Event} to add; must not be {@code null} * * @param populate if {@code true} then this {@link * EventQueueCollection} will be internally marked as initially * populated * * @return the {@link Event} that was created and added, or {@code * null} if no {@link Event} was actually added as a result of this * method's invocation * * @exception NullPointerException if any of the parameters is * {@code null} * * @see Event */ private final > E add(final E event, final boolean populate) { final String cn = this.getClass().getName(); final String mn = "add"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { event, Boolean.valueOf(populate) }); } if (this.closing) { throw new IllegalStateException(); } Objects.requireNonNull(event); final Object key = event.getKey(); if (key == null) { throw new IllegalArgumentException("event.getKey() == null"); } E returnValue = null; synchronized (this) { if (populate && !this.populated) { this.populated = true; // TODO: too early? this.firePropertyChange("populated", false, true); } EventQueue eventQueue = this.eventQueueMap.get(key); final boolean eventQueueExisted = eventQueue != null; if (!eventQueueExisted) { eventQueue = this.createEventQueue(key); if (eventQueue == null) { throw new IllegalStateException("createEventQueue(key) == null: " + key); } } assert eventQueue != null; final boolean eventAdded; final boolean eventQueueIsEmpty; synchronized (eventQueue) { eventAdded = eventQueue.addEvent(event); // Adding an event to an EventQueue can result in compression, // which may result in the EventQueue becoming empty as a // result of the add operation. eventQueueIsEmpty = eventQueue.isEmpty(); } if (eventAdded) { returnValue = event; } if (eventQueueIsEmpty) { // Compression might have emptied the queue, so an add could // result in an empty queue. We don't permit empty queues. if (eventQueueExisted) { returnValue = null; final boolean oldEmpty = this.isEmpty(); final Object oldEventQueue = this.eventQueueMap.remove(key); assert oldEventQueue != null; this.firePropertyChange("empty", oldEmpty, this.isEmpty()); } else { // Nothing to do; the queue we added the event to was // created here, and was never added to our internal eventQueueMap, so // we're done. } } else if (!eventQueueExisted) { // We created the EventQueue we just added to; now we need to // store it. final boolean oldEmpty = this.isEmpty(); final Object oldEventQueue = this.eventQueueMap.put(key, eventQueue); assert oldEventQueue == null; this.firePropertyChange("empty", oldEmpty, this.isEmpty()); // Notify anyone blocked on our empty state that we're no // longer empty. this.notifyAll(); } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } /* * PropertyChangeListener support. */ /** * Adds the supplied {@link PropertyChangeListener} to this {@link * EventQueueCollection}'s collection of such listeners so that it * will be notified only when the bound property bearing the * supplied {@code name} changes. * * @param name the name of the bound property whose changes are of * interest; may be {@code null} in which case all property change * notifications will be dispatched to the supplied {@link * PropertyChangeListener} * * @param listener the {@link PropertyChangeListener} to add; may be * {@code null} in which case no action will be taken * * @see #addPropertyChangeListener(PropertyChangeListener) */ public final void addPropertyChangeListener(final String name, final PropertyChangeListener listener) { if (listener != null) { this.propertyChangeSupport.addPropertyChangeListener(name, listener); } } /** * Adds the supplied {@link PropertyChangeListener} to this {@link * EventQueueCollection}'s collection of such listeners so that it * will be notified whenever any bound property of this {@link * EventQueueCollection} changes. * * @param listener the {@link PropertyChangeListener} to add; may be * {@code null} in which case no action will be taken * * @see #addPropertyChangeListener(String, PropertyChangeListener) */ public final void addPropertyChangeListener(final PropertyChangeListener listener) { if (listener != null) { this.propertyChangeSupport.addPropertyChangeListener(listener); } } /** * Removes the supplied {@link PropertyChangeListener} from this * {@link EventQueueCollection} so that it will no longer be * notified of changes to bound properties bearing the supplied * {@code name}. * * @param name a bound property name; may be {@code null} * * @param listener the {@link PropertyChangeListener} to remove; may * be {@code null} in which case no action will be taken * * @see #addPropertyChangeListener(String, PropertyChangeListener) * * @see #removePropertyChangeListener(PropertyChangeListener) */ public final void removePropertyChangeListener(final String name, final PropertyChangeListener listener) { if (listener != null) { this.propertyChangeSupport.removePropertyChangeListener(name, listener); } } /** * Removes the supplied {@link PropertyChangeListener} from this * {@link EventQueueCollection} so that it will no longer be * notified of any changes to bound properties. * * @param listener the {@link PropertyChangeListener} to remove; may * be {@code null} in which case no action will be taken * * @see #addPropertyChangeListener(PropertyChangeListener) * * @see #removePropertyChangeListener(String, PropertyChangeListener) */ public final void removePropertyChangeListener(final PropertyChangeListener listener) { if (listener != null) { this.propertyChangeSupport.removePropertyChangeListener(listener); } } /** * Returns an array of {@link PropertyChangeListener}s that were * {@linkplain #addPropertyChangeListener(String, * PropertyChangeListener) registered} to receive notifications for * changes to bound properties bearing the supplied {@code name}. * *

This method never returns {@code null}.

* * @param name the name of a bound property; may be {@code null} in * which case an empty array will be returned * * @return a non-{@code null} array of {@link * PropertyChangeListener}s * * @see #getPropertyChangeListeners() * * @see #addPropertyChangeListener(String, PropertyChangeListener) * * @see #removePropertyChangeListener(String, * PropertyChangeListener) */ public final PropertyChangeListener[] getPropertyChangeListeners(final String name) { return this.propertyChangeSupport.getPropertyChangeListeners(name); } /** * Returns an array of {@link PropertyChangeListener}s that were * {@linkplain #addPropertyChangeListener(String, * PropertyChangeListener) registered} to receive notifications for * changes to all bound properties. * *

This method never returns {@code null}.

* * @return a non-{@code null} array of {@link * PropertyChangeListener}s * * @see #getPropertyChangeListeners(String) * * @see #addPropertyChangeListener(PropertyChangeListener) * * @see #removePropertyChangeListener(PropertyChangeListener) */ public final PropertyChangeListener[] getPropertyChangeListeners() { return this.propertyChangeSupport.getPropertyChangeListeners(); } /** * Fires a {@link PropertyChangeEvent} to {@linkplain * #addPropertyChangeListener(String, PropertyChangeListener) * registered PropertyChangeListeners} if the supplied * {@code old} and {@code newValue} objects are non-{@code null} and * not equal to each other. * * @param propertyName the name of the bound property that might * have changed; may be {@code null} (indicating that some unknown * set of bound properties has changed) * * @param old the old value of the bound property in question; may * be {@code null} * * @param newValue the new value of the bound property; may be * {@code null} */ protected final void firePropertyChange(final String propertyName, final Object old, final Object newValue) { final String cn = this.getClass().getName(); final String mn = "firePropertyChange"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { propertyName, old, newValue }); } this.propertyChangeSupport.firePropertyChange(propertyName, old, newValue); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * Fires a {@link PropertyChangeEvent} to {@linkplain * #addPropertyChangeListener(String, PropertyChangeListener) * registered PropertyChangeListeners} if the supplied * {@code old} and {@code newValue} objects are non-{@code null} and * not equal to each other. * * @param propertyName the name of the bound property that might * have changed; may be {@code null} (indicating that some unknown * set of bound properties has changed) * * @param old the old value of the bound property in question * * @param newValue the new value of the bound property */ protected final void firePropertyChange(final String propertyName, final int old, final int newValue) { final String cn = this.getClass().getName(); final String mn = "firePropertyChange"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { propertyName, Integer.valueOf(old), Integer.valueOf(newValue) }); } this.propertyChangeSupport.firePropertyChange(propertyName, old, newValue); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * Fires a {@link PropertyChangeEvent} to {@linkplain * #addPropertyChangeListener(String, PropertyChangeListener) * registered PropertyChangeListeners} if the supplied * {@code old} and {@code newValue} objects are non-{@code null} and * not equal to each other. * * @param name the name of the bound property that might * have changed; may be {@code null} (indicating that some unknown * set of bound properties has changed) * * @param old the old value of the bound property in question * * @param newValue the new value of the bound property */ protected final void firePropertyChange(final String name, final boolean old, final boolean newValue) { final String cn = this.getClass().getName(); final String mn = "firePropertyChange"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { name, Boolean.valueOf(old), Boolean.valueOf(newValue) }); } this.propertyChangeSupport.firePropertyChange(name, old, newValue); if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /* * Inner and nested classes. */ /** * A {@link RuntimeException} indicating that a {@link Consumer} * {@linkplain EventQueueCollection#start(Consumer) started} by an * {@link EventQueueCollection} has encountered an error that might * not happen if the consumption operation is retried. * * @author Laird Nelson * * @see EventQueueCollection */ public static final class TransientException extends RuntimeException { /* * Static fields. */ /** * The version of this class for {@linkplain Serializable * serialization purposes}. * * @see Serializable */ private static final long serialVersionUID = 1L; /* * Constructors. */ /** * Creates a new {@link TransientException}. */ public TransientException() { super(); } /** * Creates a new {@link TransientException}. * * @param message a detail message describing the error; may be * {@code null} */ public TransientException(final String message) { super(message); } /** * Creates a new {@link TransientException}. * * @param cause the {@link Throwable} that caused this {@link * TransientException} to be created; may be {@code null} */ public TransientException(final Throwable cause) { super(cause); } /** * Creates a new {@link TransientException}. * * @param message a detail message describing the error; may be * {@code null} * * @param cause the {@link Throwable} that caused this {@link * TransientException} to be created; may be {@code null} */ public TransientException(final String message, final Throwable cause) { super(message, cause); } } /** * A {@link PropertyChangeListener} specifically designed for * reacting to a change in the synchronization status of an {@link * EventQueueCollection} as represented by the firing of its {@code * synchronized} bound * Java Beans property. * * @author Laird Nelson * * @see EventQueueCollection#addPropertyChangeListener(String, * PropertyChangeListener) * * @see #await() * * @see #propertyChange(PropertyChangeEvent) */ public static final class SynchronizationAwaitingPropertyChangeListener implements PropertyChangeListener { /* * Instance fields. */ /** * A {@link CountDownLatch} whose {@link * CountDownLatch#countDown()} method is invoked in certain cases * by the {@link #propertyChange(PropertyChangeEvent)} method. * *

This field is never {@code null}.

* * @see #propertyChange(PropertyChangeEvent) */ private final CountDownLatch latch; /* * Constructors. */ /** * Creates a new {@link * SynchronizationAwaitingPropertyChangeListener}. */ public SynchronizationAwaitingPropertyChangeListener() { super(); this.latch = new CountDownLatch(1); } /* * Instance methods. */ /** * If the supplied {@link PropertyChangeEvent} is non-{@code * null}, has a {@linkplain PropertyChangeEvent#getSource() * source} that is an instance of {@link EventQueueCollection}, * has a {@linkplain PropertyChangeEvent#getPropertyName() * property name} equal to {@code synchronized} and a {@linkplain * PropertyChangeEvent#getNewValue() new value} equal to {@link * Boolean#TRUE}, then it is guaranteed that any calls currently * blocked on the {@link #await()} or {@link #await(long, * TimeUnit)} methods will unblock, and subsequent invocations of * those methods will never block again. * * @param event a {@link PropertyChangeEvent} fired by an {@link * EventQueueCollection}; may be {@code null} in which case no * action will be taken * * @see EventQueueCollection#addPropertyChangeListener(String, * PropertyChangeListener) * * @see EventQueueCollection#isSynchronized() */ @Override public final void propertyChange(final PropertyChangeEvent event) { if (event != null && event.getSource() instanceof EventQueueCollection && "synchronized".equals(event.getPropertyName()) && Boolean.TRUE.equals(event.getNewValue())) { this.latch.countDown(); } } /** * Blocks until the conditions described in the documentation of * the {@link #propertyChange(PropertyChangeEvent)} method hold * true. * * @exception InterruptedException if the current {@link Thread} * is interrupted */ @Blocking public final void await() throws InterruptedException { this.latch.await(); } /** * Blocks until the conditions described in the documentation of * the {@link #propertyChange(PropertyChangeEvent)} method hold * true or the indicated time has passed. * * @param timeout the number of units of time to wait for * * @param timeUnit the unit of time designated by the {@code * timeout} parameter; must not be {@code null} * * @return {@code false} if the waiting time elapsed before the * bound property named {@code synchronized} changed its value to * {@code true}; {@code true} otherwise * * @exception InterruptedException if the current {@link Thread} * is interrupted * * @exception NullPointerException if {@code timeUnit} is {@code * null} * * @see #propertyChange(PropertyChangeEvent) */ @Blocking public final boolean await(final long timeout, final TimeUnit timeUnit) throws InterruptedException { return this.latch.await(timeout, timeUnit); } } }