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

io.permazen.kv.spanner.SpannerKVTransaction Maven / Gradle / Ivy


/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package io.permazen.kv.spanner;

import com.google.cloud.Timestamp;
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.TransactionContext;
import com.google.cloud.spanner.TransactionManager;
import com.google.common.base.Preconditions;

import io.permazen.kv.CloseableKVStore;
import io.permazen.kv.KVPair;
import io.permazen.kv.KVStore;
import io.permazen.kv.KVTransaction;
import io.permazen.kv.KVTransactionException;
import io.permazen.kv.RetryKVTransactionException;
import io.permazen.kv.StaleKVTransactionException;
import io.permazen.kv.mvcc.MutableView;
import io.permazen.kv.util.ForwardingKVStore;
import io.permazen.util.CloseableIterator;

import java.util.concurrent.Future;

import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link SpannerKVDatabase} transaction.
 */
@ThreadSafe
public class SpannerKVTransaction extends ForwardingKVStore implements KVTransaction {

    // Lock order: (1) SpannerKVTransaction, (2) SpannerKVDatabase

    private enum State {
        INITIAL,                    // transaction is open, but no data has been accessed yet
        ACCESSED,                   // transaction is open, and some data has been queried from spanner
        CLOSED                      // transaction is closed
    };

    protected final Logger log = LoggerFactory.getLogger(this.getClass());
    protected final SpannerKVDatabase kvdb;
    protected final DatabaseClient client;
    protected final String tableName;
    protected final TimestampBound consistency;

    @GuardedBy("this")
    private boolean readOnly;
    @GuardedBy("this")
    private ReadContext context;
    @GuardedBy("this")
    private TransactionManager transactionManager;              // read/write transactions only
    @GuardedBy("this")
    private ReadWriteSpannerView view;
    @GuardedBy("this")
    private State state = State.INITIAL;

    // Workaround for https://github.com/GoogleCloudPlatform/google-cloud-java/issues/3172
    @GuardedBy("this")
    private boolean transactionManagerClosed;

    /**
     * Constructor.
     *
     * @param kvdb associated database
     * @param client client for access
     * @param tableName Spanner key/value database table name
     * @param consistency transaction consistency level
     * @throws IllegalArgumentException if any paramter is null
     */
    @SuppressWarnings("this-escape")
    protected SpannerKVTransaction(SpannerKVDatabase kvdb, DatabaseClient client, String tableName, TimestampBound consistency) {
        Preconditions.checkArgument(kvdb != null);
        Preconditions.checkArgument(client != null);
        Preconditions.checkArgument(tableName != null);
        Preconditions.checkArgument(consistency != null);
        this.kvdb = kvdb;
        this.client = client;
        this.tableName = tableName;
        this.consistency = consistency;
        this.readOnly = !this.isStrongConsistency();
        if (this.log.isTraceEnabled())
            this.log.trace("{}: created from client={}", this, this.client);
    }

// Accessors

