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.time.temporal.ChronoUnit; 025 026import java.util.ArrayList; 027import java.util.Collection; 028import java.util.Collections; 029import java.util.Objects; 030import java.util.Map; 031 032import java.util.concurrent.Executors; 033import java.util.concurrent.ScheduledExecutorService; 034import java.util.concurrent.ScheduledFuture; 035import java.util.concurrent.TimeUnit; 036 037import java.util.logging.Level; 038import java.util.logging.Logger; 039 040import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only 041import io.fabric8.kubernetes.client.KubernetesClientException; 042import io.fabric8.kubernetes.client.Watcher; 043 044import io.fabric8.kubernetes.client.dsl.Listable; 045import io.fabric8.kubernetes.client.dsl.VersionWatchable; 046 047import io.fabric8.kubernetes.api.model.HasMetadata; 048import io.fabric8.kubernetes.api.model.ObjectMeta; 049import io.fabric8.kubernetes.api.model.KubernetesResourceList; 050import io.fabric8.kubernetes.api.model.ListMeta; 051 052import net.jcip.annotations.GuardedBy; 053import net.jcip.annotations.ThreadSafe; 054 055import org.microbean.development.annotation.NonBlocking; 056 057/** 058 * A pump of sorts that continuously "pulls" logical events out of 059 * Kubernetes and {@linkplain EventCache#add(Object, AbstractEvent.Type, 060 * HasMetadata) adds them} to an {@link EventCache} so as to logically 061 * "reflect" the contents of Kubernetes into the cache. 062 * 063 * <h2>Thread Safety</h2> 064 * 065 * <p>Instances of this class are safe for concurrent use by multiple 066 * {@link Thread}s.</p> 067 * 068 * @param <T> a type of Kubernetes resource 069 * 070 * @author <a href="https://about.me/lairdnelson" 071 * target="_parent">Laird Nelson</a> 072 * 073 * @see EventCache 074 */ 075@ThreadSafe 076public class Reflector<T extends HasMetadata> implements Closeable { 077 078 079 /* 080 * Instance fields. 081 */ 082 083 084 /** 085 * The operation that was supplied at construction time. 086 * 087 * <p>This field is never {@code null}.</p> 088 * 089 * <p>It is guaranteed that the value of this field may be 090 * assignable to a reference of type {@link Listable Listable<? 091 * extends KubernetesResourceList>} or to a reference of type 092 * {@link VersionWatchable VersionWatchable<? extends Closeable, 093 * Watcher<T>>}.</p> 094 * 095 * @see Listable 096 * 097 * @see VersionWatchable 098 */ 099 private final Object operation; 100 101 /** 102 * The resource version 103 */ 104 private volatile Object lastSynchronizationResourceVersion; 105 106 private final ScheduledExecutorService synchronizationExecutorService; 107 108 @GuardedBy("this") 109 private ScheduledFuture<?> synchronizationTask; 110 111 private final boolean shutdownSynchronizationExecutorServiceOnClose; 112 113 private final Duration synchronizationInterval; 114 115 @GuardedBy("this") 116 private Closeable watch; 117 118 @GuardedBy("itself") 119 private final EventCache<T> eventCache; 120 121 /** 122 * A {@link Logger} for use by this {@link Reflector}. 123 * 124 * <p>This field is never {@code null}.</p> 125 * 126 * @see #createLogger() 127 */ 128 protected final Logger logger; 129 130 131 /* 132 * Constructors. 133 */ 134 135 136 /** 137 * Creates a new {@link Reflector}. 138 * 139 * @param <X> a type that is both an appropriate kind of {@link 140 * Listable} and {@link VersionWatchable}, such as the kind of 141 * operation returned by {@link 142 * DefaultKubernetesClient#configMaps()} and the like 143 * 144 * @param operation a {@link Listable} and a {@link 145 * VersionWatchable} that can report information from a Kubernetes 146 * cluster; must not be {@code null} 147 * 148 * @param eventCache an {@link EventCache} <strong>that will be 149 * synchronized on</strong> and into which {@link Event}s will be 150 * logically "reflected"; must not be {@code null} 151 * 152 * @exception NullPointerException if {@code operation} or {@code 153 * eventCache} is {@code null} 154 * 155 * @exception IllegalStateException if the {@link #createLogger()} 156 * method returns {@code null} 157 * 158 * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 159 * Duration) 160 * 161 * @see #start() 162 */ 163 @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 164 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation, 165 final EventCache<T> eventCache) { 166 this(operation, eventCache, null, null); 167 } 168 169 /** 170 * Creates a new {@link Reflector}. 171 * 172 * @param <X> a type that is both an appropriate kind of {@link 173 * Listable} and {@link VersionWatchable}, such as the kind of 174 * operation returned by {@link 175 * DefaultKubernetesClient#configMaps()} and the like 176 * 177 * @param operation a {@link Listable} and a {@link 178 * VersionWatchable} that can report information from a Kubernetes 179 * cluster; must not be {@code null} 180 * 181 * @param eventCache an {@link EventCache} <strong>that will be 182 * synchronized on</strong> and into which {@link Event}s will be 183 * logically "reflected"; must not be {@code null} 184 * 185 * @param synchronizationInterval a {@link Duration} representing 186 * the time in between one {@linkplain EventCache#synchronize() 187 * synchronization operation} and another; may be {@code null} in 188 * which case no synchronization will occur 189 * 190 * @exception NullPointerException if {@code operation} or {@code 191 * eventCache} is {@code null} 192 * 193 * @exception IllegalStateException if the {@link #createLogger()} 194 * method returns {@code null} 195 * 196 * @see #Reflector(Listable, EventCache, ScheduledExecutorService, 197 * Duration) 198 * 199 * @see #start() 200 */ 201 @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 202 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation, 203 final EventCache<T> eventCache, 204 final Duration synchronizationInterval) { 205 this(operation, eventCache, null, synchronizationInterval); 206 } 207 208 /** 209 * Creates a new {@link Reflector}. 210 * 211 * @param <X> a type that is both an appropriate kind of {@link 212 * Listable} and {@link VersionWatchable}, such as the kind of 213 * operation returned by {@link 214 * DefaultKubernetesClient#configMaps()} and the like 215 * 216 * @param operation a {@link Listable} and a {@link 217 * VersionWatchable} that can report information from a Kubernetes 218 * cluster; must not be {@code null} 219 * 220 * @param eventCache an {@link EventCache} <strong>that will be 221 * synchronized on</strong> and into which {@link Event}s will be 222 * logically "reflected"; must not be {@code null} 223 * 224 * @param synchronizationExecutorService a {@link 225 * ScheduledExecutorService} to be used to tell the supplied {@link 226 * EventCache} to {@linkplain EventCache#synchronize() synchronize} 227 * on a schedule; may be {@code null} in which case no 228 * synchronization will occur 229 * 230 * @param synchronizationInterval a {@link Duration} representing 231 * the time in between one {@linkplain EventCache#synchronize() 232 * synchronization operation} and another; may be {@code null} in 233 * which case no synchronization will occur 234 * 235 * @exception NullPointerException if {@code operation} or {@code 236 * eventCache} is {@code null} 237 * 238 * @exception IllegalStateException if the {@link #createLogger()} 239 * method returns {@code null} 240 * 241 * @see #start() 242 */ 243 @SuppressWarnings("rawtypes") // kubernetes-client's implementations of KubernetesResourceList use raw types 244 public <X extends Listable<? extends KubernetesResourceList> & VersionWatchable<? extends Closeable, Watcher<T>>> Reflector(final X operation, 245 final EventCache<T> eventCache, 246 final ScheduledExecutorService synchronizationExecutorService, 247 final Duration synchronizationInterval) { 248 super(); 249 this.logger = this.createLogger(); 250 if (this.logger == null) { 251 throw new IllegalStateException("createLogger() == null"); 252 } 253 final String cn = this.getClass().getName(); 254 final String mn = "<init>"; 255 if (this.logger.isLoggable(Level.FINER)) { 256 this.logger.entering(cn, mn, new Object[] { operation, eventCache, synchronizationExecutorService, synchronizationInterval }); 257 } 258 Objects.requireNonNull(operation); 259 this.eventCache = Objects.requireNonNull(eventCache); 260 // TODO: research: maybe: operation.withField("metadata.resourceVersion", "0")? 261 this.operation = operation.withResourceVersion("0"); 262 this.synchronizationInterval = synchronizationInterval; 263 if (synchronizationExecutorService == null) { 264 if (synchronizationInterval == null) { 265 this.synchronizationExecutorService = null; 266 this.shutdownSynchronizationExecutorServiceOnClose = false; 267 } else { 268 this.synchronizationExecutorService = Executors.newScheduledThreadPool(1); 269 this.shutdownSynchronizationExecutorServiceOnClose = true; 270 } 271 } else { 272 this.synchronizationExecutorService = synchronizationExecutorService; 273 this.shutdownSynchronizationExecutorServiceOnClose = false; 274 } 275 if (this.logger.isLoggable(Level.FINER)) { 276 this.logger.exiting(cn, mn); 277 } 278 } 279 280 281 /* 282 * Instance methods. 283 */ 284 285 286 /** 287 * Returns a {@link Logger} that will be used for this {@link 288 * Reflector}. 289 * 290 * <p>This method never returns {@code null}.</p> 291 * 292 * <p>Overrides of this method must not return {@code null}.</p> 293 * 294 * @return a non-{@code null} {@link Logger} 295 */ 296 protected Logger createLogger() { 297 return Logger.getLogger(this.getClass().getName()); 298 } 299 300 /** 301 * Notionally closes this {@link Reflector} by terminating any 302 * {@link Thread}s that it has started and invoking the {@link 303 * #onClose()} method while holding this {@link Reflector}'s 304 * monitor. 305 * 306 * @exception IOException if an error occurs 307 * 308 * @see #onClose() 309 */ 310 @Override 311 public synchronized final void close() throws IOException { 312 final String cn = this.getClass().getName(); 313 final String mn = "close"; 314 if (this.logger.isLoggable(Level.FINER)) { 315 this.logger.entering(cn, mn); 316 } 317 try { 318 this.closeSynchronizationExecutorService(); 319 final ScheduledFuture<?> synchronizationTask = this.synchronizationTask; 320 if (synchronizationTask != null) { 321 synchronizationTask.cancel(false); 322 } 323 this.closeSynchronizationExecutorService(); 324 if (this.watch != null) { 325 this.watch.close(); 326 } 327 } finally { 328 this.onClose(); 329 } 330 if (this.logger.isLoggable(Level.FINER)) { 331 this.logger.exiting(cn, mn); 332 } 333 } 334 335 private synchronized final void closeSynchronizationExecutorService() { 336 final String cn = this.getClass().getName(); 337 final String mn = "closeSynchronizationExecutorService"; 338 if (this.logger.isLoggable(Level.FINER)) { 339 this.logger.entering(cn, mn); 340 } 341 if (this.synchronizationExecutorService != null && this.shutdownSynchronizationExecutorServiceOnClose) { 342 this.synchronizationExecutorService.shutdown(); 343 try { 344 if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { 345 this.synchronizationExecutorService.shutdownNow(); 346 if (!this.synchronizationExecutorService.awaitTermination(60L, TimeUnit.SECONDS)) { 347 this.synchronizationExecutorService.shutdownNow(); 348 } 349 } 350 } catch (final InterruptedException interruptedException) { 351 this.synchronizationExecutorService.shutdownNow(); 352 Thread.currentThread().interrupt(); 353 } 354 } 355 if (this.logger.isLoggable(Level.FINER)) { 356 this.logger.exiting(cn, mn); 357 } 358 } 359 360 private synchronized final void setUpSynchronization() { 361 final String cn = this.getClass().getName(); 362 final String mn = "setUpSynchronization"; 363 if (this.logger.isLoggable(Level.FINER)) { 364 this.logger.entering(cn, mn); 365 } 366 if (this.synchronizationExecutorService != null) { 367 368 final Duration synchronizationDuration = this.getSynchronizationInterval(); 369 final long seconds; 370 if (synchronizationDuration == null) { 371 seconds = 0L; 372 } else { 373 seconds = synchronizationDuration.get(ChronoUnit.SECONDS); 374 } 375 376 if (seconds > 0L) { 377 if (this.logger.isLoggable(Level.INFO)) { 378 this.logger.logp(Level.INFO, cn, mn, "Scheduling downstream synchronization every {0} seconds", Long.valueOf(seconds)); 379 } 380 final ScheduledFuture<?> job = this.synchronizationExecutorService.scheduleWithFixedDelay(() -> { 381 if (shouldSynchronize()) { 382 if (logger.isLoggable(Level.FINE)) { 383 logger.logp(Level.FINE, cn, mn, "Synchronizing event cache with its downstream consumers"); 384 } 385 synchronized (eventCache) { 386 eventCache.synchronize(); 387 } 388 } 389 }, 0L, seconds, TimeUnit.SECONDS); 390 assert job != null; 391 this.synchronizationTask = job; 392 } 393 394 } 395 if (this.logger.isLoggable(Level.FINER)) { 396 this.logger.exiting(cn, mn); 397 } 398 } 399 400 /** 401 * Returns whether, at any given moment, this {@link Reflector} 402 * should cause its {@link EventCache} to {@linkplain 403 * EventCache#synchronize() synchronize}. 404 * 405 * <h2>Design Notes</h2> 406 * 407 * <p>This code follows the Go code in the Kubernetes {@code 408 * client-go/tools/cache} package. One thing that becomes clear 409 * when looking at all of this through an object-oriented lens is 410 * that it is the {@link EventCache} (the {@code delta_fifo}, in the 411 * Go code) that is ultimately in charge of synchronizing. It is 412 * not clear why this is a function of a reflector. In an 413 * object-oriented world, perhaps the {@link EventCache} itself 414 * should be in charge of resynchronization schedules.</p> 415 * 416 * @return {@code true} if this {@link Reflector} should cause its 417 * {@link EventCache} to {@linkplain EventCache#synchronize() 418 * synchronize}; {@code false} otherwise 419 */ 420 protected boolean shouldSynchronize() { 421 final String cn = this.getClass().getName(); 422 final String mn = "shouldSynchronize"; 423 if (this.logger.isLoggable(Level.FINER)) { 424 this.logger.entering(cn, mn); 425 } 426 final boolean returnValue = this.synchronizationExecutorService != null; 427 if (this.logger.isLoggable(Level.FINER)) { 428 this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 429 } 430 return returnValue; 431 } 432 433 private final Duration getSynchronizationInterval() { 434 return this.synchronizationInterval; 435 } 436 437 private final Object getLastSynchronizationResourceVersion() { 438 return this.lastSynchronizationResourceVersion; 439 } 440 441 private final void setLastSynchronizationResourceVersion(final Object resourceVersion) { 442 this.lastSynchronizationResourceVersion = resourceVersion; 443 } 444 445 /** 446 * Using the {@code operation} supplied at construction time, 447 * {@linkplain Listable#list() lists} appropriate Kubernetes 448 * resources, and then, on a separate {@link Thread}, {@linkplain 449 * VersionWatchable sets up a watch} on them, calling {@link 450 * EventCache#replace(Collection, Object)} and {@link 451 * EventCache#add(Object, AbstractEvent.Type, HasMetadata)} methods 452 * as appropriate. 453 * 454 * <p>The calling {@link Thread} is not blocked by invocations of 455 * this method.</p> 456 * 457 * @see #close() 458 */ 459 @NonBlocking 460 public synchronized final void start() { 461 final String cn = this.getClass().getName(); 462 final String mn = "start"; 463 if (this.logger.isLoggable(Level.FINER)) { 464 this.logger.entering(cn, mn); 465 } 466 if (this.watch == null) { 467 468 // Run a list operation, and get the resourceVersion of that list. 469 @SuppressWarnings("unchecked") 470 final KubernetesResourceList<? extends T> list = ((Listable<? extends KubernetesResourceList<? extends T>>)this.operation).list(); 471 assert list != null; 472 final ListMeta metadata = list.getMetadata(); 473 assert metadata != null; 474 final String resourceVersion = metadata.getResourceVersion(); 475 assert resourceVersion != null; 476 477 // Using the results of that list operation, do a full replace 478 // on the EventCache with them. 479 final Collection<? extends T> replacementItems; 480 final Collection<? extends T> items = list.getItems(); 481 if (items == null || items.isEmpty()) { 482 replacementItems = Collections.emptySet(); 483 } else { 484 replacementItems = Collections.unmodifiableCollection(new ArrayList<>(items)); 485 } 486 synchronized (eventCache) { 487 this.eventCache.replace(replacementItems, resourceVersion); 488 } 489 490 // Record the resource version we captured during our list 491 // operation. 492 this.setLastSynchronizationResourceVersion(resourceVersion); 493 494 // Now that we've vetted that our list operation works (i.e. no 495 // syntax errors, no connectivity problems) we can schedule 496 // resynchronizations if necessary. 497 this.setUpSynchronization(); 498 499 // Now that we've taken care of our list() operation, set up our 500 // watch() operation. 501 @SuppressWarnings("unchecked") 502 final Closeable temp = ((VersionWatchable<? extends Closeable, Watcher<T>>)operation).withResourceVersion(resourceVersion).watch(new WatchHandler()); 503 assert temp != null; 504 this.watch = temp; 505 506 } 507 if (this.logger.isLoggable(Level.FINER)) { 508 this.logger.exiting(cn, mn); 509 } 510 } 511 512 /** 513 * Invoked when {@link #close()} is invoked. 514 * 515 * <p>The default implementation of this method does nothing.</p> 516 */ 517 protected synchronized void onClose() { 518 519 } 520 521 522 /* 523 * Inner and nested classes. 524 */ 525 526 527 /** 528 * A {@link Watcher} of Kubernetes resources. 529 * 530 * @author <a href="https://about.me/lairdnelson" 531 * target="_parent">Laird Nelson</a> 532 * 533 * @see Watcher 534 */ 535 private final class WatchHandler implements Watcher<T> { 536 537 538 /* 539 * Constructors. 540 */ 541 542 543 /** 544 * Creates a new {@link WatchHandler}. 545 */ 546 private WatchHandler() { 547 super(); 548 final String cn = this.getClass().getName(); 549 final String mn = "<init>"; 550 if (logger.isLoggable(Level.FINER)) { 551 logger.entering(cn, mn); 552 logger.exiting(cn, mn); 553 } 554 } 555 556 557 /* 558 * Instance methods. 559 */ 560 561 562 /** 563 * Calls the {@link EventCache#add(Object, AbstractEvent.Type, 564 * HasMetadata)} method on the enclosing {@link Reflector}'s 565 * associated {@link EventCache} with information harvested from 566 * the supplied {@code resource}, and using an {@link Event.Type} 567 * selected appropriately given the supplied {@link 568 * Watcher.Action}. 569 * 570 * @param action the kind of Kubernetes event that happened; must 571 * not be {@code null} 572 * 573 * @param resource the {@link HasMetadata} object that was 574 * affected; must not be {@code null} 575 * 576 * @exception NullPointerException if {@code action} or {@code 577 * resource} was {@code null} 578 * 579 * @exception IllegalStateException if another error occurred 580 */ 581 @Override 582 public final void eventReceived(final Watcher.Action action, final T resource) { 583 final String cn = this.getClass().getName(); 584 final String mn = "eventReceived"; 585 if (logger.isLoggable(Level.FINER)) { 586 logger.entering(cn, mn, new Object[] { action, resource }); 587 } 588 Objects.requireNonNull(action); 589 Objects.requireNonNull(resource); 590 591 final ObjectMeta metadata = resource.getMetadata(); 592 assert metadata != null; 593 594 final Event.Type eventType; 595 596 switch (action) { 597 case ADDED: 598 eventType = Event.Type.ADDITION; 599 break; 600 case MODIFIED: 601 eventType = Event.Type.MODIFICATION; 602 break; 603 case DELETED: 604 eventType = Event.Type.DELETION; 605 break; 606 case ERROR: 607 // Uh...the Go code has: 608 // 609 // if event.Type == watch.Error { 610 // return apierrs.FromObject(event.Object) 611 // } 612 // 613 // Now, apierrs.FromObject is here: 614 // https://github.com/kubernetes/apimachinery/blob/kubernetes-1.9.2/pkg/api/errors/errors.go#L80-L88 615 // This is looking for a Status object. But 616 // WatchConnectionHandler will never forward on such a thing: 617 // 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 618 // 619 // So it follows that if by some chance we get here, resource 620 // will definitely be a HasMetadata. We go back to the Go 621 // code again, and remember that if the type is Error, the 622 // equivalent of this watch handler simply returns and goes home. 623 // 624 // Now, if we were to throw a RuntimeException here, which is 625 // the idiomatic equivalent of returning and going home, this 626 // would cause a watch reconnect: 627 // 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 628 // ...up to the reconnect limit. 629 // 630 // ...which is fine, but I'm not sure that in an error case a 631 // WatchEvent will ever HAVE a HasMetadata as its payload. 632 // Which means MAYBE we'll never get here. But if we do, all 633 // we can do is throw a RuntimeException...which ends up 634 // reducing to the same case as the default case below, so we 635 // fall through. 636 default: 637 eventType = null; 638 throw new IllegalStateException(); 639 } 640 641 // Add an Event of the proper kind to our EventCache. 642 if (eventType != null) { 643 if (logger.isLoggable(Level.FINE)) { 644 logger.logp(Level.FINE, cn, mn, "Adding event to cache: {0} {1}", new Object[] { eventType, resource }); 645 } 646 synchronized (eventCache) { 647 eventCache.add(Reflector.this, eventType, resource); 648 } 649 } 650 651 // Record the most recent resource version we're tracking to be 652 // that of this last successful watch() operation. We set it 653 // earlier during a list() operation. 654 setLastSynchronizationResourceVersion(metadata.getResourceVersion()); 655 656 if (logger.isLoggable(Level.FINER)) { 657 logger.exiting(cn, mn); 658 } 659 } 660 661 /** 662 * Invoked when the Kubernetes client connection closes. 663 * 664 * @param exception any {@link KubernetesClientException} that 665 * caused this closing to happen; may be {@code null} 666 */ 667 @Override 668 public final void onClose(final KubernetesClientException exception) { 669 final String cn = this.getClass().getName(); 670 final String mn = "onClose"; 671 if (logger.isLoggable(Level.FINER)) { 672 logger.entering(cn, mn, exception); 673 } 674 if (exception != null && logger.isLoggable(Level.WARNING)) { 675 logger.logp(Level.WARNING, cn, mn, exception.getMessage(), exception); 676 } 677 if (logger.isLoggable(Level.FINER)) { 678 logger.exiting(cn, mn, exception); 679 } 680 } 681 682 } 683 684}