All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.alloydb.Refresher Maven / Gradle / Ivy

/*
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.cloud.alloydb;

import com.google.common.base.Throwables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Handles periodic refresh operations for an instance. */
class Refresher {
  private static final Logger logger = LoggerFactory.getLogger(Refresher.class);
  private static final long DEFAULT_CONNECT_TIMEOUT_MS = 45000;

  private final ListeningScheduledExecutorService executor;

  private final Object connectionInfoGuard = new Object();
  private final AsyncRateLimiter rateLimiter;

  private final RefreshCalculator refreshCalculator;
  private final Supplier> refreshOperation;
  private final String name;

  @GuardedBy("connectionInfoGuard")
  private ListenableFuture current;

  @GuardedBy("connectionInfoGuard")
  private ListenableFuture next;

  @GuardedBy("connectionInfoGuard")
  private boolean refreshRunning;

  @GuardedBy("connectionInfoGuard")
  private Throwable currentRefreshFailure;

  @GuardedBy("connectionInfoGuard")
  private boolean closed;

  @GuardedBy("connectionInfoGuard")
  private boolean triggerNextRefresh = true;

  Refresher(
      String name,
      ListeningScheduledExecutorService executor,
      Supplier> refreshOperation,
      AsyncRateLimiter rateLimiter) {
    this(name, executor, new RefreshCalculator(), refreshOperation, rateLimiter, true);
  }

  /**
   * Create a new refresher with a special RefreshCalculator
   *
   * @param name the name of what is being refreshed, for logging.
   * @param executor the executor to schedule refresh tasks.
   * @param refreshCalculator the refresh calculator to determine when to start the next refresh.
   * @param refreshOperation The supplier that refreshes the data.
   * @param rateLimiter The rate limiter.
   * @param triggerNextRefresh The next refresh operation should be triggered.
   */
  Refresher(
      String name,
      ListeningScheduledExecutorService executor,
      RefreshCalculator refreshCalculator,
      Supplier> refreshOperation,
      AsyncRateLimiter rateLimiter,
      boolean triggerNextRefresh) {
    this.name = name;
    this.executor = executor;
    this.refreshCalculator = refreshCalculator;
    this.refreshOperation = refreshOperation;
    this.rateLimiter = rateLimiter;
    this.triggerNextRefresh = triggerNextRefresh;
    synchronized (connectionInfoGuard) {
      forceRefresh();
      this.current = this.next;
    }
  }