    /**
     * Get the consistency level configured for this transaction.
     *
     * 

* Note that read-write transactions always use strong consistency. * * @return consistency of this transaction */ public synchronized TimestampBound getConsistency() { return this.consistency; } /** * Convenience method to determine whether this transaction is using strong consistency. * * @return true if this transaction has strong consistency */ public synchronized boolean isStrongConsistency() { return this.consistency.getMode().equals(TimestampBound.Mode.STRONG); } /** * Get the timestamp associated with this transaction. * *

* For read-only transactions, this returns the Spanner timestamp at which the data was accessed. * It should not be invoked until at least one data query has occurred. * *

* For read-write transactions, this returns the Spanner timestamp at which the changes were successfully applied. * * @return this transaction's Spanner timestamp * @throws IllegalStateException if timestamp is not available yet * @throws IllegalStateException if invoked on a read/write transaction that was not successfully committed */ public synchronized Timestamp getTimestamp() { try { switch (this.state) { case INITIAL: throw new IllegalStateException("no data has been accessed yet"); case ACCESSED: if (!this.readOnly) throw new IllegalStateException("transaction is not committed yet"); break; case CLOSED: default: break; } if (this.context instanceof TransactionContext) return this.transactionManager.getCommitTimestamp(); if (this.context instanceof ReadOnlyTransaction) return ((ReadOnlyTransaction)this.context).getReadTimestamp(); return null; } catch (SpannerException e) { throw this.wrapException(e); } } // KVTransaction @Override public SpannerKVDatabase getKVDatabase() { return this.kvdb; } /** * Set transaction timeout. * *

* Currently not supported; this method does nothing. */ @Override public void setTimeout(long timeout) { // ignore - not supported } @Override public synchronized boolean isReadOnly() { return this.readOnly; } @Override public synchronized void setReadOnly(boolean readOnly) { Preconditions.checkState(this.state == State.INITIAL || readOnly == this.readOnly, "data already accessed"); Preconditions.checkArgument(this.isStrongConsistency() || readOnly, "strong consistency is required for read-write transactions"); if (this.log.isTraceEnabled()) this.log.trace("{}: setting readOnly={}", this, readOnly); this.readOnly = readOnly; } @Override public synchronized void commit() { // Logging if (this.log.isTraceEnabled()) { this.log.trace("{}: commit() invoked: state={} view={} context={} txmgr={}[{}]", this, this.state, this.view, this.context, this.transactionManager, this.transactionManager != null ? this.transactionManager.getState() : ""); } // Check state switch (this.state) { case INITIAL: assert this.view == null; assert this.context == null; assert this.transactionManager == null; this.state = State.CLOSED; return; case ACCESSED: break; case CLOSED: default: throw new StaleKVTransactionException(this); } // Commit transaction (if read/write) and close view try { if (this.context instanceof TransactionContext) { assert this.transactionManager != null; // Transfer mutations into the transaction context this.view.bufferMutations((TransactionContext)this.context); // Commit transaction if (this.log.isTraceEnabled()) { this.log.trace("{}: committing context={} txmgr={}[{}]", this, this.context, this.transactionManager, this.transactionManager.getState()); } try { this.transactionManager.commit(); } finally { if (this.transactionManager.getState() != TransactionManager.TransactionState.ABORTED) this.transactionManagerClosed = true; } if (this.log.isTraceEnabled()) this.log.trace("{}: commit successful", this); } } catch (SpannerException e) { if (this.log.isTraceEnabled()) this.log.trace("{}: commit failed: ", this, e.toString()); throw this.wrapException(e); } finally { this.cleanup(); } } @Override public synchronized void rollback() { // Logging if (this.log.isTraceEnabled()) { this.log.trace("{}: rollback() invoked: state={} view={} context={} txmgr={}[{}]", this, this.state, this.view, this.context, this.transactionManager, this.transactionManager != null ? this.transactionManager.getState() : ""); } // Check state switch (this.state) { case INITIAL: assert this.view == null; assert this.context == null; assert this.transactionManager == null; this.state = State.CLOSED; return; case ACCESSED: break; case CLOSED: default: return; } // Rollback transaction (if read/write) and close view try { if (this.context instanceof TransactionContext) { assert this.transactionManager != null; if (this.log.isTraceEnabled()) { this.log.trace("{}: rolling back context={} txmgr={}[{}]", this, this.context, this.transactionManager, this.transactionManager.getState()); } this.transactionManagerClosed = true; this.transactionManager.rollback(); if (this.log.isTraceEnabled()) this.log.trace("{}: rollback successful", this); } } catch (SpannerException e) { if (this.log.isDebugEnabled()) this.log.debug(this + ": got exception during rollback (ignoring)", e); } finally { this.cleanup(); } } private void cleanup() { // Sanity check assert Thread.holdsLock(this); assert State.ACCESSED.equals(this.state); // Update database RTT estimate this.kvdb.updateRttEstimate(this.view.getRttEstimate()); // Close the view try { if (this.log.isTraceEnabled()) { this.log.trace("{}: cleanup(): view={} context={} txmgr={}[{}]: closing view", this, this.view, this.context, this.transactionManager, this.transactionManager != null ? this.transactionManager.getState() : ""); } this.view.close(); if (this.log.isTraceEnabled()) this.log.trace("{}: cleanup(): view closed", this); } finally { this.view = null; this.context = null; this.state = State.CLOSED; } // Close the transaction manager, if any if (this.transactionManager != null && !this.transactionManagerClosed) { if (this.log.isTraceEnabled()) this.log.trace("{}: cleanup(): closing txmgr", this); this.transactionManager.close(); this.transactionManagerClosed = true; if (this.log.isTraceEnabled()) this.log.trace("{}: cleanup(): txmgr closed", this); } } /** * Set key watch. * *

* Key watches are not supported. * * @throws UnsupportedOperationException always */ @Override public Future watchKey(byte[] key) { throw new UnsupportedOperationException(); } /** * Create a mutable snapshot of this transaction. * *

* This method is not supported. * *

* With Spanner, a transaction is not needed to create mutable snapshots; instead, see * {@link SpannerKVDatabase#snapshot SpannerKVDatabase.snapshot()} and {@link MutableView}. * * @throws UnsupportedOperationException always */ @Override public CloseableKVStore readOnlySnapshot() { throw new UnsupportedOperationException(); } // ForwardingKVStore @Override public byte[] get(byte[] key) { try { return super.get(key); } catch (SpannerException e) { this.rollback(); throw this.wrapException(e); } } @Override public KVPair getAtLeast(byte[] minKey, byte[] maxKey) { try { return super.getAtLeast(minKey, maxKey); } catch (SpannerException e) { this.rollback(); throw this.wrapException(e); } } @Override public KVPair getAtMost(byte[] maxKey, byte[] minKey) { try { return super.getAtMost(maxKey, minKey); } catch (SpannerException e) { this.rollback(); throw this.wrapException(e); } } @Override public CloseableIterator getRange(byte[] minKey, byte[] maxKey, boolean reverse) { try { return super.getRange(minKey, maxKey, reverse); } catch (SpannerException e) { this.rollback(); throw this.wrapException(e); } } @Override protected synchronized KVStore delegate() { // Check state switch (this.state) { case INITIAL: assert this.view == null; assert this.context == null; assert this.transactionManager == null; break; case ACCESSED: assert this.view != null; assert this.context != null; assert this.transactionManager != null == !this.readOnly; if (this.log.isTraceEnabled()) { this.log.trace("{}: delegate(): view={} exists, context={}, txmgr={}[{}]", this, this.view, this.context, this.transactionManager, this.transactionManager != null ? this.transactionManager.getState() : ""); } return this.view; case CLOSED: default: assert this.view == null; assert this.context == null; throw new StaleKVTransactionException(this); } // Create the appropriate context and view if (this.readOnly) { if (this.log.isTraceEnabled()) this.log.trace("{}: delegate(): creating r/o transaction", this); this.context = this.client.readOnlyTransaction(this.consistency); if (this.log.isTraceEnabled()) this.log.trace("{}: delegate(): created r/o transaction, context={}", this, this.context); } else { if (this.log.isTraceEnabled()) this.log.trace("{}: delegate(): creating r/w transaction", this); this.transactionManager = this.client.transactionManager(); this.context = this.transactionManager.begin(); if (this.log.isTraceEnabled()) { this.log.trace("{}: delegate(): created r/w transaction, context={}, txmgr={}[{}]", this, this.context, this.transactionManager, this.transactionManager.getState()); } } this.view = new ReadWriteSpannerView(this.tableName, context, this::wrapException, this.kvdb.getExecutorService(), (long)this.kvdb.getRttEstimate()); if (this.log.isTraceEnabled()) this.log.trace("{}: delegate(): created view={}", this, this.view); // Done this.state = State.ACCESSED; return this.view; } protected RuntimeException wrapException(SpannerException e) { return e.isRetryable() || e instanceof AbortedException || ErrorCode.ABORTED.equals(e.getErrorCode()) ? new RetryKVTransactionException(this, e.getMessage(), e) : new KVTransactionException(this, e.getMessage(), e); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy