Class: ActiveRecordSpannerAdapter::Transaction

Inherits:
Object
  • Object
show all
Defined in:
lib/activerecord_spanner_adapter/transaction.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection, isolation) ⇒ Transaction

Returns a new instance of Transaction.



11
12
13
14
15
16
17
18
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 11

def initialize connection, isolation
  @connection = connection
  @isolation = isolation
  @committable = ![:read_only, :pdml].include?(isolation) && !isolation.is_a?(Hash)
  @state = :INITIALIZED
  @sequence_number = 0
  @mutations = []
end

Instance Attribute Details

#stateObject (readonly)

Returns the value of attribute state.



9
10
11
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 9

def state
  @state
end

Instance Method Details

#_transaction_isolation_level_to_grpc(isolation) ⇒ Object



79
80
81
82
83
84
85
86
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 79

def _transaction_isolation_level_to_grpc isolation
  case isolation
  when :serializable
    Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::SERIALIZABLE
  when :repeatable_read
    Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::REPEATABLE_READ
  end
end

#active?Boolean

Returns:

  • (Boolean)


20
21
22
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 20

def active?
  @state == :STARTED
end

#beginObject

Begins the transaction.

Read-only and PDML transactions are started by executing a BeginTransaction RPC. Read/write transactions are not really started by this method, and instead a transaction selector is prepared that will be included with the first statement on the transaction.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 39

def begin
  raise "Nested transactions are not allowed" if @state != :INITIALIZED
  begin
    case @isolation
    when Hash
      if @isolation[:timestamp]
        @grpc_transaction = @connection.session.create_snapshot timestamp: @isolation[:timestamp]
      elsif @isolation[:staleness]
        @grpc_transaction = @connection.session.create_snapshot staleness: @isolation[:staleness]
      elsif @isolation[:strong]
        @grpc_transaction = @connection.session.create_snapshot strong: true
      else
        raise "Invalid snapshot argument: #{@isolation}"
      end
    when :read_only
      @grpc_transaction = @connection.session.create_snapshot strong: true
    when :pdml
      @grpc_transaction = @connection.session.create_pdml
    else
      grpc_isolation = _transaction_isolation_level_to_grpc @isolation
      @begin_transaction_selector = Google::Cloud::Spanner::V1::TransactionSelector.new \
        begin: Google::Cloud::Spanner::V1::TransactionOptions.new(
          read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new,
          isolation_level: grpc_isolation
        )
    end
    @state = :STARTED
  rescue Google::Cloud::NotFoundError => e
    if @connection.session_not_found? e
      @connection.reset!
      retry
    end
    @state = :FAILED
    raise
  rescue StandardError
    @state = :FAILED
    raise
  end
end

#buffer(mutation) ⇒ Object



29
30
31
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 29

def buffer mutation
  @mutations << mutation
end

#commitObject



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 98

def commit
  raise "This transaction is not active" unless active?

  begin
    # Start a transaction with an explicit BeginTransaction RPC if the transaction only contains mutations.
    force_begin_read_write if @committable && !@mutations.empty? && !@grpc_transaction

    @connection.session.commit_transaction @grpc_transaction, @mutations if @committable && @grpc_transaction
    @state = :COMMITTED
  rescue Google::Cloud::NotFoundError => e
    if @connection.session_not_found? e
      shoot_and_forget_rollback
      @connection.reset!
      @connection.raise_aborted_err
    end
    @state = :FAILED
    raise
  rescue StandardError
    @state = :FAILED
    raise
  end
end

#force_begin_read_writeObject

Forces a BeginTransaction RPC for a read/write transaction. This is used by a connection if the first statement of a transaction failed.



90
91
92
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 90

def force_begin_read_write
  @grpc_transaction = @connection.session.create_transaction
end

#grpc_transaction=(grpc) ⇒ Object

Sets the underlying gRPC transaction to use for this Transaction. This is used for queries/DML statements that inlined the BeginTransaction option and returned a transaction in the metadata.



147
148
149
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 147

def grpc_transaction= grpc
  @grpc_transaction = Google::Cloud::Spanner::Transaction.from_grpc grpc, @connection.session
end

#isolationObject



24
25
26
27
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 24

def isolation
  return nil unless active?
  @isolation
end

#mark_abortedObject



140
141
142
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 140

def mark_aborted
  @state = :ABORTED
end

#next_sequence_numberObject



94
95
96
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 94

def next_sequence_number
  @sequence_number += 1 if @committable
end

#rollbackObject



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 121

def rollback
  # Allow rollback after abort and/or a failed commit.
  raise "This transaction is not active" unless active? || @state == :FAILED || @state == :ABORTED
  if active? && @grpc_transaction
    # We do a shoot-and-forget rollback here, as the error that caused the transaction to be rolled back could
    # also have invalidated the transaction (e.g. `Session not found`). If the rollback fails for any other
    # reason, we also do not need to retry it or propagate the error to the application, as the transaction will
    # automatically be aborted by Cloud Spanner after 10 seconds anyways.
    shoot_and_forget_rollback
  end
  @state = :ROLLED_BACK
end

#shoot_and_forget_rollbackObject



134
135
136
137
138
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 134

def shoot_and_forget_rollback
  @connection.session.rollback @grpc_transaction.transaction_id if @committable
rescue StandardError
  # Ignored
end

#transaction_selectorObject



151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/activerecord_spanner_adapter/transaction.rb', line 151

def transaction_selector
  return unless active?

  # Use the transaction that has been started by a BeginTransaction RPC or returned by a
  # statement, if present.
  return Google::Cloud::Spanner::V1::TransactionSelector.new id: @grpc_transaction.transaction_id \
      if @grpc_transaction

  # Return a transaction selector that will instruct the statement to also start a transaction
  # and return its id as a side effect.
  @begin_transaction_selector
end