001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017-2018 microBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.kubernetes.controller;
018
019import java.util.Map;
020import java.util.Objects;
021
022import java.util.function.Consumer;
023
024import java.util.logging.Level;
025import java.util.logging.Logger;
026
027import io.fabric8.kubernetes.api.model.HasMetadata;
028
029import net.jcip.annotations.GuardedBy;
030
031/**
032 * A {@link Consumer} of {@link EventQueue}s that tracks the
033 * Kubernetes resources they contain before allowing subclasses to
034 * process their individual {@link Event}s.
035 *
036 * <p>Typically you would supply an implementation of this class to a
037 * {@link Controller}.</p>
038 *
039 * @param <T> a Kubernetes resource type
040 *
041 * @author <a href="https://about.me/lairdnelson"
042 * target="_parent">Laird Nelson</a>
043 *
044 * @see #accept(AbstractEvent)
045 *
046 * @see Controller
047 */
048public abstract class ResourceTrackingEventQueueConsumer<T extends HasMetadata> implements Consumer<EventQueue<? extends T>> {
049
050
051  /*
052   * Instance fields.
053   */
054
055  
056  /**
057   * A mutable {@link Map} of {@link HasMetadata} objects indexed by
058   * their keys (often a pairing of namespace and name).
059   *
060   * <p>This field may be {@code null} in which case no resource
061   * tracking will take place.</p>
062   *
063   * <p>The value of this field is {@linkplain
064   * #ResourceTrackingEventQueueConsumer(Map) supplied at construction
065   * time} and is <strong>synchronized on</strong> and written to, if
066   * non-{@code null}, by the {@link #accept(EventQueue)} method.</p>
067   *
068   * <p>This class <strong>synchronizes on this field's
069   * value</strong>, if it is non-{@code null}, when mutating its
070   * contents.</p>
071   */
072  @GuardedBy("itself")
073  private final Map<Object, T> knownObjects;
074
075  /**
076   * A {@link Logger} for use by this {@link
077   * ResourceTrackingEventQueueConsumer} implementation.
078   *
079   * <p>This field is never {@code null}.</p>
080   *
081   * @see #createLogger()
082   */
083  protected final Logger logger;
084
085  
086  /*
087   * Constructors.
088   */
089
090
091  /**
092   * Creates a new {@link ResourceTrackingEventQueueConsumer}.
093   *
094   * @param knownObjects a mutable {@link Map} of {@link HasMetadata}
095   * objects indexed by their keys (often a pairing of namespace and
096   * name); must not be {@code null}; <strong>will have its contents
097   * changed</strong> by this {@link
098   * ResourceTrackingEventQueueConsumer}'s {@link #accept(EventQueue)}
099   * method; <strong>will be synchronized on</strong> by this {@link
100   * ResourceTrackingEventQueueConsumer}'s {@link #accept(EventQueue)}
101   * method
102   *
103   * @see #accept(EventQueue)
104   */
105  protected ResourceTrackingEventQueueConsumer(final Map<Object, T> knownObjects) {
106    super();
107    this.logger = this.createLogger();
108    if (this.logger == null) {
109      throw new IllegalStateException("createLogger() == null");
110    }
111    final String cn = this.getClass().getName();
112    final String mn = "<init>";
113    if (this.logger.isLoggable(Level.FINER)) {
114      final String knownObjectsString;
115      if (knownObjects == null) {
116        knownObjectsString = null;
117      } else {
118        synchronized (knownObjects) {
119          knownObjectsString = knownObjects.toString();
120        }
121      }      
122      this.logger.entering(cn, mn, knownObjectsString);
123    }
124    this.knownObjects = knownObjects;
125    if (this.logger.isLoggable(Level.FINER)) {
126      this.logger.exiting(cn, mn);
127    }
128  }
129
130
131  /*
132   * Instance methods.
133   */
134
135
136  /**
137   * Returns a {@link Logger} for use with this {@link
138   * ResourceTrackingEventQueueConsumer}.
139   *
140   * <p>This method never returns {@code null}.</p>
141   *
142   * <p>Overrides of this method must not return {@code null}.</p>
143   *
144   * @return a non-{@code null} {@link Logger}
145   */
146  protected Logger createLogger() {
147    return Logger.getLogger(this.getClass().getName());
148  }
149
150  
151  /**
152   * {@linkplain EventQueue#iterator() Loops through} all the {@link
153   * AbstractEvent}s in the supplied {@link EventQueue}, keeping track
154   * of the {@link HasMetadata} it concerns along the way by
155   * <strong>synchronizing on</strong> and writing to the {@link Map}
156   * {@linkplain #ResourceTrackingEventQueueConsumer(Map) supplied at
157   * construction time}.
158   *
159   * <p>Individual {@link AbstractEvent}s are forwarded on to the
160   * {@link #accept(AbstractEvent)} method.</p>
161   *
162   * <h2>Implementation Notes</h2>
163   *
164   * <p>This loosely models the <a
165   * href="https://github.com/kubernetes/client-go/blob/v6.0.0/tools/cache/shared_informer.go#L343">{@code
166   * HandleDeltas} function in {@code
167   * tools/cache/shared_informer.go}</a>.  The final distribution step
168   * is left unimplemented on purpose.</p>
169   *
170   * @param eventQueue the {@link EventQueue} to process; may be
171   * {@code null} in which case no action will be taken
172   *
173   * @see #accept(AbstractEvent)
174   */
175  @Override
176  public final void accept(final EventQueue<? extends T> eventQueue) {
177    final String cn = this.getClass().getName();
178    final String mn = "accept";
179    if (eventQueue == null) {
180      if (this.logger.isLoggable(Level.FINER)) {
181        this.logger.entering(cn, mn, null);
182      }
183    } else {
184      synchronized (eventQueue) {
185        if (this.logger.isLoggable(Level.FINER)) {
186          this.logger.entering(cn, mn, eventQueue);
187        }
188        
189        final Object key = eventQueue.getKey();
190        if (key == null) {
191          throw new IllegalStateException("eventQueue.getKey() == null; eventQueue: " + eventQueue);
192        }        
193
194        for (final AbstractEvent<? extends T> event : eventQueue) {
195          if (event != null) {
196            assert key.equals(event.getKey());            
197            final Event.Type eventType = event.getType();
198            assert eventType != null;            
199            final T newResource = event.getResource();
200            if (event.getPriorResource() != null && this.logger.isLoggable(Level.FINE)) {
201              this.logger.logp(Level.FINE, cn, mn, "Unexpected state; event has a priorResource: {0}", event.getPriorResource());
202            }
203            final T priorResource;
204            final AbstractEvent<? extends T> newEvent;
205            if (this.knownObjects == null) {
206              priorResource = null;
207              newEvent = event;
208            } else {
209              synchronized (this.knownObjects) {
210                if (Event.Type.DELETION.equals(eventType)) {
211                  priorResource = this.knownObjects.remove(key);
212                  newEvent = event;
213                } else {
214                  assert eventType.equals(Event.Type.ADDITION) || eventType.equals(Event.Type.MODIFICATION);
215                  priorResource = this.knownObjects.put(key, newResource);
216                  if (event instanceof SynchronizationEvent) {
217                    if (priorResource == null) {
218                      assert Event.Type.ADDITION.equals(eventType) : "!Event.Type.ADDITION.equals(eventType): " + eventType;
219                      newEvent = event;
220                    } else {
221                      assert Event.Type.MODIFICATION.equals(eventType) : "!Event.Type.MODIFICATION.equals(eventType): " + eventType;
222                      newEvent = this.createSynchronizationEvent(Event.Type.MODIFICATION, priorResource, newResource);
223                    }
224                  } else if (priorResource == null) {
225                    if (Event.Type.ADDITION.equals(eventType)) {
226                      newEvent = event;
227                    } else {
228                      newEvent = this.createEvent(Event.Type.ADDITION, null, newResource);
229                    }
230                  } else {
231                    newEvent = this.createEvent(Event.Type.MODIFICATION, priorResource, newResource);
232                  }
233                }
234              }
235            }
236            assert newEvent != null;
237            assert newEvent instanceof SynchronizationEvent || newEvent instanceof Event;
238            this.accept(newEvent);
239          }
240        }
241        
242      }
243    }
244    if (this.logger.isLoggable(Level.FINER)) {
245      this.logger.exiting(cn, mn);
246    }
247  }
248
249  /**
250   * Creates and returns a new {@link Event}.
251   *
252   * <p>This method never returns {@code null}.</p>
253   *
254   * <p>Overrides of this method must not return {@code null}.</p>
255   *
256   * @param eventType the {@link AbstractEvent.Type} for the new
257   * {@link Event}; must not be {@code null}; when supplied by the
258   * {@link #accept(EventQueue)} method's internals, will always be
259   * either {@link AbstractEvent.Type#ADDITION} or {@link
260   * AbstractEvent.Type#MODIFICATION}
261   *
262   * @param priorResource the prior state of the resource the new
263   * {@link Event} will represent; may be (and often is) {@code null}
264   *
265   * @param resource the latest state of the resource the new {@link
266   * Event} will represent; must not be {@code null}
267   *
268   * @return a new, non-{@code null} {@link Event} with each
269   * invocation
270   *
271   * @exception NullPointerException if {@code eventType} or {@code
272   * resource} is {@code null}
273   */
274  protected Event<T> createEvent(final Event.Type eventType, final T priorResource, final T resource) {
275    final String cn = this.getClass().getName();
276    final String mn = "createEvent";
277    if (this.logger.isLoggable(Level.FINER)) {
278      this.logger.entering(cn, mn, new Object[] { eventType, priorResource, resource });
279    }
280    Objects.requireNonNull(eventType);
281    final Event<T> returnValue = new Event<>(this, eventType, priorResource, resource);
282    if (this.logger.isLoggable(Level.FINER)) {
283      this.logger.exiting(cn, mn, returnValue);
284    }
285    return returnValue;
286  }
287
288  /**
289   * Creates and returns a new {@link SynchronizationEvent}.
290   *
291   * <p>This method never returns {@code null}.</p>
292   *
293   * <p>Overrides of this method must not return {@code null}.</p>
294   *
295   * @param eventType the {@link AbstractEvent.Type} for the new
296   * {@link SynchronizationEvent}; must not be {@code null}; when
297   * supplied by the {@link #accept(EventQueue)} method's internals,
298   * will always be {@link AbstractEvent.Type#MODIFICATION}
299   *
300   * @param priorResource the prior state of the resource the new
301   * {@link SynchronizationEvent} will represent; may be (and often
302   * is) {@code null}
303   *
304   * @param resource the latest state of the resource the new {@link
305   * SynchronizationEvent} will represent; must not be {@code null}
306   *
307   * @return a new, non-{@code null} {@link SynchronizationEvent} with
308   * each invocation
309   *
310   * @exception NullPointerException if {@code eventType} or {@code
311   * resource} is {@code null}
312   */
313  protected SynchronizationEvent<T> createSynchronizationEvent(final Event.Type eventType, final T priorResource, final T resource) {
314    final String cn = this.getClass().getName();
315    final String mn = "createSynchronizationEvent";
316    if (this.logger.isLoggable(Level.FINER)) {
317      this.logger.entering(cn, mn, new Object[] { eventType, priorResource, resource });
318    }
319    Objects.requireNonNull(eventType);
320    final SynchronizationEvent<T> returnValue = new SynchronizationEvent<>(this, eventType, priorResource, resource);
321    if (this.logger.isLoggable(Level.FINER)) {
322      this.logger.exiting(cn, mn, returnValue);
323    }
324    return returnValue;
325  }
326
327  /**
328   * Called to process a given {@link AbstractEvent} from the {@link
329   * EventQueue} supplied to the {@link #accept(EventQueue)} method,
330   * <strong>with that {@link EventQueue}'s monitor held</strong>.
331   *
332   * <p>Implementations of this method should be relatively fast as
333   * this method dictates the speed of {@link EventQueue}
334   * processing.</p>
335   *
336   * @param event the {@link AbstractEvent} encountered in the {@link
337   * EventQueue}; must not be {@code null}
338   *
339   * @exception NullPointerException if {@code event} is {@code null}
340   *
341   * @see #accept(EventQueue)
342   */
343  protected abstract void accept(final AbstractEvent<? extends T> event);
344  
345}