  /**
   * Returns the current data related to the instance from {@link #startRefreshAttempt()}. May block
   * if no valid data is currently available. This method is called by an application thread when it
   * is trying to create a new connection to the database. (It is not called by a
   * ListeningScheduledExecutorService task.) So it is OK to block waiting for a future to complete.
   *
   * 

When no refresh attempt is in progress, this returns immediately. Otherwise, it waits up to * timeoutMs milliseconds. If a refresh attempt succeeds, returns immediately at the end of that * successful attempt. If no attempts succeed within the timeout, throws a RuntimeException with * the exception from the last failed refresh attempt as the cause. */ ConnectionInfo getConnectionInfo(long timeoutMs) { ListenableFuture f; synchronized (connectionInfoGuard) { if (closed) { throw new IllegalStateException("Connection closed"); } f = current; } try { return f.get(timeoutMs, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { synchronized (connectionInfoGuard) { if (currentRefreshFailure != null) { throw new RuntimeException( String.format( "Unable to get valid instance data within %d ms." + " Last refresh attempt failed:", timeoutMs) + currentRefreshFailure.getMessage(), currentRefreshFailure); } } throw new RuntimeException( String.format( "Unable to get valid instance data within %d ms. No refresh has completed.", timeoutMs), e); } catch (ExecutionException | InterruptedException ex) { Throwable cause = ex.getCause(); Throwables.throwIfUnchecked(cause); throw new RuntimeException(cause); } } /** * Attempts to force a new refresh of the instance data. May fail if called too frequently or if a * new refresh is already in progress. If successful, other methods will block until refresh has * been completed. */ void forceRefresh() { synchronized (connectionInfoGuard) { if (closed) { throw new IllegalStateException("Connection closed"); } // Don't force a refresh until the current refresh operation // has produced a successful refresh. if (refreshRunning) { return; } if (next != null) { next.cancel(false); } logger.debug( String.format( "[%s] Force Refresh: the next refresh operation was cancelled." + " Scheduling new refresh operation immediately.", name)); next = this.startRefreshAttempt(); } } /** Force a new refresh of the instance data if the client certificate has expired. */ void refreshIfExpired() { ConnectionInfo info = getConnectionInfo(DEFAULT_CONNECT_TIMEOUT_MS); logger.debug( String.format( "[%s] Now = %s, Current client certificate expiration = %s", name, Instant.now().toString(), info.getExpiration())); if (Instant.now().isAfter(info.getExpiration())) { logger.debug( String.format( "[%s] Client certificate has expired. Starting next refresh operation immediately.", name)); forceRefresh(); } } /** * Triggers an update of internal information obtained from the AlloyDB Admin API, returning a * future that resolves once a valid T has been acquired. This sets up a chain of futures that * will 1. Acquire a rate limiter. 2. Attempt to fetch instance data. 3. Schedule the next attempt * to get instance data based on the success/failure of this attempt. */ private ListenableFuture startRefreshAttempt() { // As soon as we begin submitting refresh attempts to the executor, mark a refresh // as "in-progress" so that subsequent forceRefresh() calls balk until this one completes. synchronized (connectionInfoGuard) { refreshRunning = true; } logger.debug(String.format("[%s] Refresh Operation: Acquiring rate limiter permit.", name)); ListenableFuture delay = rateLimiter.acquireAsync(executor); delay.addListener( () -> logger.debug( String.format("[%s] Refresh Operation: Rate limiter permit acquired.", name)), executor); // Once rate limiter is done, attempt to getInstanceData. ListenableFuture f = Futures.whenAllComplete(delay).callAsync(refreshOperation::get, executor); // Finally, reschedule refresh after getInstanceData is complete. return Futures.whenAllComplete(f).callAsync(() -> handleRefreshResult(f), executor); } private ListenableFuture handleRefreshResult( ListenableFuture connectionInfoFuture) { try { // This does not block, because it only gets called when connectionInfoFuture has completed. // This will throw an exception if the refresh attempt has failed. ConnectionInfo info = connectionInfoFuture.get(); logger.debug( String.format( "[%s] Refresh Operation: Completed refresh with new certificate expiration at %s.", name, info.getExpiration().toString())); long secondsToRefresh = refreshCalculator.calculateSecondsUntilNextRefresh(Instant.now(), info.getExpiration()); synchronized (connectionInfoGuard) { // Refresh completed successfully, reset forceRefreshRunning. refreshRunning = false; currentRefreshFailure = null; current = Futures.immediateFuture(info); // Now update nextInstanceData to perform a refresh after the // scheduled delay if (!closed && triggerNextRefresh) { logger.debug( String.format( "[%s] Refresh Operation: Next operation scheduled at %s.", name, Instant.now() .plus(secondsToRefresh, ChronoUnit.SECONDS) .truncatedTo(ChronoUnit.SECONDS) .toString())); next = Futures.scheduleAsync( this::startRefreshAttempt, secondsToRefresh, TimeUnit.SECONDS, executor); } // Resolves to an T immediately return current; } } catch (ExecutionException | InterruptedException e) { // No refresh retry when the TerminalException is raised. final Throwable cause = e.getCause(); if (cause instanceof TerminalException) { logger.debug(String.format("[%s] Refresh Operation: Failed! No retry.", name), e); throw (TerminalException) cause; } logger.debug( String.format( "[%s] Refresh Operation: Failed! Starting next refresh operation immediately.", name), e); synchronized (connectionInfoGuard) { currentRefreshFailure = e; if (!closed) { next = this.startRefreshAttempt(); } // Resolves after the next successful refresh attempt. return next; } } } void close() { synchronized (connectionInfoGuard) { if (closed) { return; } // Cancel any in-progress requests if (!this.current.isDone()) { this.current.cancel(true); } if (!this.next.isDone()) { this.next.cancel(true); } this.current = Futures.immediateFailedFuture(new RuntimeException("Connection is closed.")); this.closed = true; } } ListenableFuture getNext() { synchronized (connectionInfoGuard) { return this.next; } } ListenableFuture getCurrent() { synchronized (connectionInfoGuard) { return this.current; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy