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 110 * ResourceTrackingEventQueueConsumer#ResourceTrackingEventQueueConsumer(Map) 111 */ 112 public EventDistributor(final Map<Object, T> knownObjects) { 113 super(knownObjects); 114 final ReadWriteLock lock = new ReentrantReadWriteLock(); 115 this.readLock = lock.readLock(); 116 this.writeLock = lock.writeLock(); 117 this.distributors = new ArrayList<>(); 118 this.synchronizingDistributors = new ArrayList<>(); 119 this.synchronizationInterval = null; // TODO: implement/fix 120 } 121 122 123 /* 124 * Instance methods. 125 */ 126 127 128 /** 129 * Adds the supplied {@link Consumer} to this {@link 130 * EventDistributor} as a listener that will be notified of each 131 * {@link AbstractEvent} this {@link EventDistributor} receives. 132 * 133 * <p>The supplied {@link Consumer}'s {@link 134 * Consumer#accept(Object)} method may be called later on a separate 135 * thread of execution.</p> 136 * 137 * @param consumer a {@link Consumer} of {@link AbstractEvent}s; may 138 * be {@code null} in which case no action will be taken 139 * 140 * @see #removeConsumer(Consumer) 141 */ 142 public final void addConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) { 143 if (consumer != null) { 144 this.writeLock.lock(); 145 try { 146 final Pump<T> distributor = new Pump<>(this.synchronizationInterval, consumer); 147 this.distributors.add(distributor); 148 this.synchronizingDistributors.add(distributor); 149 } finally { 150 this.writeLock.unlock(); 151 } 152 } 153 } 154 155 /** 156 * Removes any {@link Consumer} {@linkplain Object#equals(Object) 157 * equal to} a {@link Consumer} previously {@linkplain 158 * #addConsumer(Consumer) added} to this {@link EventDistributor}. 159 * 160 * @param consumer the {@link Consumer} to remove; may be {@code 161 * null} in which case no action will be taken 162 * 163 * @see #addConsumer(Consumer) 164 */ 165 public final void removeConsumer(final Consumer<? super AbstractEvent<? extends T>> consumer) { 166 if (consumer != null) { 167 this.writeLock.lock(); 168 try { 169 final Iterator<? extends Pump<?>> iterator = this.distributors.iterator(); 170 assert iterator != null; 171 while (iterator.hasNext()) { 172 final Pump<?> distributor = iterator.next(); 173 if (distributor != null && consumer.equals(distributor.getEventConsumer())) { 174 iterator.remove(); 175 break; 176 } 177 } 178 } finally { 179 this.writeLock.unlock(); 180 } 181 } 182 } 183 184 /** 185 * Releases resources held by this {@link EventDistributor} during 186 * its execution. 187 */ 188 @Override 189 public final void close() { 190 this.writeLock.lock(); 191 try { 192 this.distributors.parallelStream() 193 .forEach(distributor -> { 194 distributor.close(); 195 }); 196 this.synchronizingDistributors.clear(); 197 this.distributors.clear(); 198 } finally { 199 this.writeLock.unlock(); 200 } 201 } 202 203 /** 204 * Returns {@code true} if this {@link EventDistributor} should 205 * <em>synchronize</em> with its upstream source. 206 * 207 * <h2>Design Notes</h2> 208 * 209 * <p>The Kubernetes {@code tools/cache} package spreads 210 * synchronization out among the reflector, controller, event cache 211 * and event processor constructs for no seemingly good reason. 212 * They should probably be consolidated, particularly in an 213 * object-oriented environment such as Java.</p> 214 * 215 * @return {@code true} if synchronization should occur; {@code 216 * false} otherwise 217 * 218 * @see EventCache#synchronize() 219 */ 220 public final boolean shouldSynchronize() { 221 boolean returnValue = false; 222 this.writeLock.lock(); 223 try { 224 this.synchronizingDistributors.clear(); 225 final Instant now = Instant.now(); 226 this.distributors.parallelStream() 227 .filter(distributor -> distributor.shouldSynchronize(now)) 228 .forEach(distributor -> { 229 this.synchronizingDistributors.add(distributor); 230 distributor.determineNextSynchronizationInterval(now); 231 }); 232 returnValue = !this.synchronizingDistributors.isEmpty(); 233 } finally { 234 this.writeLock.unlock(); 235 } 236 return returnValue; 237 } 238 239 /** 240 * Consumes the supplied {@link AbstractEvent} by forwarding it to 241 * the {@link Consumer#accept(Object)} method of each {@link 242 * Consumer} {@linkplain #addConsumer(Consumer) registered} with 243 * this {@link EventDistributor}. 244 * 245 * @param event the {@link AbstractEvent} to forward; may be {@code 246 * null} in which case no action is taken 247 * 248 * @see #addConsumer(Consumer) 249 */ 250 @Override 251 protected final void accept(final AbstractEvent<? extends T> event) { 252 if (event != null) { 253 if (event instanceof SynchronizationEvent) { 254 this.accept((SynchronizationEvent<? extends T>)event); 255 } else if (event instanceof Event) { 256 this.accept((Event<? extends T>)event); 257 } else { 258 assert false : "Unexpected event type: " + event.getClass(); 259 } 260 } 261 } 262 263 private final void accept(final SynchronizationEvent<? extends T> event) { 264 this.readLock.lock(); 265 try { 266 if (!this.synchronizingDistributors.isEmpty()) { 267 this.synchronizingDistributors.parallelStream() 268 .forEach(distributor -> distributor.accept(event)); 269 } 270 } finally { 271 this.readLock.unlock(); 272 } 273 } 274 275 private final void accept(final Event<? extends T> event) { 276 this.readLock.lock(); 277 try { 278 if (!this.distributors.isEmpty()) { 279 this.distributors.parallelStream() 280 .forEach(distributor -> distributor.accept(event)); 281 } 282 } finally { 283 this.readLock.unlock(); 284 } 285 } 286 287 288 /* 289 * Inner and nested classes. 290 */ 291 292 293 /** 294 * A {@link Consumer} of {@link AbstractEvent} instances that puts 295 * them on an internal queue and, in a separate thread, removes them 296 * from the queue and forwards them to the "real" {@link Consumer} 297 * supplied at construction time. 298 * 299 * @author <a href="https://about.me/lairdnelson" 300 * target="_parent">Laird Nelson</a> 301 */ 302 private static final class Pump<T extends HasMetadata> implements Consumer<AbstractEvent<? extends T>>, AutoCloseable { 303 304 private volatile boolean closing; 305 306 private volatile Instant nextSynchronizationInstant; 307 308 private volatile Duration synchronizationInterval; 309 310 final BlockingQueue<AbstractEvent<? extends T>> queue; 311 312 private final ScheduledExecutorService executor; 313 314 private final Future<?> task; 315 316 private final Consumer<? super AbstractEvent<? extends T>> eventConsumer; 317 318 private Pump(final Duration synchronizationInterval, final Consumer<? super AbstractEvent<? extends T>> eventConsumer) { 319 super(); 320 Objects.requireNonNull(eventConsumer); 321 this.eventConsumer = eventConsumer; 322 this.executor = this.createScheduledThreadPoolExecutor(); 323 if (this.executor == null) { 324 throw new IllegalStateException("createScheduledThreadPoolExecutor() == null"); 325 } 326 this.queue = new LinkedBlockingQueue<>(); 327 328 // Schedule a hopefully never-ending task to pump events from 329 // our queue to the supplied eventConsumer. We schedule this 330 // instead of simply executing it so that if for any reason it 331 // exits it will get restarted. Cancelling a scheduled task 332 // will also cancel all resubmissions of it, so this is the most 333 // robust thing to do. The delay of one second is arbitrary. 334 this.task = this.executor.scheduleWithFixedDelay(() -> { 335 try { 336 while (!Thread.currentThread().isInterrupted()) { 337 this.eventConsumer.accept(this.queue.take()); 338 } 339 } catch (final InterruptedException interruptedException) { 340 Thread.currentThread().interrupt(); 341 } 342 }, 0L, 1L, TimeUnit.SECONDS); 343 344 this.setSynchronizationInterval(synchronizationInterval); 345 } 346 347 private final ScheduledExecutorService createScheduledThreadPoolExecutor() { 348 final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); 349 executor.setRemoveOnCancelPolicy(true); 350 return executor; 351 } 352 353 private final Consumer<? super AbstractEvent<? extends T>> getEventConsumer() { 354 return this.eventConsumer; 355 } 356 357 /** 358 * Adds the supplied {@link AbstractEvent} to an internal {@link 359 * BlockingQueue} and schedules a task to consume it. 360 * 361 * @param event the {@link AbstractEvent} to add; may be {@code 362 * null} in which case no action is taken 363 */ 364 @Override 365 public final void accept(final AbstractEvent<? extends T> event) { 366 if (this.closing) { 367 throw new IllegalStateException(); 368 } 369 if (event != null) { 370 final boolean added = this.queue.add(event); 371 assert added; 372 } 373 } 374 375 @Override 376 public final void close() { 377 this.closing = true; 378 this.executor.shutdown(); 379 this.task.cancel(true); 380 try { 381 if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) { 382 this.executor.shutdownNow(); 383 if (!this.executor.awaitTermination(60, TimeUnit.SECONDS)) { 384 // TODO: log 385 } 386 } 387 } catch (final InterruptedException interruptedException) { 388 this.executor.shutdownNow(); 389 Thread.currentThread().interrupt(); 390 } 391 } 392 393 394 /* 395 * Synchronization-related methods. It seems odd that one of these 396 * listeners would need to report details about synchronization, but 397 * that's what the Go code does. Maybe this functionality could be 398 * relocated "higher up". 399 */ 400 401 402 private final boolean shouldSynchronize(Instant now) { 403 if (now == null) { 404 now = Instant.now(); 405 } 406 final Duration interval = this.getSynchronizationInterval(); 407 final boolean returnValue = interval != null && !interval.isZero() && now.compareTo(this.nextSynchronizationInstant) >= 0; 408 return returnValue; 409 } 410 411 private final void determineNextSynchronizationInterval(Instant now) { 412 if (now == null) { 413 now = Instant.now(); 414 } 415 this.nextSynchronizationInstant = now.plus(this.synchronizationInterval); 416 } 417 418 public final void setSynchronizationInterval(final Duration synchronizationInterval) { 419 this.synchronizationInterval = synchronizationInterval; 420 } 421 422 public final Duration getSynchronizationInterval() { 423 return this.synchronizationInterval; 424 } 425 } 426 427}