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

com.google.appengine.api.datastore.TransactionStackImpl Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2021 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
 *
 *     https://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.appengine.api.datastore;

import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.Future;

/**
 * For now each thread is going to have its own stack. This prevents users from sharing a
 * transaction across threads and also prevents users from reliably sharing a transaction across
 * requests that happen to be serviced by the same thread. When we start allowing users to create
 * threads we could change this implementation to allow transactions to be shared across threads,
 * but there's little point in supporting this now.
 *
 */
class TransactionStackImpl implements TransactionStack {

  private final ThreadLocalTransactionStack stack;

  public TransactionStackImpl() {
    // The thread local is static because the services we expose
    // in the datastore api and the services they depend on may have very
    // short lifespans (creating 1 or more per request), and we need to make
    // sure our transaction state persists independent of the lifecycle of
    // the services that need access to the transaction state.  This makes
    // testing a little bit tricky though, so make sure that tests invoke
    // clearStack() in their tearDown() methods.
    this(new ThreadLocalTransactionStack.StaticMember());
  }

  /**
   * Just for testing. Gives tests the opportunity to use some other implementation of {@link
   * ThreadLocalTransactionStack} that doesn't maintain state across instances like {@link
   * ThreadLocalTransactionStack.StaticMember} does.
   */
  TransactionStackImpl(ThreadLocalTransactionStack stack) {
    this.stack = stack;
  }

  @Override
  public void push(Transaction txn) {
    if (txn == null) {
      throw new NullPointerException("txn cannot be null");
    }
    getStack().txns.addFirst(txn);
  }

  // Only used by tests.  Could probably go away.
  Transaction pop() {
    try {
      Transaction txn = getStack().txns.removeFirst();
      // txn is guaranteed not to be null because we don't allow null when we
      // push and we would get a NoSuchElementException if the stack was empty.
      getStack().txnIdToTransactionData.remove(txn.getId());
      return txn;
    } catch (NoSuchElementException e) {
      throw new IllegalStateException(e);
    }
  }

  @Override
  public void remove(Transaction txn) {
    // Performance is linear, but for this to have any real negative
    // impact users would have to have a large number (like hundreds) of nested
    // txns.  Doesn't seem worth trying to optimize for this case.
    if (!getStack().txns.remove(txn)) {
      throw new IllegalStateException(
          "Attempted to deregister a transaction that is not currently registered.");
    }
    getStack().txnIdToTransactionData.remove(txn.getId());
  }

  @Override
  public Transaction peek() {
    try {
      return getStack().txns.getFirst();
    } catch (NoSuchElementException e) {
      throw new IllegalStateException(e);
    }
  }

  @Override
  public Transaction peek(Transaction returnedIfNoTxn) {
    LinkedList txns = getStack().txns;
    Transaction txn = txns.isEmpty() ? null : txns.peek();
    return txn == null ? returnedIfNoTxn : txn;
  }

  @Override
  public Collection getAll() {
    // defensive copy
    return new ArrayList(getStack().txns);
  }

  TransactionDataMap getStack() {
    return stack.get();
  }

  @Override
  public void clearAll() {
    getStack().clear();
  }

  @Override
  public void addFuture(Transaction txn, Future future) {
    getFutures(txn).add(future);
  }

  private TransactionData getTransactionData(Transaction txn) {
    TransactionDataMap txnDataMap = getStack();
    TransactionData data = txnDataMap.txnIdToTransactionData.get(txn.getId());
    if (data == null) {
      // No data yet so create one and associate it.
      data = new TransactionData();
      // The call to getId() will block until the BeginTxn rpc finishes.
      txnDataMap.txnIdToTransactionData.put(txn.getId(), data);
    }
    return data;
  }

  @Override
  public LinkedHashSet> getFutures(Transaction txn) {
    return getTransactionData(txn).futures;
  }

  @Override
  public void addPutEntities(Transaction txn, List putEntities) {
    getTransactionData(txn).puts.addAll(putEntities);
  }

  @Override
  public void addDeletedKeys(Transaction txn, List deletedKeys) {
    getTransactionData(txn).deletes.addAll(deletedKeys);
  }

  @Override
  public List getPutEntities(Transaction txn) {
    return getTransactionData(txn).puts;
  }

  @Override
  public List getDeletedKeys(Transaction txn) {
    return getTransactionData(txn).deletes;
  }

  /**
   * A wrapper for a ThreadLocal that gives us flexibility in terms of the
   * lifecycle of the ThreadLocal values. This just exists so that our production code can use a
   * static member and our test code can use an instance member (it's easy to end up with flaky
   * tests when your tests rely on static members because it's too easy to forget to clear them
   * out).
   */
  interface ThreadLocalTransactionStack {

    TransactionDataMap get();

    class StaticMember implements ThreadLocalTransactionStack {
      private static final ThreadLocal STACK =
          new ThreadLocal() {
            @Override
            protected TransactionDataMap initialValue() {
              return new TransactionDataMap();
            }
          };

      @Override
      public TransactionDataMap get() {
        return STACK.get();
      }
    }
  }

  /**
   * Associates a list of {@link Transaction Transactions} (the stack) with a map that ties
   * transaction ids to its associated {@link TransactionData}. Given a given Transaction in the
   * list we can find: The Futures whose completion must be blocked on when committing or rolling
   * back the Transaction can be found. The entities that have been put and deleted as part of the
   * Transaction (used for post op callbacks).
   */
  static final class TransactionDataMap {
    final LinkedList txns = new LinkedList();
    final Map txnIdToTransactionData =
        new HashMap();

    void clear() {
      txns.clear();
      txnIdToTransactionData.clear();
    }
  }

  /**
   * Data associated with a Transaction. We can't store this data inside {@link TransactionImpl}
   * because of an API design mistake we made very early on - making Transaction an interface and
   * allowing users to pass arbitrary implementations to us without checking the runtime type.
   */
  static final class TransactionData {
    final LinkedHashSet> futures = new LinkedHashSet>();
    final List deletes = Lists.newArrayList();
    final List puts = Lists.newArrayList();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy