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

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

/* -*- 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.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterator;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

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

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

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

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.

* * @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> map; /** * 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; private volatile Future eventQueueConsumptionTask; /** * 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, 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, 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) { 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.map = new LinkedHashMap<>(initialCapacity, loadFactor); this.knownObjects = knownObjects; 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.map.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. final Object key = this.getKey(knownObject); if (key != null) { final EventQueue eventQueue = this.map.get(key); if (eventQueue == null || eventQueue.isEmpty()) { // 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. Here we // take action *only if* the equivalent of // oldObj exists, therefore this is a // SynchronizationEvent of type MODIFICATION, // not ADDITION. this.synchronize(this, AbstractEvent.Type.MODIFICATION, knownObject, true /* yes, populate */); } } } } } } } } } 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}, notionally firing * {@link SynchronizationEvent}s and {@link Event}s of type {@link * AbstractEvent.Type#DELETION} as appropriate. * *

{@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; 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; 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; 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)); this.synchronize(this, AbstractEvent.Type.ADDITION, resource, false); } } } } } int queuedDeletions = 0; final Map knownObjects = this.getKnownObjects(); if (knownObjects == null) { for (final EventQueue eventQueue : this.map.values()) { assert eventQueue != null; final Object key; final AbstractEvent newestEvent; synchronized (eventQueue) { if (eventQueue.isEmpty()) { newestEvent = null; key = null; assert false : "eventQueue.isEmpty(): " + eventQueue; } else { 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); this.add(event, false /* don't treat this as a population event */); } } } else { 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); this.add(event, false /* don't treat this as a population event */); queuedDeletions++; } } } } } } } if (!this.populated) { this.populated = true; this.firePropertyChange("populated", false, true); assert size >= 0; assert queuedDeletions >= 0; final int oldInitialPopulationCount = this.initialPopulationCount; this.initialPopulationCount = size + queuedDeletions; this.firePropertyChange("initialPopulationCount", oldInitialPopulationCount, this.initialPopulationCount); if (this.initialPopulationCount == 0) { 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 may return {@code null}.

* *

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

* * @param siphon 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}, or {@code * null} if no task was started * * @exception NullPointerException if {@code siphon} is {@code null} */ @NonBlocking public final Future start(final Consumer> siphon) { final String cn = this.getClass().getName(); final String mn = "start"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, siphon); } Objects.requireNonNull(siphon); final Future returnValue; synchronized (this) { if (this.consumerExecutor == null) { this.consumerExecutor = this.createScheduledThreadPoolExecutor(); if (this.consumerExecutor == null) { throw new IllegalStateException(); } this.eventQueueConsumptionTask = this.consumerExecutor.scheduleWithFixedDelay(this.createEventQueueConsumptionTask(siphon), 0L, 1L, TimeUnit.SECONDS); returnValue = this.eventQueueConsumptionTask; } else { returnValue = null; } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, returnValue); } return returnValue; } private 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); } final ExecutorService consumerExecutor; final Future task; synchronized (this) { this.closing = true; task = this.eventQueueConsumptionTask; consumerExecutor = this.consumerExecutor; } if (consumerExecutor != null) { // Stop accepting new tasks. consumerExecutor.shutdown(); if (task != null) { task.cancel(true); } // Cancel all tasks firmly. consumerExecutor.shutdownNow(); try { // Wait for termination to complete normally. if (!consumerExecutor.awaitTermination(60, TimeUnit.SECONDS) && this.logger.isLoggable(Level.WARNING)) { this.logger.logp(Level.WARNING, cn, mn, "consumerExecutor.awaitTermination() failed"); } } catch (final InterruptedException interruptedException) { consumerExecutor.shutdownNow(); Thread.currentThread().interrupt(); } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } private final Runnable createEventQueueConsumptionTask(final Consumer> siphon) { Objects.requireNonNull(siphon); final Runnable returnValue = () -> { while (!Thread.currentThread().isInterrupted()) { @Blocking final EventQueue eventQueue = this.get(); if (eventQueue != null) { synchronized (eventQueue) { try { siphon.accept(eventQueue); } catch (final TransientException transientException) { this.map.putIfAbsent(eventQueue.getKey(), eventQueue); } } } } }; return returnValue; } /** * Returns 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}. * *

This method may return {@code null} in which case the current * {@link Thread} has been {@linkplain Thread#interrupt() * interrupted}.

* * @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; } @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 } assert this.populated : "this.populated == false"; if (this.isEmpty()) { assert this.closing : "this.isEmpty() && !this.closing"; returnValue = null; } else { final Iterator> iterator = this.map.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. 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} */ 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; } 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 synchronize(final Object source, final AbstractEvent.Type eventType, final T resource, final boolean populate) { final String cn = this.getClass().getName(); final String mn = "synchronize"; 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); 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) { final boolean old = this.populated; this.populated = true; this.firePropertyChange("populated", old, true); } EventQueue eventQueue = this.map.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 old = this.isEmpty(); this.map.remove(key); this.firePropertyChange("empty", old, this.isEmpty()); } else { // Nothing to do; the queue we added the event to was // created here, and was never added to our internal map, so // we're done. } } else if (!eventQueueExisted) { // We created the EventQueue we just added to; now we need to // store it. final boolean old = this.isEmpty(); this.map.put(key, eventQueue); this.firePropertyChange("empty", old, 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); } } }