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.Closeable; 020import java.io.IOException; 021 022import java.time.Duration; 023 024import java.util.Map; 025import java.util.Objects; 026 027import java.util.concurrent.Future; 028import java.util.concurrent.ScheduledExecutorService; 029 030import java.util.function.Consumer; 031 032import java.util.logging.Level; 033import java.util.logging.Logger; 034 035import io.fabric8.kubernetes.api.model.HasMetadata; 036import io.fabric8.kubernetes.api.model.KubernetesResourceList; 037 038import io.fabric8.kubernetes.client.Watcher; 039 040import io.fabric8.kubernetes.client.dsl.Listable; 041import io.fabric8.kubernetes.client.dsl.VersionWatchable; 042 043import net.jcip.annotations.Immutable; 044import net.jcip.annotations.ThreadSafe; 045 046import org.microbean.development.annotation.NonBlocking; 047 048/** 049 * A convenient combination of a {@link Reflector}, a {@link 050 * VersionWatchable} and {@link Listable} implementation, an 051 * (internal) {@link EventQueueCollection}, a {@link Map} of known 052 * Kubernetes resources and an {@link EventQueue} {@link Consumer} 053 * that {@linkplain Reflector#start() mirrors Kubernetes cluster 054 * events} into a {@linkplain EventQueueCollection collection of 055 * <code>EventQueue</code>s} and {@linkplain 056 * EventQueueCollection#start(Consumer) arranges for their consumption 057 * and processing}. 058 * 059 * <p>{@linkplain #start() Starting} a {@link Controller} {@linkplain 060 * EventQueueCollection#start(Consumer) starts the 061 * <code>Consumer</code>} supplied at construction time, and 062 * {@linkplain Reflector#start() starts the embedded 063 * <code>Reflector</code>}. {@linkplain #close() Closing} a {@link 064 * Controller} {@linkplain Reflector#close() closes its embedded 065 * <code>Reflector</code>} and {@linkplain 066 * EventQueueCollection#close() causes the <code>Consumer</code> 067 * supplied at construction time to stop receiving 068 * <code>Event</code>s}.</p> 069 * 070 * <p>Several {@code protected} methods in this class exist to make 071 * customization easier; none require overriding and their default 072 * behavior is usually just fine.</p> 073 * 074 * <h2>Thread Safety</h2> 075 * 076 * <p>Instances of this class are safe for concurrent use by multiple 077 * threads.</p> 078 * 079 * @param <T> a Kubernetes resource type 080 * 081 * @author <a href="https://about.me/lairdnelson" 082 * target="_parent">Laird Nelson</a> 083 * 084 * @see Reflector 085 * 086 * @see EventQueueCollection 087 * 088 * @see ResourceTrackingEventQueueConsumer 089 * 090 * @see #start() 091 * 092 * @see #close() 093 */ 094@Immutable 095@ThreadSafe 096public class Controller<T extends HasMetadata> implements Closeable { 097 098 099 /* 100 * Instance fields. 101 */ 102 103 104 /** 105 * A {@link Logger} used by this {@link Controller}. 106 * 107 * <p>This field is never {@code null}.</p> 108 * 109 * @see #createLogger() 110 */ 111 protected final Logger logger; 112 113 /** 114 * The {@link Reflector} used by this {@link Controller} to mirror 115 * Kubernetes events. 116 * 117 * <p>This field is never {@code null}.</p> 118 */ 119 private final Reflector<T> reflector; 120 121 /** 122 * The {@link EventQueueCollection} used by the {@link #reflector 123 * Reflector} and by the {@link Consumer} supplied at construction 124 * time. 125 * 126 * <p>This field is never {@code null}.</p> 127 * 128 * @see EventQueueCollection#add(Object, AbstractEvent.Type, 129 * HasMetadata) 130 * 131 * @see EventQueueCollection#replace(Collection, Object) 132 * 133 * @see EventQueueCollection#synchronize() 134 * 135 * @see EventQueueCollection#start(Consumer) 136 */ 137 private final EventQueueCollection<T> eventCache; 138 139 /** 140 * A {@link Consumer} of {@link EventQueue}s that processes {@link 141 * Event}s produced, ultimately, by the {@link #reflector 142 * Reflector}. 143 * 144 * <p>This field is never {@code null}.</p> 145 */ 146 private final Consumer<? super EventQueue<? extends T>> siphon; 147 148 149 /* 150 * Constructors. 151 */ 152 153 154 /** 155 * Creates a new {@link Controller} but does not {@linkplain 156 * #start() start it}. 157 * 158 * @param <X> a {@link Listable} and {@link VersionWatchable} that 159 * will be used by the embedded {@link Reflector}; must not be 160 * {@code null} 161 * 162 * @param operation a {@link Listable} and a {@link 163 * VersionWatchable} that produces Kubernetes events; must not be 164 * {@code null} 165 * 166 * @param siphon the {@link Consumer} that will process each {@link 167 * EventQueue} as it becomes ready; must not be {@code null} 168 * 169 * @exception NullPointerException if {@code operation} or {@code 170 * siphon} is {@code null} 171 * 172 * @see #Controller(Listable, ScheduledExecutorService, Duration, 173 * Map, Consumer) 174 * 175 * @see #start() 176 */ 177 @SuppressWarnings("rawtypes") 178 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation, 179 final Consumer<? super EventQueue<? extends T>> siphon) { 180 this(operation, null, null, null, siphon); 181 } 182 183 /** 184 * Creates a new {@link Controller} but does not {@linkplain 185 * #start() start it}. 186 * 187 * @param <X> a {@link Listable} and {@link VersionWatchable} that 188 * will be used by the embedded {@link Reflector}; must not be 189 * {@code null} 190 * 191 * @param operation a {@link Listable} and a {@link 192 * VersionWatchable} that produces Kubernetes events; must not be 193 * {@code null} 194 * 195 * @param knownObjects a {@link Map} containing the last known state 196 * of Kubernetes resources the embedded {@link EventQueueCollection} 197 * is caching events for; may be {@code null} if this {@link 198 * Controller} is not interested in tracking deletions of objects; 199 * if non-{@code null} <strong>will be synchronized on by this 200 * class</strong> during retrieval and traversal operations 201 * 202 * @param siphon the {@link Consumer} that will process each {@link 203 * EventQueue} as it becomes ready; must not be {@code null} 204 * 205 * @exception NullPointerException if {@code operation} or {@code 206 * siphon} is {@code null} 207 * 208 * @see #Controller(Listable, ScheduledExecutorService, Duration, 209 * Map, Consumer) 210 * 211 * @see #start() 212 */ 213 @SuppressWarnings("rawtypes") 214 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation, 215 final Map<Object, T> knownObjects, 216 final Consumer<? super EventQueue<? extends T>> siphon) { 217 this(operation, null, null, knownObjects, siphon); 218 } 219 220 /** 221 * Creates a new {@link Controller} but does not {@linkplain 222 * #start() start it}. 223 * 224 * @param <X> a {@link Listable} and {@link VersionWatchable} that 225 * will be used by the embedded {@link Reflector}; must not be 226 * {@code null} 227 * 228 * @param operation a {@link Listable} and a {@link 229 * VersionWatchable} that produces Kubernetes events; must not be 230 * {@code null} 231 * 232 * @param synchronizationInterval a {@link Duration} representing 233 * the time in between one {@linkplain EventCache#synchronize() 234 * synchronization operation} and another; may be {@code null} in 235 * which case no synchronization will occur 236 * 237 * @param siphon the {@link Consumer} that will process each {@link 238 * EventQueue} as it becomes ready; must not be {@code null} 239 * 240 * @exception NullPointerException if {@code operation} or {@code 241 * siphon} is {@code null} 242 * 243 * @see #Controller(Listable, ScheduledExecutorService, Duration, 244 * Map, Consumer) 245 * 246 * @see #start() 247 */ 248 @SuppressWarnings("rawtypes") 249 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation, 250 final Duration synchronizationInterval, 251 final Consumer<? super EventQueue<? extends T>> siphon) { 252 this(operation, null, synchronizationInterval, null, siphon); 253 } 254 255 /** 256 * Creates a new {@link Controller} but does not {@linkplain 257 * #start() start it}. 258 * 259 * @param <X> a {@link Listable} and {@link VersionWatchable} that 260 * will be used by the embedded {@link Reflector}; must not be 261 * {@code null} 262 * 263 * @param operation a {@link Listable} and a {@link 264 * VersionWatchable} that produces Kubernetes events; must not be 265 * {@code null} 266 * 267 * @param synchronizationInterval a {@link Duration} representing 268 * the time in between one {@linkplain EventCache#synchronize() 269 * synchronization operation} and another; may be {@code null} in 270 * which case no synchronization will occur 271 * 272 * @param knownObjects a {@link Map} containing the last known state 273 * of Kubernetes resources the embedded {@link EventQueueCollection} 274 * is caching events for; may be {@code null} if this {@link 275 * Controller} is not interested in tracking deletions of objects; 276 * if non-{@code null} <strong>will be synchronized on by this 277 * class</strong> during retrieval and traversal operations 278 * 279 * @param siphon the {@link Consumer} that will process each {@link 280 * EventQueue} as it becomes ready; must not be {@code null} 281 * 282 * @exception NullPointerException if {@code operation} or {@code 283 * siphon} is {@code null} 284 * 285 * @see #Controller(Listable, ScheduledExecutorService, Duration, 286 * Map, Consumer) 287 * 288 * @see #start() 289 */ 290 @SuppressWarnings("rawtypes") 291 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation, 292 final Duration synchronizationInterval, 293 final Map<Object, T> knownObjects, 294 final Consumer<? super EventQueue<? extends T>> siphon) { 295 this(operation, null, synchronizationInterval, knownObjects, siphon); 296 } 297 298 /** 299 * Creates a new {@link Controller} but does not {@linkplain 300 * #start() start it}. 301 * 302 * @param <X> a {@link Listable} and {@link VersionWatchable} that 303 * will be used by the embedded {@link Reflector}; must not be 304 * {@code null} 305 * 306 * @param operation a {@link Listable} and a {@link 307 * VersionWatchable} that produces Kubernetes events; must not be 308 * {@code null} 309 * 310 * @param synchronizationExecutorService the {@link 311 * ScheduledExecutorService} that will be passed to the {@link 312 * Reflector} constructor; may be {@code null} in which case a 313 * default {@link ScheduledExecutorService} may be used instead 314 * 315 * @param synchronizationInterval a {@link Duration} representing 316 * the time in between one {@linkplain EventCache#synchronize() 317 * synchronization operation} and another; may be {@code null} in 318 * which case no synchronization will occur 319 * 320 * @param knownObjects a {@link Map} containing the last known state 321 * of Kubernetes resources the embedded {@link EventQueueCollection} 322 * is caching events for; may be {@code null} if this {@link 323 * Controller} is not interested in tracking deletions of objects; 324 * if non-{@code null} <strong>will be synchronized on by this 325 * class</strong> during retrieval and traversal operations 326 * 327 * @param siphon the {@link Consumer} that will process each {@link 328 * EventQueue} as it becomes ready; must not be {@code null} 329 * 330 * @exception NullPointerException if {@code operation} or {@code 331 * siphon} is {@code null} 332 * 333 * @see #start() 334 */ 335 @SuppressWarnings("rawtypes") 336 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Controller(final X operation, 337 final ScheduledExecutorService synchronizationExecutorService, 338 final Duration synchronizationInterval, 339 final Map<Object, T> knownObjects, 340 final Consumer<? super EventQueue<? extends T>> siphon) { 341 super(); 342 this.logger = this.createLogger(); 343 if (this.logger == null) { 344 throw new IllegalStateException("createLogger() == null"); 345 } 346 final String cn = this.getClass().getName(); 347 final String mn = "<init>"; 348 if (this.logger.isLoggable(Level.FINER)) { 349 this.logger.entering(cn, mn, new Object[] { operation, synchronizationExecutorService, synchronizationInterval, knownObjects, siphon }); 350 } 351 this.siphon = Objects.requireNonNull(siphon); 352 this.eventCache = new ControllerEventQueueCollection(knownObjects); 353 this.reflector = new ControllerReflector(operation, synchronizationExecutorService, synchronizationInterval); 354 if (this.logger.isLoggable(Level.FINER)) { 355 this.logger.exiting(cn, mn); 356 } 357 } 358 359 360 /* 361 * Instance methods. 362 */ 363 364 365 /** 366 * Returns a {@link Logger} for use by this {@link Controller}. 367 * 368 * <p>This method never returns {@code null}.</p> 369 * 370 * <p>Overrides of this method must not return {@code null}.</p> 371 * 372 * @return a non-{@code null} {@link Logger} 373 */ 374 protected Logger createLogger() { 375 return Logger.getLogger(this.getClass().getName()); 376 } 377 378 /** 379 * {@linkplain EventQueueCollection#start(Consumer) Starts the 380 * embedded <code>EventQueueCollection</code> consumption machinery} 381 * and then {@linkplain Reflector#start() starts the embedded 382 * <code>Reflector</code>}. 383 * 384 * @see EventQueueCollection#start(Consumer) 385 * 386 * @see Reflector#start() 387 */ 388 @NonBlocking 389 public final void start() { 390 final String cn = this.getClass().getName(); 391 final String mn = "start"; 392 if (this.logger.isLoggable(Level.FINER)) { 393 this.logger.entering(cn, mn); 394 } 395 if (this.logger.isLoggable(Level.INFO)) { 396 this.logger.logp(Level.INFO, cn, mn, "Starting {0}", this.siphon); 397 } 398 final Future<?> siphonTask = this.eventCache.start(this.siphon); 399 assert siphonTask != null; 400 if (this.logger.isLoggable(Level.INFO)) { 401 this.logger.logp(Level.INFO, cn, mn, "Starting {0}", this.reflector); 402 } 403 try { 404 this.reflector.start(); 405 } catch (final RuntimeException runtimeException) { 406 try { 407 this.reflector.close(); 408 } catch (final IOException suppressMe) { 409 runtimeException.addSuppressed(suppressMe); 410 } 411 siphonTask.cancel(true); 412 assert siphonTask.isDone(); 413 try { 414 this.eventCache.close(); 415 } catch (final RuntimeException suppressMe) { 416 runtimeException.addSuppressed(suppressMe); 417 } 418 throw runtimeException; 419 } 420 if (this.logger.isLoggable(Level.FINER)) { 421 this.logger.exiting(cn, mn); 422 } 423 } 424 425 /** 426 * {@linkplain Reflector#close() Closes the embedded 427 * <code>Reflector</code>} and then {@linkplain 428 * EventQueueCollection#close() closes the embedded 429 * <code>EventQueueCollection</code>}, handling exceptions 430 * appropriately. 431 * 432 * @exception IOException if the {@link Reflector} could not 433 * {@linkplain Reflector#close() close} properly 434 * 435 * @see Reflector#close() 436 * 437 * @see EventQueueCollection#close() 438 */ 439 @Override 440 public final void close() throws IOException { 441 final String cn = this.getClass().getName(); 442 final String mn = "close"; 443 if (this.logger.isLoggable(Level.FINER)) { 444 this.logger.entering(cn, mn); 445 } 446 Exception throwMe = null; 447 try { 448 if (this.logger.isLoggable(Level.INFO)) { 449 this.logger.logp(Level.INFO, cn, mn, "Closing {0}", this.reflector); 450 } 451 this.reflector.close(); 452 } catch (final Exception everything) { 453 throwMe = everything; 454 } 455 456 try { 457 if (this.logger.isLoggable(Level.INFO)) { 458 this.logger.logp(Level.INFO, cn, mn, "Closing {0}", this.eventCache); 459 } 460 this.eventCache.close(); 461 } catch (final RuntimeException runtimeException) { 462 if (throwMe == null) { 463 throw runtimeException; 464 } 465 throwMe.addSuppressed(runtimeException); 466 } 467 468 if (throwMe instanceof IOException) { 469 throw (IOException)throwMe; 470 } else if (throwMe instanceof RuntimeException) { 471 throw (RuntimeException)throwMe; 472 } else if (throwMe != null) { 473 throw new IllegalStateException(throwMe.getMessage(), throwMe); 474 } 475 476 if (this.logger.isLoggable(Level.FINER)) { 477 this.logger.exiting(cn, mn); 478 } 479 } 480 481 /** 482 * Returns if the embedded {@link Reflector} should {@linkplain 483 * Reflector#shouldSynchronize() synchronize}. 484 * 485 * <p>This implementation returns {@code true}.</p> 486 * 487 * @return {@code true} if the embedded {@link Reflector} should 488 * {@linkplain Reflector#shouldSynchronize() synchronize}; {@code 489 * false} otherwise 490 */ 491 protected boolean shouldSynchronize() { 492 final String cn = this.getClass().getName(); 493 final String mn = "shouldSynchronize"; 494 if (this.logger.isLoggable(Level.FINER)) { 495 this.logger.entering(cn, mn); 496 } 497 final boolean returnValue = true; 498 if (this.logger.isLoggable(Level.FINER)) { 499 this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 500 } 501 return returnValue; 502 } 503 504 /** 505 * Invoked after the embedded {@link Reflector} {@linkplain 506 * Reflector#onClose() closes}. 507 * 508 * <p>This implementation does nothing.</p> 509 * 510 * @see Reflector#close() 511 * 512 * @see Reflector#onClose() 513 */ 514 protected void onClose() { 515 516 } 517 518 /** 519 * Returns a key that can be used to identify the supplied {@link 520 * HasMetadata}. 521 * 522 * <p>This method never returns {@code null}.</p> 523 * 524 * <p>Overrides of this method must not return {@code null}.</p> 525 * 526 * <p>The default implementation of this method returns the return 527 * value of invoking the {@link HasMetadatas#getKey(HasMetadata)} 528 * method.</p> 529 * 530 * @param resource the Kubernetes resource for which a key is 531 * desired; must not be {@code null} 532 * 533 * @return a non-{@code null} key for the supplied {@link 534 * HasMetadata} 535 * 536 * @exception NullPointerException if {@code resource} is {@code 537 * null} 538 */ 539 protected Object getKey(final T resource) { 540 final String cn = this.getClass().getName(); 541 final String mn = "getKey"; 542 if (this.logger.isLoggable(Level.FINER)) { 543 this.logger.entering(cn, mn, resource); 544 } 545 final Object returnValue = HasMetadatas.getKey(Objects.requireNonNull(resource)); 546 if (this.logger.isLoggable(Level.FINER)) { 547 this.logger.exiting(cn, mn, returnValue); 548 } 549 return returnValue; 550 } 551 552 /** 553 * Creates a new {@link Event} when invoked. 554 * 555 * <p>This method never returns {@code null}.</p> 556 * 557 * <p>Overrides of this method must not return {@code null}.</p> 558 * 559 * <p>Overrides of this method must return a new {@link Event} or 560 * subclass with each invocation.</p> 561 * 562 * @param source the source of the new {@link Event}; must not be 563 * {@code null} 564 * 565 * @param eventType the {@link Event.Type} for the new {@link 566 * Event}; must not be {@code null} 567 * 568 * @param resource the {@link HasMetadata} that the new {@link 569 * Event} concerns; must not be {@code null} 570 * 571 * @return a new, non-{@code null} {@link Event} 572 * 573 * @exception NullPointerException if any of the parameters is 574 * {@code null} 575 */ 576 protected Event<T> createEvent(final Object source, final Event.Type eventType, final T resource) { 577 final String cn = this.getClass().getName(); 578 final String mn = "createEvent"; 579 if (this.logger.isLoggable(Level.FINER)) { 580 this.logger.entering(cn, mn, new Object[] { source, eventType, resource }); 581 } 582 final Event<T> returnValue = new Event<>(Objects.requireNonNull(source), Objects.requireNonNull(eventType), null, Objects.requireNonNull(resource)); 583 if (this.logger.isLoggable(Level.FINER)) { 584 this.logger.exiting(cn, mn, returnValue); 585 } 586 return returnValue; 587 } 588 589 /** 590 * Creates a new {@link EventQueue} when invoked. 591 * 592 * <p>This method never returns {@code null}.</p> 593 * 594 * <p>Overrides of this method must not return {@code null}.</p> 595 * 596 * <p>Overrides of this method must return a new {@link EventQueue} 597 * or subclass with each invocation.</p> 598 * 599 * @param key the key to create the new {@link EventQueue} with; 600 * must not be {@code null} 601 * 602 * @return a new, non-{@code null} {@link EventQueue} 603 * 604 * @exception NullPointerException if {@code key} is {@code null} 605 */ 606 protected EventQueue<T> createEventQueue(final Object key) { 607 final String cn = this.getClass().getName(); 608 final String mn = "createEventQueue"; 609 if (this.logger.isLoggable(Level.FINER)) { 610 this.logger.entering(cn, mn, key); 611 } 612 final EventQueue<T> returnValue = new EventQueue<>(key); 613 if (this.logger.isLoggable(Level.FINER)) { 614 this.logger.exiting(cn, mn, returnValue); 615 } 616 return returnValue; 617 } 618 619 620 /* 621 * Inner and nested classes. 622 */ 623 624 625 /** 626 * An {@link EventQueueCollection} that delegates its overridable 627 * methods to their equivalents in the {@link Controller} class. 628 * 629 * @author <a href="https://about.me/lairdnelson" 630 * target="_parent">Laird Nelson</a> 631 * 632 * @see EventQueueCollection 633 * 634 * @see EventCache 635 */ 636 private final class ControllerEventQueueCollection extends EventQueueCollection<T> { 637 638 639 /* 640 * Constructors. 641 */ 642 643 644 private ControllerEventQueueCollection(final Map<?, ? extends T> knownObjects) { 645 super(knownObjects); 646 } 647 648 649 /* 650 * Instance methods. 651 */ 652 653 654 @Override 655 protected final Event<T> createEvent(final Object source, final Event.Type eventType, final T resource) { 656 return Controller.this.createEvent(source, eventType, resource); 657 } 658 659 @Override 660 protected final EventQueue<T> createEventQueue(final Object key) { 661 return Controller.this.createEventQueue(key); 662 } 663 664 @Override 665 protected final Object getKey(final T resource) { 666 return Controller.this.getKey(resource); 667 } 668 669 } 670 671 672 /** 673 * A {@link Reflector} that delegates its overridable 674 * methods to their equivalents in the {@link Controller} class. 675 * 676 * @author <a href="https://about.me/lairdnelson" 677 * target="_parent">Laird Nelson</a> 678 * 679 * @see Reflector 680 */ 681 private final class ControllerReflector extends Reflector<T> { 682 683 684 /* 685 * Constructors. 686 */ 687 688 689 @SuppressWarnings("rawtypes") 690 private <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> ControllerReflector(final X operation, 691 final ScheduledExecutorService synchronizationExecutorService, 692 final Duration synchronizationInterval) { 693 super(operation, Controller.this.eventCache, synchronizationExecutorService, synchronizationInterval); 694 } 695 696 697 /* 698 * Instance methods. 699 */ 700 701 702 @Override 703 protected final boolean shouldSynchronize() { 704 return Controller.this.shouldSynchronize(); 705 } 706 707 @Override 708 protected final void onClose() { 709 Controller.this.onClose(); 710 } 711 } 712 713}