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

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

Go to download

The AlloyDB JDBC Connector provides a socket factory for the Postgres JDBC driver for AlloyDB. The socket factory provides an mTLS 1.3 connection to any specified AlloyDB instance without requiring any manually configured TLS certificates.

There is a newer version: 1.1.8
Show newest version
/*
 * 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 - 2025 Weber Informatics LLC | Privacy Policy