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

com.google.cloud.firestore.ServerSideTransactionRunner Maven / Gradle / Ivy

There is a newer version: 3.29.1
Show newest version
/*
 * Copyright 2020 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.firestore;

import static com.google.cloud.firestore.telemetry.TraceUtil.*;

import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.core.CurrentMillisClock;
import com.google.api.core.SettableApiFuture;
import com.google.api.gax.retrying.ExponentialRetryAlgorithm;
import com.google.api.gax.retrying.TimedAttemptSettings;
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
import com.google.cloud.firestore.telemetry.TraceUtil.Span;
import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.Context;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;

/**
 * Implements backoff and retry semantics for Firestore transactions.
 *
 * 

A TransactionRunner is instantiated with a `userCallback`, a `userCallbackExecutor` and * `numberOfAttempts`. Upon invoking {@link #run()}, the class invokes the provided callback on the * specified executor at most `numberOfAttempts` times. {@link #run()} returns an ApiFuture that * resolves when all retries complete. * *

TransactionRunner uses exponential backoff to increase the chance that retries succeed. To * customize the backoff settings, you can specify custom settings via {@link FirestoreOptions}. */ final class ServerSideTransactionRunner { private final Transaction.AsyncFunction userCallback; private final FirestoreImpl firestore; private final ScheduledExecutorService firestoreExecutor; private final Executor userCallbackExecutor; private final ExponentialRetryAlgorithm backoffAlgorithm; private final TransactionOptions transactionOptions; private TimedAttemptSettings nextBackoffAttempt; private ServerSideTransaction transaction; private int attemptsRemaining; private Span runTransactionSpan; private TraceUtil.Context runTransactionContext; /** * @param firestore The active Firestore instance * @param userCallback The user provided transaction callback * @param transactionOptions The options determining which executor the {@code userCallback} is * run on and whether the transaction is read-write or read-only */ ServerSideTransactionRunner( FirestoreImpl firestore, Transaction.AsyncFunction userCallback, TransactionOptions transactionOptions) { this.transactionOptions = transactionOptions; this.firestore = firestore; this.firestoreExecutor = firestore.getClient().getExecutor(); this.userCallback = userCallback; this.attemptsRemaining = transactionOptions.getNumberOfAttempts(); this.userCallbackExecutor = Context.currentContextExecutor( transactionOptions.getExecutor() != null ? transactionOptions.getExecutor() : this.firestore.getClient().getExecutor()); this.backoffAlgorithm = new ExponentialRetryAlgorithm( firestore.getOptions().getRetrySettings(), CurrentMillisClock.getDefaultClock()); this.nextBackoffAttempt = backoffAlgorithm.createFirstAttempt(); } @Nonnull private TraceUtil getTraceUtil() { return firestore.getOptions().getTraceUtil(); } ApiFuture run() { runTransactionSpan = getTraceUtil().startSpan(TraceUtil.SPAN_NAME_TRANSACTION_RUN); runTransactionSpan.setAttribute( ATTRIBUTE_KEY_TRANSACTION_TYPE, transactionOptions.getType().name()); runTransactionSpan.setAttribute( ATTRIBUTE_KEY_ATTEMPTS_ALLOWED, transactionOptions.getNumberOfAttempts()); runTransactionSpan.setAttribute(ATTRIBUTE_KEY_ATTEMPTS_REMAINING, attemptsRemaining); try (Scope ignored = runTransactionSpan.makeCurrent()) { runTransactionContext = getTraceUtil().currentContext(); --attemptsRemaining; ApiFuture result = ApiFutures.catchingAsync( ApiFutures.transformAsync( maybeRollback(), this::rollbackCallback, MoreExecutors.directExecutor()), Throwable.class, this::restartTransactionCallback, MoreExecutors.directExecutor()); runTransactionSpan.endAtFuture(result); return result; } catch (Exception error) { runTransactionSpan.end(error); throw error; } } ApiFuture begin() { TraceUtil.Span span = getTraceUtil().startSpan(TraceUtil.SPAN_NAME_TRANSACTION_BEGIN, runTransactionContext); try (Scope ignored = span.makeCurrent()) { ServerSideTransaction previousTransaction = this.transaction; this.transaction = null; ApiFuture result = ServerSideTransaction.begin(firestore, transactionOptions, previousTransaction); result = ApiFutures.transform( result, serverSideTransaction -> { serverSideTransaction.setTransactionTraceContext(runTransactionContext); return serverSideTransaction; }); span.endAtFuture(result); return result; } catch (Exception error) { span.end(error); throw error; } } private ApiFuture maybeRollback() { return hasTransaction() ? transaction.rollback() : ApiFutures.immediateFuture(null); } private boolean hasTransaction() { return transaction != null; } /** A callback that invokes the BeginTransaction callback. */ private ApiFuture rollbackCallback(Void input) { final SettableApiFuture backoff = SettableApiFuture.create(); // Add a backoff delay. At first, this is 0. firestoreExecutor.schedule( () -> backoff.set(null), nextBackoffAttempt.getRandomizedRetryDelay().toMillis(), TimeUnit.MILLISECONDS); nextBackoffAttempt = backoffAlgorithm.createNextAttempt(nextBackoffAttempt); return ApiFutures.transformAsync( backoff, this::backoffCallback, MoreExecutors.directExecutor()); } /** * Invokes the user callback on the user callback executor and returns the user-provided result. */ private SettableApiFuture invokeUserCallback() { final SettableApiFuture returnedResult = SettableApiFuture.create(); userCallbackExecutor.execute( () -> { ApiFuture userCallbackResult; try { userCallbackResult = userCallback.updateCallback(transaction); } catch (Exception e) { userCallbackResult = ApiFutures.immediateFailedFuture(e); } ApiFutures.addCallback( userCallbackResult, new ApiFutureCallback() { @Override public void onFailure(Throwable t) { returnedResult.setException(t); } @Override public void onSuccess(T result) { returnedResult.set(result); } }, firestoreExecutor); }); return returnedResult; } /** A callback that invokes the BeginTransaction callback. */ private ApiFuture backoffCallback(Void input) { return ApiFutures.transformAsync( begin(), this::beginTransactionCallback, MoreExecutors.directExecutor()); } /** * The callback for the BeginTransaction RPC, which invokes the user callback and handles all * errors thereafter. */ private ApiFuture beginTransactionCallback(ServerSideTransaction serverSideTransaction) { this.transaction = serverSideTransaction; return ApiFutures.transformAsync( invokeUserCallback(), this::userFunctionCallback, MoreExecutors.directExecutor()); } /** * The callback that is invoked after the user function finishes execution. It invokes the Commit * RPC. */ private ApiFuture userFunctionCallback(T userFunctionResult) { return ApiFutures.transform( transaction.commit(), // The callback that is invoked after the Commit RPC returns. It returns the user result. input -> userFunctionResult, MoreExecutors.directExecutor()); } /** A callback that restarts a transaction after an ApiException. It invokes the Rollback RPC. */ private ApiFuture restartTransactionCallback(Throwable throwable) { if (!(throwable instanceof ApiException)) { // This is likely a failure in the user callback. return rollbackAndReject(throwable); } ApiException apiException = (ApiException) throwable; if (isRetryableTransactionError(apiException)) { if (attemptsRemaining > 0) { getTraceUtil() .currentSpan() .addEvent("Initiating transaction retry. Attempts remaining: " + attemptsRemaining); return run(); } else { final FirestoreException firestoreException = FirestoreException.forApiException( apiException, "Transaction was cancelled because of too many retries."); return rollbackAndReject(firestoreException); } } else { final FirestoreException firestoreException = FirestoreException.forApiException( apiException, "Transaction failed with non-retryable error"); return rollbackAndReject(firestoreException); } } /** Determines whether the provided error is considered retryable. */ private static boolean isRetryableTransactionError(ApiException exception) { switch (exception.getStatusCode().getCode()) { // This list is based on // https://github.com/firebase/firebase-js-sdk/blob/c822e78b00dd3420dcc749beb2f09a947aa4a344/packages/firestore/src/core/transaction_runner.ts#L112 case ABORTED: case CANCELLED: case UNKNOWN: case DEADLINE_EXCEEDED: case INTERNAL: case UNAVAILABLE: case UNAUTHENTICATED: case RESOURCE_EXHAUSTED: return true; case INVALID_ARGUMENT: // The Firestore backend uses "INVALID_ARGUMENT" for transactions IDs that have expired. // While INVALID_ARGUMENT is generally not retryable, we retry this specific case. return exception.getMessage().contains("transaction has expired"); default: return false; } } /** Rolls the transaction back and returns the error. */ private ApiFuture rollbackAndReject(final Throwable throwable) { final SettableApiFuture failedTransaction = SettableApiFuture.create(); if (hasTransaction()) { // We use `addListener()` since we want to return the original exception regardless of // whether rollback() succeeds. transaction .rollback() .addListener( () -> { runTransactionSpan.end(throwable); failedTransaction.setException(throwable); }, MoreExecutors.directExecutor()); } else { runTransactionSpan.end(throwable); failedTransaction.setException(throwable); } return failedTransaction; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy