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

org.microbean.kubernetes.controller.Reflector 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.io.Closeable;
import java.io.IOException;

import java.time.Duration;

import java.time.temporal.ChronoUnit;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.Map;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

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

import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watcher;

import io.fabric8.kubernetes.client.dsl.Listable;
import io.fabric8.kubernetes.client.dsl.VersionWatchable;

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

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

import org.microbean.development.annotation.NonBlocking;

/**
 * A pump of sorts that continuously "pulls" logical events out of
 * Kubernetes and {@linkplain EventCache#add(Object, AbstractEvent.Type,
 * HasMetadata) adds them} to an {@link EventCache} so as to logically
 * "reflect" the contents of Kubernetes into the cache.
 *
 * 

Thread Safety

* *

Instances of this class are safe for concurrent use by multiple * {@link Thread}s.

* * @param a type of Kubernetes resource * * @author Laird Nelson * * @see EventCache */ @ThreadSafe public class Reflector implements Closeable { /* * Instance fields. */ /** * The operation that was supplied at construction time. * *

This field is never {@code null}.

* *

It is guaranteed that the value of this field may be * assignable to a reference of type {@link Listable Listable<? * extends KubernetesResourceList>} or to a reference of type * {@link VersionWatchable VersionWatchable<? extends Closeable, * Watcher<T>>}.

* * @see Listable * * @see VersionWatchable */ private final Object operation; /** * The resource version */ private volatile Object lastSynchronizationResourceVersion; private final ScheduledExecutorService synchronizationExecutorService; @GuardedBy("this") private ScheduledFuture synchronizationTask; private final boolean shutdownSynchronizationExecutorServiceOnClose; private final long synchronizationIntervalInSeconds; @GuardedBy("this") private Closeable watch; @GuardedBy("itself") private final EventCache eventCache; /** * A {@link Logger} for use by this {@link Reflector}. * *

This field is never {@code null}.

* * @see #createLogger() */ protected final Logger logger; /* * Constructors. */ /** * Creates a new {@link Reflector}. * * @param a type that is both an appropriate kind of {@link * Listable} and {@link VersionWatchable}, such as the kind of * operation returned by {@link * DefaultKubernetesClient#configMaps()} and the like * * @param operation a {@link Listable} and a {@link * VersionWatchable} that can report information from a Kubernetes * cluster; must not be {@code null} * * @param eventCache an {@link EventCache} that will be * synchronized on and into which {@link Event}s will be * logically "reflected"; must not be {@code null} * * @exception NullPointerException if {@code operation} or {@code * eventCache} is {@code null} * * @exception IllegalStateException if the {@link #createLogger()} * method returns {@code null} * * @see #Reflector(Listable, EventCache, ScheduledExecutorService, * Duration) * * @see #start() */ @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types public & VersionWatchable>> Reflector(final X operation, final EventCache eventCache) { this(operation, eventCache, null, null); } /** * Creates a new {@link Reflector}. * * @param a type that is both an appropriate kind of {@link * Listable} and {@link VersionWatchable}, such as the kind of * operation returned by {@link * DefaultKubernetesClient#configMaps()} and the like * * @param operation a {@link Listable} and a {@link * VersionWatchable} that can report information from a Kubernetes * cluster; must not be {@code null} * * @param eventCache an {@link EventCache} that will be * synchronized on and into which {@link Event}s will be * logically "reflected"; must not be {@code null} * * @param synchronizationInterval a {@link Duration} representing * the time in between one {@linkplain EventCache#synchronize() * synchronization operation} and another; interpreted with a * granularity of seconds; may be {@code null} or semantically equal * to {@code 0} seconds in which case no synchronization will occur * * @exception NullPointerException if {@code operation} or {@code * eventCache} is {@code null} * * @exception IllegalStateException if the {@link #createLogger()} * method returns {@code null} * * @see #Reflector(Listable, EventCache, ScheduledExecutorService, * Duration) * * @see #start() */ @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types public & VersionWatchable>> Reflector(final X operation, final EventCache eventCache, final Duration synchronizationInterval) { this(operation, eventCache, null, synchronizationInterval); } /** * Creates a new {@link Reflector}. * * @param a type that is both an appropriate kind of {@link * Listable} and {@link VersionWatchable}, such as the kind of * operation returned by {@link * DefaultKubernetesClient#configMaps()} and the like * * @param operation a {@link Listable} and a {@link * VersionWatchable} that can report information from a Kubernetes * cluster; must not be {@code null} * * @param eventCache an {@link EventCache} that will be * synchronized on and into which {@link Event}s will be * logically "reflected"; must not be {@code null} * * @param synchronizationExecutorService a {@link * ScheduledExecutorService} to be used to tell the supplied {@link * EventCache} to {@linkplain EventCache#synchronize() synchronize} * on a schedule; may be {@code null} in which case no * synchronization will occur * * @param synchronizationInterval a {@link Duration} representing * the time in between one {@linkplain EventCache#synchronize() * synchronization operation} and another; may be {@code null} in * which case no synchronization will occur * * @exception NullPointerException if {@code operation} or {@code * eventCache} is {@code null} * * @exception IllegalStateException if the {@link #createLogger()} * method returns {@code null} * * @see #start() */ @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types public & VersionWatchable>> Reflector(final X operation, final EventCache eventCache, final ScheduledExecutorService synchronizationExecutorService, final Duration synchronizationInterval) { super(); this.logger = this.createLogger(); if (this.logger == null) { throw new IllegalStateException("createLogger() == null"); } final String cn = this.getClass().getName(); final String mn = ""; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn, new Object[] { operation, eventCache, synchronizationExecutorService, synchronizationInterval }); } Objects.requireNonNull(operation); this.eventCache = Objects.requireNonNull(eventCache); // TODO: research: maybe: operation.withField("metadata.resourceVersion", "0")? this.operation = operation.withResourceVersion("0"); if (synchronizationInterval == null) { this.synchronizationIntervalInSeconds = 0L; } else { this.synchronizationIntervalInSeconds = synchronizationInterval.get(ChronoUnit.SECONDS); } if (this.synchronizationIntervalInSeconds <= 0L) { this.synchronizationExecutorService = null; this.shutdownSynchronizationExecutorServiceOnClose = false; } else if (synchronizationExecutorService == null) { this.synchronizationExecutorService = Executors.newScheduledThreadPool(1); this.shutdownSynchronizationExecutorServiceOnClose = true; } else { this.synchronizationExecutorService = synchronizationExecutorService; this.shutdownSynchronizationExecutorServiceOnClose = false; } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /* * Instance methods. */ /** * Returns a {@link Logger} that will be used for this {@link * Reflector}. * *

This method never returns {@code null}.

* *

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

* * @return a non-{@code null} {@link Logger} */ protected Logger createLogger() { return Logger.getLogger(this.getClass().getName()); } /** * Notionally closes this {@link Reflector} by terminating any * {@link Thread}s that it has started and invoking the {@link * #onClose()} method while holding this {@link Reflector}'s * monitor. * * @exception IOException if an error occurs * * @see #onClose() */ @Override public synchronized final void close() throws IOException { final String cn = this.getClass().getName(); final String mn = "close"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } try { this.closeSynchronizationExecutorService(); if (this.watch != null) { this.watch.close(); } } finally { this.onClose(); } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } private synchronized final void closeSynchronizationExecutorService() { final String cn = this.getClass().getName(); final String mn = "closeSynchronizationExecutorService"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } if (this.synchronizationExecutorService != null && this.shutdownSynchronizationExecutorServiceOnClose) { // Stop accepting new tasks. Not that any will be showing up // anyway, but it's the right thing to do. this.synchronizationExecutorService.shutdown(); // Nicely cancel our task. final ScheduledFuture synchronizationTask = this.synchronizationTask; if (synchronizationTask != null) { synchronizationTask.cancel(true /* interrupt the task */); } try { if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { this.synchronizationExecutorService.shutdownNow(); if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { // TODO: log } } } catch (final InterruptedException interruptedException) { this.synchronizationExecutorService.shutdownNow(); Thread.currentThread().interrupt(); } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } private synchronized final void setUpSynchronization() { final String cn = this.getClass().getName(); final String mn = "setUpSynchronization"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } if (this.synchronizationExecutorService != null && this.synchronizationTask == null) { assert this.synchronizationIntervalInSeconds > 0L; if (this.logger.isLoggable(Level.INFO)) { this.logger.logp(Level.INFO, cn, mn, "Scheduling downstream synchronization every {0} seconds", Long.valueOf(this.synchronizationIntervalInSeconds)); } final ScheduledFuture job = this.synchronizationExecutorService.scheduleWithFixedDelay(() -> { if (shouldSynchronize()) { if (logger.isLoggable(Level.FINE)) { logger.logp(Level.FINE, cn, mn, "Synchronizing event cache with its downstream consumers"); } synchronized (eventCache) { eventCache.synchronize(); } } }, 0L, this.synchronizationIntervalInSeconds, TimeUnit.SECONDS); assert job != null; this.synchronizationTask = job; } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * Returns whether, at any given moment, this {@link Reflector} * should cause its {@link EventCache} to {@linkplain * EventCache#synchronize() synchronize}. * *

Design Notes

* *

This code follows the Go code in the Kubernetes {@code * client-go/tools/cache} package. One thing that becomes clear * when looking at all of this through an object-oriented lens is * that it is the {@link EventCache} (the {@code delta_fifo}, in the * Go code) that is ultimately in charge of synchronizing. It is * not clear why this is a function of a reflector. In an * object-oriented world, perhaps the {@link EventCache} itself * should be in charge of resynchronization schedules.

* * @return {@code true} if this {@link Reflector} should cause its * {@link EventCache} to {@linkplain EventCache#synchronize() * synchronize}; {@code false} otherwise */ protected boolean shouldSynchronize() { final String cn = this.getClass().getName(); final String mn = "shouldSynchronize"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } final boolean returnValue = this.synchronizationExecutorService != null; if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); } return returnValue; } private final Object getLastSynchronizationResourceVersion() { return this.lastSynchronizationResourceVersion; } private final void setLastSynchronizationResourceVersion(final Object resourceVersion) { this.lastSynchronizationResourceVersion = resourceVersion; } /** * Using the {@code operation} supplied at construction time, * {@linkplain Listable#list() lists} appropriate Kubernetes * resources, and then, on a separate {@link Thread}, {@linkplain * VersionWatchable sets up a watch} on them, calling {@link * EventCache#replace(Collection, Object)} and {@link * EventCache#add(Object, AbstractEvent.Type, HasMetadata)} methods * as appropriate. * *

The calling {@link Thread} is not blocked by invocations of * this method.

* * @see #close() */ @NonBlocking public final void start() { final String cn = this.getClass().getName(); final String mn = "start"; if (this.logger.isLoggable(Level.FINER)) { this.logger.entering(cn, mn); } synchronized (this) { if (this.watch == null) { // Run a list operation, and get the resourceVersion of that list. @SuppressWarnings("unchecked") final KubernetesResourceList list = ((Listable>)this.operation).list(); assert list != null; final ListMeta metadata = list.getMetadata(); assert metadata != null; final String resourceVersion = metadata.getResourceVersion(); assert resourceVersion != null; // Using the results of that list operation, do a full replace // on the EventCache with them. final Collection replacementItems; final Collection items = list.getItems(); if (items == null || items.isEmpty()) { replacementItems = Collections.emptySet(); } else { replacementItems = Collections.unmodifiableCollection(new ArrayList<>(items)); } synchronized (eventCache) { this.eventCache.replace(replacementItems, resourceVersion); } // Record the resource version we captured during our list // operation. this.setLastSynchronizationResourceVersion(resourceVersion); // Now that we've vetted that our list operation works (i.e. no // syntax errors, no connectivity problems) we can schedule // resynchronizations if necessary. this.setUpSynchronization(); // Now that we've taken care of our list() operation, set up our // watch() operation. @SuppressWarnings("unchecked") final Closeable temp = ((VersionWatchable>)operation).withResourceVersion(resourceVersion).watch(new WatchHandler()); assert temp != null; this.watch = temp; } } if (this.logger.isLoggable(Level.FINER)) { this.logger.exiting(cn, mn); } } /** * Invoked when {@link #close()} is invoked. * *

The default implementation of this method does nothing.

*/ protected synchronized void onClose() { } /* * Inner and nested classes. */ /** * A {@link Watcher} of Kubernetes resources. * * @author Laird Nelson * * @see Watcher */ private final class WatchHandler implements Watcher { /* * Constructors. */ /** * Creates a new {@link WatchHandler}. */ private WatchHandler() { super(); final String cn = this.getClass().getName(); final String mn = ""; if (logger.isLoggable(Level.FINER)) { logger.entering(cn, mn); logger.exiting(cn, mn); } } /* * Instance methods. */ /** * Calls the {@link EventCache#add(Object, AbstractEvent.Type, * HasMetadata)} method on the enclosing {@link Reflector}'s * associated {@link EventCache} with information harvested from * the supplied {@code resource}, and using an {@link Event.Type} * selected appropriately given the supplied {@link * Watcher.Action}. * * @param action the kind of Kubernetes event that happened; must * not be {@code null} * * @param resource the {@link HasMetadata} object that was * affected; must not be {@code null} * * @exception NullPointerException if {@code action} or {@code * resource} was {@code null} * * @exception IllegalStateException if another error occurred */ @Override public final void eventReceived(final Watcher.Action action, final T resource) { final String cn = this.getClass().getName(); final String mn = "eventReceived"; if (logger.isLoggable(Level.FINER)) { logger.entering(cn, mn, new Object[] { action, resource }); } Objects.requireNonNull(action); Objects.requireNonNull(resource); final ObjectMeta metadata = resource.getMetadata(); assert metadata != null; final Event.Type eventType; switch (action) { case ADDED: eventType = Event.Type.ADDITION; break; case MODIFIED: eventType = Event.Type.MODIFICATION; break; case DELETED: eventType = Event.Type.DELETION; break; case ERROR: // Uh...the Go code has: // // if event.Type == watch.Error { // return apierrs.FromObject(event.Object) // } // // Now, apierrs.FromObject is here: // https://github.com/kubernetes/apimachinery/blob/kubernetes-1.9.2/pkg/api/errors/errors.go#L80-L88 // This is looking for a Status object. But // WatchConnectionHandler will never forward on such a thing: // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L246-L258 // // So it follows that if by some chance we get here, resource // will definitely be a HasMetadata. We go back to the Go // code again, and remember that if the type is Error, the // equivalent of this watch handler simply returns and goes home. // // Now, if we were to throw a RuntimeException here, which is // the idiomatic equivalent of returning and going home, this // would cause a watch reconnect: // https://github.com/fabric8io/kubernetes-client/blob/v3.1.8/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/WatchConnectionManager.java#L159-L205 // ...up to the reconnect limit. // // ...which is fine, but I'm not sure that in an error case a // WatchEvent will ever HAVE a HasMetadata as its payload. // Which means MAYBE we'll never get here. But if we do, all // we can do is throw a RuntimeException...which ends up // reducing to the same case as the default case below, so we // fall through. default: eventType = null; throw new IllegalStateException(); } // Add an Event of the proper kind to our EventCache. if (eventType != null) { if (logger.isLoggable(Level.FINE)) { logger.logp(Level.FINE, cn, mn, "Adding event to cache: {0} {1}", new Object[] { eventType, resource }); } synchronized (eventCache) { eventCache.add(Reflector.this, eventType, resource); } } // Record the most recent resource version we're tracking to be // that of this last successful watch() operation. We set it // earlier during a list() operation. setLastSynchronizationResourceVersion(metadata.getResourceVersion()); if (logger.isLoggable(Level.FINER)) { logger.exiting(cn, mn); } } /** * Invoked when the Kubernetes client connection closes. * * @param exception any {@link KubernetesClientException} that * caused this closing to happen; may be {@code null} */ @Override public final void onClose(final KubernetesClientException exception) { final String cn = this.getClass().getName(); final String mn = "onClose"; if (logger.isLoggable(Level.FINER)) { logger.entering(cn, mn, exception); } if (exception != null && logger.isLoggable(Level.WARNING)) { logger.logp(Level.WARNING, cn, mn, exception.getMessage(), exception); } if (logger.isLoggable(Level.FINER)) { logger.exiting(cn, mn, exception); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy