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.util.AbstractCollection; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.Iterator; 024import java.util.LinkedList; 025import java.util.NoSuchElementException; // for javadoc only 026import java.util.Objects; 027import java.util.Queue; 028 029import java.util.function.Consumer; 030 031import java.util.logging.Level; 032import java.util.logging.Logger; 033 034import io.fabric8.kubernetes.api.model.HasMetadata; 035 036import net.jcip.annotations.GuardedBy; 037import net.jcip.annotations.ThreadSafe; 038 039/** 040 * A publicly-unmodifiable {@link AbstractCollection} of {@link 041 * AbstractEvent}s produced by an {@link EventQueueCollection}. 042 * 043 * <p>All {@link AbstractEvent}s in an {@link EventQueue} describe the 044 * life of a single {@linkplain HasMetadata resource} in 045 * Kubernetes.</p> 046 * 047 * <h2>Thread Safety</h2> 048 * 049 * <p>This class is safe for concurrent use by multiple {@link 050 * Thread}s. Some operations, like the usage of the {@link 051 * #iterator()} method, require that callers synchronize on the {@link 052 * EventQueue} directly. This class' internals synchronize on {@code 053 * this} when locking is needed.</p> 054 * 055 * <p>Overrides of this class must also be safe for concurrent use by 056 * multiple {@link Thread}s.</p> 057 * 058 * @param <T> the type of a Kubernetes resource 059 * 060 * @author <a href="https://about.me/lairdnelson" 061 * target="_parent">Laird Nelson</a> 062 * 063 * @see EventQueueCollection 064 */ 065@ThreadSafe 066public class EventQueue<T extends HasMetadata> extends AbstractCollection<AbstractEvent<T>> { 067 068 069 /* 070 * Instance fields. 071 */ 072 073 074 /** 075 * A {@link Logger} for use by this {@link EventQueue}. 076 * 077 * <p>This field is never {@code null}.</p> 078 * 079 * @see #createLogger() 080 */ 081 protected final Logger logger; 082 083 /** 084 * The key identifying the Kubernetes resource to which all of the 085 * {@link AbstractEvent}s managed by this {@link EventQueue} apply. 086 * 087 * <p>This field is never {@code null}.</p> 088 */ 089 private final Object key; 090 091 /** 092 * The actual underlying queue of {@link AbstractEvent}s. 093 * 094 * <p>This field is never {@code null}.</p> 095 */ 096 @GuardedBy("this") 097 private final LinkedList<AbstractEvent<T>> events; 098 099 100 /* 101 * Constructors. 102 */ 103 104 105 /** 106 * Creates a new {@link EventQueue}. 107 * 108 * @param key the key identifying the Kubernetes resource to which 109 * all of the {@link AbstractEvent}s managed by this {@link 110 * EventQueue} apply; must not be {@code null} 111 * 112 * @exception NullPointerException if {@code key} is {@code null} 113 * 114 * @exception IllegalStateException if the {@link #createLogger()} 115 * method returns {@code null} 116 */ 117 protected EventQueue(final Object key) { 118 super(); 119 this.logger = this.createLogger(); 120 if (this.logger == null) { 121 throw new IllegalStateException("createLogger() == null"); 122 } 123 final String cn = this.getClass().getName(); 124 final String mn = "<init>"; 125 if (this.logger.isLoggable(Level.FINER)) { 126 this.logger.entering(cn, mn, key); 127 } 128 this.key = Objects.requireNonNull(key); 129 this.events = new LinkedList<>(); 130 if (this.logger.isLoggable(Level.FINER)) { 131 this.logger.exiting(cn, mn); 132 } 133 } 134 135 136 /* 137 * Instance methods. 138 */ 139 140 141 /** 142 * Returns a {@link Logger} for use by this {@link EventQueue}. 143 * 144 * <p>This method never returns {@code null}.</p> 145 * 146 * <p>Overrides of this method must not return {@code null}.</p> 147 * 148 * @return a non-{@code null} {@link Logger} 149 */ 150 protected Logger createLogger() { 151 return Logger.getLogger(this.getClass().getName()); 152 } 153 154 /** 155 * Returns the key identifying the Kubernetes resource to which all 156 * of the {@link AbstractEvent}s managed by this {@link EventQueue} 157 * apply. 158 * 159 * <p>This method never returns {@code null}.</p> 160 * 161 * @return a non-{@code null} {@link Object} 162 * 163 * @see #EventQueue(Object) 164 */ 165 public final Object getKey() { 166 final String cn = this.getClass().getName(); 167 final String mn = "getKey"; 168 if (this.logger.isLoggable(Level.FINER)) { 169 this.logger.entering(cn, mn); 170 } 171 final Object returnValue = this.key; 172 if (this.logger.isLoggable(Level.FINER)) { 173 this.logger.entering(cn, mn, returnValue); 174 } 175 return returnValue; 176 } 177 178 /** 179 * Returns {@code true} if this {@link EventQueue} is empty. 180 * 181 * @return {@code true} if this {@link EventQueue} is empty; {@code 182 * false} otherwise 183 * 184 * @see #size() 185 */ 186 public synchronized final boolean isEmpty() { 187 final String cn = this.getClass().getName(); 188 final String mn = "isEmpty"; 189 if (this.logger.isLoggable(Level.FINER)) { 190 this.logger.entering(cn, mn); 191 } 192 final boolean returnValue = this.events.isEmpty(); 193 if (this.logger.isLoggable(Level.FINER)) { 194 this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 195 } 196 return returnValue; 197 } 198 199 /** 200 * Returns the size of this {@link EventQueue}. 201 * 202 * <p>This method never returns an {@code int} less than {@code 203 * 0}.</p> 204 * 205 * @return the size of this {@link EventQueue}; never negative 206 * 207 * @see #isEmpty() 208 */ 209 @Override 210 public synchronized final int size() { 211 final String cn = this.getClass().getName(); 212 final String mn = "size"; 213 if (this.logger.isLoggable(Level.FINER)) { 214 this.logger.entering(cn, mn); 215 } 216 final int returnValue = this.events.size(); 217 if (this.logger.isLoggable(Level.FINER)) { 218 this.logger.exiting(cn, mn, Integer.valueOf(returnValue)); 219 } 220 return returnValue; 221 } 222 223 /** 224 * Adds the supplied {@link AbstractEvent} to this {@link 225 * EventQueue} under certain conditions. 226 * 227 * <p>The supplied {@link AbstractEvent} is added to this {@link 228 * EventQueue} if:</p> 229 * 230 * <ul> 231 * 232 * <li>its {@linkplain AbstractEvent#getKey() key} is equal to this 233 * {@link EventQueue}'s {@linkplain #getKey() key}</li> 234 * 235 * <li>it is either not a {@linkplain SynchronizationEvent} 236 * synchronization event}, or it <em>is</em> a {@linkplain 237 * SynchronizationEvent synchronization event} and this {@link 238 * EventQueue} does not represent a sequence of events that 239 * {@linkplain #resultsInDeletion() describes a deletion}, and</li> 240 * 241 * <li>optional {@linkplain #compress(Collection) compression} does 242 * not result in this {@link EventQueue} being empty</li> 243 * 244 * </ul> 245 * 246 * @param event the {@link AbstractEvent} to add; must not be {@code 247 * null} 248 * 249 * @return {@code true} if an addition took place and {@linkplain 250 * #compress(Collection) optional compression} did not result in 251 * this {@link EventQueue} {@linkplain #isEmpty() becoming empty}; 252 * {@code false} otherwise 253 * 254 * @exception NullPointerException if {@code event} is {@code null} 255 * 256 * @exception IllegalArgumentException if {@code event}'s 257 * {@linkplain AbstractEvent#getKey() key} is not equal to this 258 * {@link EventQueue}'s {@linkplain #getKey() key} 259 * 260 * @see #compress(Collection) 261 * 262 * @see SynchronizationEvent 263 * 264 * @see #resultsInDeletion() 265 */ 266 final boolean addEvent(final AbstractEvent<T> event) { 267 final String cn = this.getClass().getName(); 268 final String mn = "addEvent"; 269 if (this.logger.isLoggable(Level.FINER)) { 270 this.logger.entering(cn, mn, event); 271 } 272 273 Objects.requireNonNull(event); 274 275 final Object key = this.getKey(); 276 if (!key.equals(event.getKey())) { 277 throw new IllegalArgumentException("!this.getKey().equals(event.getKey()): " + key + ", " + event.getKey()); 278 } 279 280 boolean returnValue = false; 281 282 final AbstractEvent.Type eventType = event.getType(); 283 assert eventType != null; 284 285 synchronized (this) { 286 if (!(event instanceof SynchronizationEvent) || !this.resultsInDeletion()) { 287 // If the event is NOT a synchronization event (so it's an 288 // addition, modification, or deletion)... 289 // ...OR if it IS a synchronization event AND we are NOT 290 // already going to delete this queue... 291 returnValue = this.events.add(event); 292 if (returnValue) { 293 this.deduplicate(); 294 final Collection<AbstractEvent<T>> readOnlyEvents = Collections.unmodifiableCollection(this.events); 295 final Collection<AbstractEvent<T>> newEvents = this.compress(readOnlyEvents); 296 if (newEvents != readOnlyEvents) { 297 this.events.clear(); 298 if (newEvents != null && !newEvents.isEmpty()) { 299 this.events.addAll(newEvents); 300 } 301 } 302 returnValue = !this.isEmpty(); 303 } 304 } 305 } 306 307 if (this.logger.isLoggable(Level.FINER)) { 308 this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 309 } 310 return returnValue; 311 } 312 313 /** 314 * Returns the last (and definitionally newest) {@link 315 * AbstractEvent} in this {@link EventQueue}. 316 * 317 * <p>This method never returns {@code null}.</p> 318 * 319 * @return the last {@link AbstractEvent} in this {@link 320 * EventQueue}; never {@code null} 321 * 322 * @exception NoSuchElementException if this {@link EventQueue} is 323 * {@linkplain #isEmpty() empty} 324 */ 325 synchronized final AbstractEvent<T> getLast() { 326 final String cn = this.getClass().getName(); 327 final String mn = "getLast"; 328 if (this.logger.isLoggable(Level.FINER)) { 329 this.logger.entering(cn, mn); 330 } 331 final AbstractEvent<T> returnValue = this.events.getLast(); 332 if (this.logger.isLoggable(Level.FINER)) { 333 this.logger.exiting(cn, mn, returnValue); 334 } 335 return returnValue; 336 } 337 338 /** 339 * Synchronizes on this {@link EventQueue} and, while holding its 340 * monitor, invokes the {@link Consumer#accept(Object)} method on 341 * the supplied {@link Consumer} for every {@link AbstractEvent} in 342 * this {@link EventQueue}. 343 * 344 * @param action the {@link Consumer} in question; must not be 345 * {@code null} 346 * 347 * @exception NullPointerException if {@code action} is {@code null} 348 */ 349 @Override 350 public synchronized final void forEach(final Consumer<? super AbstractEvent<T>> action) { 351 super.forEach(action); 352 } 353 354 /** 355 * Synchronizes on this {@link EventQueue} and, while holding its 356 * monitor, returns an unmodifiable {@link Iterator} over its 357 * contents. 358 * 359 * <p>This method never returns {@code null}.</p> 360 * 361 * @return a non-{@code null} unmodifiable {@link Iterator} of 362 * {@link AbstractEvent}s 363 */ 364 @Override 365 public synchronized final Iterator<AbstractEvent<T>> iterator() { 366 return Collections.unmodifiableCollection(this.events).iterator(); 367 } 368 369 /** 370 * If this {@link EventQueue}'s {@linkplain #size() size} is greater 371 * than {@code 2}, and if its last two {@link AbstractEvent}s are 372 * {@linkplain AbstractEvent.Type#DELETION deletions}, and if the 373 * next-to-last deletion {@link AbstractEvent}'s {@linkplain 374 * AbstractEvent#isFinalStateKnown() state is known}, then this method 375 * causes that {@link AbstractEvent} to replace the two under consideration. 376 * 377 * <p>This method is called only by the {@link #addEvent(AbstractEvent)} 378 * method.</p> 379 * 380 * @see #addEvent(AbstractEvent) 381 */ 382 private synchronized final void deduplicate() { 383 final String cn = this.getClass().getName(); 384 final String mn = "deduplicate"; 385 if (this.logger.isLoggable(Level.FINER)) { 386 this.logger.entering(cn, mn); 387 } 388 final int size = this.size(); 389 if (size > 2) { 390 final AbstractEvent<T> lastEvent = this.events.get(size - 1); 391 final AbstractEvent<T> nextToLastEvent = this.events.get(size - 2); 392 final AbstractEvent<T> event; 393 if (lastEvent != null && nextToLastEvent != null && AbstractEvent.Type.DELETION.equals(lastEvent.getType()) && AbstractEvent.Type.DELETION.equals(nextToLastEvent.getType())) { 394 event = nextToLastEvent.isFinalStateKnown() ? nextToLastEvent : lastEvent; 395 } else { 396 event = null; 397 } 398 if (event != null) { 399 this.events.set(size - 2, event); 400 this.events.remove(size - 1); 401 } 402 } 403 if (this.logger.isLoggable(Level.FINER)) { 404 this.logger.exiting(cn, mn); 405 } 406 } 407 408 /** 409 * Returns {@code true} if this {@link EventQueue} is {@linkplain 410 * #isEmpty() not empty} and the {@linkplain #getLast() last 411 * <code>AbstractEvent</code> in this <code>EventQueue</code>} is a 412 * {@linkplain AbstractEvent.Type#DELETION deletion event}. 413 * 414 * @return {@code true} if this {@link EventQueue} currently 415 * logically represents the deletion of a resource, {@code false} 416 * otherwise 417 */ 418 synchronized final boolean resultsInDeletion() { 419 final String cn = this.getClass().getName(); 420 final String mn = "resultsInDeletion"; 421 if (this.logger.isLoggable(Level.FINER)) { 422 this.logger.entering(cn, mn); 423 } 424 final boolean returnValue = !this.isEmpty() && this.getLast().getType().equals(AbstractEvent.Type.DELETION); 425 if (this.logger.isLoggable(Level.FINER)) { 426 this.logger.exiting(cn, mn, Boolean.valueOf(returnValue)); 427 } 428 return returnValue; 429 } 430 431 /** 432 * Performs a compression operation on the supplied {@link 433 * Collection} of {@link AbstractEvent}s and returns the result of that 434 * operation. 435 * 436 * <p>This method may return {@code null}, which will result in the 437 * emptying of this {@link EventQueue}.</p> 438 * 439 * <p>This method is called while holding this {@link EventQueue}'s 440 * monitor.</p> 441 * 442 * <p>This method is called when an {@link EventQueueCollection} (or 443 * some other {@link AbstractEvent} producer with access to 444 * package-protected methods of this class) adds an {@link AbstractEvent} to 445 * this {@link EventQueue} and provides the {@link EventQueue} 446 * implementation with the ability to eliminate duplicates or 447 * otherwise compress the event stream it represents.</p> 448 * 449 * <p>This implementation simply returns the supplied {@code events} 450 * {@link Collection}; i.e. no compression is performed.</p> 451 * 452 * @param events an {@link 453 * Collections#unmodifiableCollection(Collection) unmodifiable 454 * <tt>Collection</tt>} of {@link AbstractEvent}s representing the 455 * current state of this {@link EventQueue}; will never be {@code 456 * null} 457 * 458 * @return the new state that this {@link EventQueue} should assume; 459 * may be {@code null}; may simply be the supplied {@code events} 460 * {@link Collection} if compression is not desired or implemented 461 */ 462 protected Collection<AbstractEvent<T>> compress(final Collection<AbstractEvent<T>> events) { 463 return events; 464 } 465 466 /** 467 * Returns a hashcode for this {@link EventQueue}. 468 * 469 * @return a hashcode for this {@link EventQueue} 470 * 471 * @see #equals(Object) 472 */ 473 @Override 474 public final int hashCode() { 475 int hashCode = 17; 476 477 Object value = this.getKey(); 478 int c = value == null ? 0 : value.hashCode(); 479 hashCode = 37 * hashCode + c; 480 481 synchronized (this) { 482 value = this.events; 483 c = value == null ? 0 : value.hashCode(); 484 } 485 hashCode = 37 * hashCode + c; 486 487 return hashCode; 488 } 489 490 /** 491 * Returns {@code true} if the supplied {@link Object} is also an 492 * {@link EventQueue} and is equal in all respects to this one. 493 * 494 * @param other the {@link Object} to test; may be {@code null} in 495 * which case {@code null} will be returned 496 * 497 * @return {@code true} if the supplied {@link Object} is also an 498 * {@link EventQueue} and is equal in all respects to this one; 499 * {@code false} otherwise 500 * 501 * @see #hashCode() 502 */ 503 @Override 504 public final boolean equals(final Object other) { 505 if (other == this) { 506 return true; 507 } else if (other instanceof EventQueue) { 508 final EventQueue<?> her = (EventQueue<?>)other; 509 510 final Object key = this.getKey(); 511 if (key == null) { 512 if (her.getKey() != null) { 513 return false; 514 } 515 } else if (!key.equals(her.getKey())) { 516 return false; 517 } 518 519 synchronized (this) { 520 final Object events = this.events; 521 if (events == null) { 522 synchronized (her) { 523 if (her.events != null) { 524 return false; 525 } 526 } 527 } else { 528 synchronized (her) { 529 if (!events.equals(her.events)) { 530 return false; 531 } 532 } 533 } 534 } 535 536 return true; 537 } else { 538 return false; 539 } 540 } 541 542 /** 543 * Returns a {@link String} representation of this {@link 544 * EventQueue}. 545 * 546 * <p>This method never returns {@code null}.</p> 547 * 548 * @return a non-{@code null} {@link String} representation of this 549 * {@link EventQueue} 550 */ 551 @Override 552 public synchronized final String toString() { 553 return new StringBuilder().append(this.getKey()).append(": ").append(this.events).toString(); 554 } 555 556}