001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.kubernetes.controller; 018 019import java.io.IOException; 020 021import java.time.Duration; 022import java.time.Instant; 023 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Iterator; 027import java.util.Map; 028import java.util.Objects; 029 030import java.util.concurrent.BlockingQueue; 031import java.util.concurrent.Executor; 032import java.util.concurrent.Executors; 033import java.util.concurrent.ExecutorService; 034import java.util.concurrent.Future; 035import java.util.concurrent.CopyOnWriteArrayList; 036import java.util.concurrent.LinkedBlockingQueue; 037import java.util.concurrent.ScheduledExecutorService; 038import java.util.concurrent.ScheduledThreadPoolExecutor; 039import java.util.concurrent.TimeUnit; 040 041import java.util.concurrent.locks.Lock; 042import java.util.concurrent.locks.ReadWriteLock; 043import java.util.concurrent.locks.ReentrantReadWriteLock; 044 045import java.util.function.Consumer; 046 047import io.fabric8.kubernetes.api.model.HasMetadata; 048 049import net.jcip.annotations.Immutable; 050import net.jcip.annotations.GuardedBy; 051import net.jcip.annotations.ThreadSafe; 052 053/** 054 * A {@link ResourceTrackingEventQueueConsumer} that {@linkplain 055 * ResourceTrackingEventQueueConsumer#accept(EventQueue) consumes 056 * <tt>EventQueue</tt> instances} by feeding each {@link 057 * AbstractEvent} in the {@link EventQueue} being consumed to {@link 058 * Consumer}s of {@link AbstractEvent}s that have been {@linkplain 059 * #addConsumer(Consumer) registered}. 060 * 061 * <p>{@link EventDistributor} instances must be {@linkplain #close() 062 * closed} and discarded after use.</p> 063 * 064 * @param <T> a type of Kubernetes resource 065 * 066 * @author <a href="https://about.me/lairdnelson" 067 * target="_parent">Laird Nelson</a> 068 * 069 * @see #addConsumer(Consumer) 070 * 071 * @see #removeConsumer(Consumer) 072 */ 073@Immutable 074@ThreadSafe 075public final class EventDistributor<T extends HasMetadata> extends ResourceTrackingEventQueueConsumer<T> implements AutoCloseable { 076 077 078 /* 079 * Instance fields. 080 */ 081 082 083 @GuardedBy("readLock && writeLock") 084 private final Collection<Pump<T>> distributors; 085 086 @GuardedBy("readLock && writeLock") 087 private final Collection<Pump<T>> synchronizingDistributors; 088 089 private final Duration synchronizationInterval; 090 091 private final Lock readLock; 092 093 private final Lock writeLock; 094 095 096 /* 097 * Constructors. 098 */ 099 100 101 /** 102 * Creates a new {@link EventDistributor}. 103 * 104 * @param knownObjects a mutable {@link Map} of Kubernetes resources 105 * that contains or will contain Kubernetes resources known to this 106 * {@link EventDistributor} and whatever mechanism (such as a {@link 107 * Controller}) is feeding it; may be {@code null} 108 * 109 * @see #EventDistributor(Map, Duration) 110 */ 111 public EventDistributor(final Map<Object, T> knownObjects) { 112 this(knownObjects, null); 113 } 114 115 /** 116 * Creates a new {@link EventDistributor}. 117 * 118 * @param knownObjects a mutable {@link Map} of Kubernetes resources 119 * that contains or will contain Kubernetes resources known to this 120 * {@link EventDistributor} and whatever mechanism (such as a {@link 121 * Controller}) is feeding it; may be {@code null} 122 * 123 * @param synchronizationInterval a {@link Duration} representing 124 * the interval after which an attempt to synchronize might happen; 125 * may be {@code null} in which case no synchronization will occur 126 * 127 * @see 128 * ResourceTrackingEventQueueConsumer#ResourceTrackingEventQueueConsumer(Map) 129 */ 130 public EventDistributor(final Map<Object, T> knownObjects, final Duration synchronizationInterval) { 131 super(knownObjects); 132 final ReadWriteLock lock = new ReentrantReadWriteLock(); 133 this.readLock = lock.readLock(); 134 this.writeLock = lock.writeLock(); 135 this.distributors = new ArrayList<>(); 136 this.synchronizingDistributors = new ArrayList<>(); 137 this.synchronizationInterval = synchronizationInterval; 138 } 139 140 141 /* 142 * Instance methods. 143 */ 144 145 146 /** 147 * Adds the supplied {@link Consumer} to this {@link 148 * EventDistributor} as a listener that will be notified of each 149 * {@link AbstractEvent} this {@link EventDistributor} receives. 150 * 151 * <p>The supplied {@link Consumer}'s {@link 152 * Consumer#accept(Object)} method may be called later on a separate 153 * thread of execution.</p> 154 * 155 * @param consumer a {@link Consumer} of {@link AbstractEvent}s; may 156 * be {@code null} in which case no action will be taken 157 * 158 * @see #removeConsumer(Consumer) 159 */ 160 public final void addConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) { 161 if (consumer != null) { 162 this.writeLock.lock(); 163 try { 164 final Pump<T> distributor = new Pump<>(this.synchronizationInterval, consumer); 165 this.distributors.add(distributor); 166 this.synchronizingDistributors.add(distributor); 167 } finally { 168 this.writeLock.unlock(); 169 } 170 } 171 } 172 173 /** 174 * Removes any {@link Consumer} {@linkplain Object#equals(Object) 175 * equal to} a {@link Consumer} previously {@linkplain 176 * #addConsumer(Consumer) added} to this {@link EventDistributor}. 177 * 178 * @param consumer the {@link Consumer} to remove; may be {@code 179 * null} in which case no action will be taken 180 * 181 * @see #addConsumer(Consumer) 182 */ 183 public final void removeConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) { 184 if (consumer != null) { 185 this.writeLock.lock(); 186 try { 187 final Iterator<? extends Pump<?>> iterator = this.distributors.iterator(); 188 assert iterator != null; 189 while (iterator.hasNext()) { 190 final Pump<?> distributor = iterator.next(); 191 if (distributor != null && consumer.equals(distributor.getEventConsumer())) { 192 iterator.remove(); 193 break; 194 } 195 } 196 } finally { 197 this.writeLock.unlock(); 198 } 199 } 200 } 201 202 /** 203 * Releases resources held by this {@link EventDistributor} during 204 * its execution. 205 */ 206 @Override 207 public final void close() { 208 this.writeLock.lock(); 209 try { 210 this.distributors.parallelStream() 211 .forEach(distributor -> { 212 distributor.close(); 213 }); 214 this.synchronizingDistributors.clear(); 215 this.distributors.clear(); 216 } finally { 217 this.writeLock.unlock(); 218 } 219 } 220 221 /** 222 * Returns {@code true} if this {@link EventDistributor} should 223 * <em>synchronize</em> with its upstream source. 224 * 225 * <h2>Design Notes</h2> 226 * 227 * <p>The Kubernetes {@code tools/cache} package spreads 228 * synchronization out among the reflector, controller, event cache 229 * and event processor constructs for no seemingly good reason. 230 * They should probably be consolidated, particularly in an 231 * object-oriented environment such as Java.</p> 232 * 233 * @return {@code true} if synchronization should occur; {@code 234 * false} otherwise 235 * 236 * @see EventCache#synchronize() 237 */ 238 public final boolean shouldSynchronize() { 239 boolean returnValue = false; 240 this.writeLock.lock(); 241 try { 242 this.synchronizingDistributors.clear(); 243 final Instant now = Instant.now(); 244 this.distributors.parallelStream() 245 .filter(distributor -> distributor.shouldSynchronize(now)) 246 .forEach(distributor -> { 247 this.synchronizingDistributors.add(distributor); 248 distributor.determineNextSynchronizationInterval(now); 249 }); 250 returnValue = !this.synchronizingDistributors.isEmpty(); 251 } finally { 252 this.writeLock.unlock(); 253 } 254 return returnValue; 255 } 256 257 /** 258 * Consumes the supplied {@link AbstractEvent} by forwarding it to 259 * the {@link Consumer#accept(Object)} method of each {@link 260 * Consumer} {@linkplain #addConsumer(Consumer) registered} with 261 * this {@link EventDistributor}. 262 * 263 * @param event the {@link AbstractEvent} to forward; may be {@code 264 * null} in which case no action is taken 265 * 266 * @see #addConsumer(Consumer) 267 */ 268 @Override 269 protected final void accept(final AbstractEvent<? extends T> event) { 270 if (event != null) { 271 if (event instanceof SynchronizationEvent) { 272 this.accept((SynchronizationEvent<? extends T>)event); 273 } else if (event instanceof Event) { 274 this.accept((Event<? extends T>)event); 275 } else { 276 assert false : "Unexpected event type: " + event.getClass(); 277 } 278 } 279 } 280 281 private final void accept(final SynchronizationEvent<? extends T> event) { 282 this.readLock.lock(); 283 try { 284 if (!this.synchronizingDistributors.isEmpty()) { 285 this.synchronizingDistributors.parallelStream() 286 .forEach(distributor -> distributor.accept(event)); 287 } 288 } finally { 289 this.readLock.unlock(); 290 } 291 } 292 293 private final void accept(final Event<? extends T> event) { 294 this.readLock.lock(); 295 try { 296 if (!this.distributors.isEmpty()) { 297 this.distributors.parallelStream() 298 .forEach(distributor -> distributor.accept(event)); 299 } 300 } finally { 301 this.readLock.unlock(); 302 } 303 } 304 305 306 /* 307 * Inner and nested classes. 308 */ 309 310 311 /** 312 * A {@link Consumer} of {@link AbstractEvent} instances that puts 313 * them on an internal queue and, in a separate thread, removes them 314 * from the queue and forwards them to the "real" {@link Consumer} 315 * supplied at construction time. 316 * 317 * @author <a href="https://about.me/lairdnelson" 318 * target="_parent">Laird Nelson</a> 319 */ 320 private static final class Pump<T extends HasMetadata> implements Consumer<AbstractEvent<? extends T>>, AutoCloseable { 321 322 private volatile boolean closing; 323 324 private volatile Instant nextSynchronizationInstant; 325 326 private volatile Duration synchronizationInterval; 327 328 final BlockingQueue<AbstractEvent<? extends T>> queue; 329 330 private final ScheduledExecutorService executor; 331 332 private final Future<?> task; 333 334 private final Consumer<? super AbstractEvent<? extends T>> eventConsumer; 335 336 private Pump(final Duration synchronizationInterval, final Consumer<? super AbstractEvent<? extends T>> eventConsumer) { 337 super(); 338 Objects.requireNonNull(eventConsumer); 339 this.eventConsumer = eventConsumer; 340 this.executor = this.createScheduledThreadPoolExecutor(); 341 if (this.executor == null) { 342 throw new IllegalStateException("createScheduledThreadPoolExecutor() == null"); 343 } 344 this.queue = new LinkedBlockingQueue<>(); 345 346 // Schedule a hopefully never-ending task to pump events from 347 // our queue to the supplied eventConsumer. We schedule this 348 // instead of simply executing it so that if for any reason it 349 // exits it will get restarted. Cancelling a scheduled task 350 // will also cancel all resubmissions of it, so this is the most 351 // robust thing to do. The delay of one second is arbitrary. 352 this.task = this.executor.scheduleWithFixedDelay(() -> { 353 try { 354 while (!Thread.currentThread().isInterrupted()) { 355 this.eventConsumer.accept(this.queue.take()); 356 } 357 } catch (final InterruptedException interruptedException) { 358 Thread.currentThread().interrupt(); 359 } 360 }, 0L, 1L, TimeUnit.SECONDS); 361 362 this.setSynchronizationInterval(synchronizationInterval); 363 } 364 365 private final ScheduledExecutorService createScheduledThreadPoolExecutor() { 366 final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); 367 executor.setRemoveOnCancelPolicy(true); 368 return executor; 369 } 370 371 private final Consumer<? super AbstractEvent<? extends T>> getEventConsumer() { 372 return this.eventConsumer; 373 } 374 375 /** 376 * Adds the supplied {@link AbstractEvent} to an internal {@link 377 * BlockingQueue} and schedules a task to consume it. 378 * 379 * @param event the {@link AbstractEvent} to add; may be {@code 380 * null} in which case no action is taken 381 */ 382 @Override 383 public final void accept(final AbstractEvent<? extends T> event) { 384 if (this.closing) { 385 throw new IllegalStateException(); 386 } 387 if (event != null) { 388 final boolean added = this.queue.add(event); 389 assert added; 390 } 391 } 392 393 @Override 394 public final void close() { 395 this.closing = true; 396 this.executor.shutdown(); 397 this.task.cancel(true); 398 try { 399 if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) { 400 this.executor.shutdownNow(); 401 if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) { 402 // TODO: log 403 } 404 } 405 } catch (final InterruptedException interruptedException) { 406 this.executor.shutdownNow(); 407 Thread.currentThread().interrupt(); 408 } 409 } 410 411 412 /* 413 * Synchronization-related methods. It seems odd that one of these 414 * listeners would need to report details about synchronization, but 415 * that's what the Go code does. Maybe this functionality could be 416 * relocated "higher up". 417 */ 418 419 420 private final boolean shouldSynchronize(Instant now) { 421 final Duration interval = this.getSynchronizationInterval(); 422 final boolean returnValue; 423 if (interval == null || interval.isZero()) { 424 returnValue = false; 425 } else if (now == null) { 426 returnValue = Instant.now().compareTo(this.nextSynchronizationInstant) >= 0; 427 } else { 428 returnValue = now.compareTo(this.nextSynchronizationInstant) >= 0; 429 } 430 return returnValue; 431 } 432 433 private final void determineNextSynchronizationInterval(Instant now) { 434 final Duration synchronizationInterval = this.getSynchronizationInterval(); 435 if (synchronizationInterval == null) { 436 if (now == null) { 437 this.nextSynchronizationInstant = Instant.now(); 438 } else { 439 this.nextSynchronizationInstant = now; 440 } 441 } else if (now == null) { 442 this.nextSynchronizationInstant = Instant.now().plus(synchronizationInterval); 443 } else { 444 this.nextSynchronizationInstant = now.plus(synchronizationInterval); 445 } 446 } 447 448 public final void setSynchronizationInterval(final Duration synchronizationInterval) { 449 this.synchronizationInterval = synchronizationInterval; 450 } 451 452 public final Duration getSynchronizationInterval() { 453 return this.synchronizationInterval; 454 } 455 } 456 457}