google-cloud-spanner 2.17.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc70eb07f76250946af950e0dae045072c767ed0fa1be9c2f983c24f4730679a
4
- data.tar.gz: d8faa1c37467724ed5227dcea972c0da233b56e1cf88354ff084c8394792b6ea
3
+ metadata.gz: 37f9332aedaf9d53b1eb13596dafa4b3571393449b85ce5e63750b5c57381e3f
4
+ data.tar.gz: b137394c7819e4795a98727e626772ece2404ef40bc7ef84153014768ddbfb89
5
5
  SHA512:
6
- metadata.gz: 84ceadbf10e41390c371177d5fedfb846ea83e2eabd0017b94f83a08b9ded5fff51de65f64c3cd7ee52112066593a156fb944d8b895f18665f1f90aec53dbf17
7
- data.tar.gz: 226d5e9d43e373aa736ea58f0b4fe49ea846f995996f91448fbf387b2ba12e574c91c5bf313d37a0625324b725de46a3eee7092cbded4e23b1c6956a1674c48d
6
+ metadata.gz: 0ea41265627415904e066549390d59ad72a2be577c3b6f7ac67d8a47197e4f2df4a79f9c2c14735bcd383989b2f3bfa602fe68e3ebde1d7cc055252d8644f36f
7
+ data.tar.gz: 2cc9bf3bbe521e85c72ad2dd255eedf09225c0bbcc67bc4c319a4036aa7beac01c18f73a0fb6ff1fd203b96b15ff2b3bcbb82f2f8ddfd0f2aa72e1d2f31f03e9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Release History
2
2
 
3
+ ### 2.18.0 (2023-09-05)
4
+
5
+ #### Features
6
+
7
+ * Implement "Inline Begin Transaction" ([#54](https://github.com/googleapis/ruby-spanner/issues/54))
8
+
3
9
  ### 2.17.0 (2023-06-23)
4
10
 
5
11
  #### Features
@@ -0,0 +1,50 @@
1
+ # Copyright 2023 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Spanner
19
+ ##
20
+ # @private Helper class to process BatchDML response
21
+ class BatchUpdateResults
22
+ ## Object of type
23
+ # Google::Cloud::Spanner::V1::ExecuteBatchDmlResponse
24
+ attr_reader :grpc
25
+
26
+ def initialize grpc
27
+ @grpc = grpc
28
+ end
29
+
30
+ def row_counts
31
+ if @grpc.status.code.zero?
32
+ @grpc.result_sets.map { |rs| rs.stats.row_count_exact }
33
+ else
34
+ begin
35
+ raise Google::Cloud::Error.from_error @grpc.status
36
+ rescue Google::Cloud::Error
37
+ raise Google::Cloud::Spanner::BatchUpdateError.from_grpc @grpc
38
+ end
39
+ end
40
+ end
41
+
42
+ ##
43
+ # Returns transaction if available. Otherwise returns nil
44
+ def transaction
45
+ @grpc&.result_sets&.first&.metadata&.transaction
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -49,6 +49,10 @@ module Google
49
49
  # end
50
50
  #
51
51
  class Client
52
+ ##
53
+ # @private
54
+ IS_TRANSACTION_RUNNING_KEY = "ruby_spanner_is_transaction_running".freeze
55
+
52
56
  ##
53
57
  # @private Creates a new Spanner Client instance.
54
58
  def initialize project, instance_id, database_id, session_labels: nil,
@@ -1634,6 +1638,7 @@ module Google
1634
1638
  # rubocop:disable Metrics/MethodLength
1635
1639
  # rubocop:disable Metrics/BlockLength
1636
1640
 
1641
+
1637
1642
  ##
1638
1643
  # Creates a transaction for reads and writes that execute atomically at
1639
1644
  # a single logical point in time across columns, rows, and tables in a
@@ -1788,7 +1793,7 @@ module Google
1788
1793
  def transaction deadline: 120, commit_options: nil,
1789
1794
  request_options: nil, call_options: nil
1790
1795
  ensure_service!
1791
- unless Thread.current[:transaction_id].nil?
1796
+ unless Thread.current[IS_TRANSACTION_RUNNING_KEY].nil?
1792
1797
  raise "Nested transactions are not allowed"
1793
1798
  end
1794
1799
 
@@ -1799,17 +1804,20 @@ module Google
1799
1804
  request_options = Convert.to_request_options \
1800
1805
  request_options, tag_type: :transaction_tag
1801
1806
 
1802
- @pool.with_transaction do |tx|
1807
+ @pool.with_session do |session|
1808
+ tx = session.create_empty_transaction
1803
1809
  if request_options
1804
1810
  tx.transaction_tag = request_options[:transaction_tag]
1805
1811
  end
1806
1812
 
1807
1813
  begin
1808
- Thread.current[:transaction_id] = tx.transaction_id
1814
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = true
1809
1815
  yield tx
1816
+ transaction_id = nil
1817
+ transaction_id = tx.transaction_id if tx.existing_transaction?
1810
1818
  commit_resp = @project.service.commit \
1811
1819
  tx.session.path, tx.mutations,
1812
- transaction_id: tx.transaction_id,
1820
+ transaction_id: transaction_id,
1813
1821
  commit_options: commit_options,
1814
1822
  request_options: request_options,
1815
1823
  call_options: call_options
@@ -1819,28 +1827,21 @@ module Google
1819
1827
  Google::Cloud::AbortedError,
1820
1828
  GRPC::Internal,
1821
1829
  Google::Cloud::InternalError => e
1822
- raise e if internal_error_and_not_retryable? e
1823
- # Re-raise if deadline has passed
1824
- if current_time - start_time > deadline
1825
- if e.is_a? GRPC::BadStatus
1826
- e = Google::Cloud::Error.from_error e
1827
- end
1828
- raise e
1829
- end
1830
+ check_and_propagate_err! e, (current_time - start_time > deadline)
1830
1831
  # Sleep the amount from RetryDelay, or incremental backoff
1831
1832
  sleep(delay_from_aborted(e) || backoff *= 1.3)
1832
1833
  # Create new transaction on the session and retry the block
1833
- tx = tx.session.create_transaction
1834
+ tx = session.create_transaction
1834
1835
  retry
1835
1836
  rescue StandardError => e
1836
1837
  # Rollback transaction when handling unexpected error
1837
- tx.session.rollback tx.transaction_id
1838
+ tx.session.rollback tx.transaction_id if tx.existing_transaction?
1838
1839
  # Return nil if raised with rollback.
1839
1840
  return nil if e.is_a? Rollback
1840
1841
  # Re-raise error.
1841
1842
  raise e
1842
1843
  ensure
1843
- Thread.current[:transaction_id] = nil
1844
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = nil
1844
1845
  end
1845
1846
  end
1846
1847
  end
@@ -1923,7 +1924,7 @@ module Google
1923
1924
  exact_staleness: exact_staleness
1924
1925
 
1925
1926
  ensure_service!
1926
- unless Thread.current[:transaction_id].nil?
1927
+ unless Thread.current[IS_TRANSACTION_RUNNING_KEY].nil?
1927
1928
  raise "Nested snapshots are not allowed"
1928
1929
  end
1929
1930
 
@@ -1933,11 +1934,11 @@ module Google
1933
1934
  timestamp: (timestamp || read_timestamp),
1934
1935
  staleness: (staleness || exact_staleness),
1935
1936
  call_options: call_options
1936
- Thread.current[:transaction_id] = snp_grpc.id
1937
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = true
1937
1938
  snp = Snapshot.from_grpc snp_grpc, session
1938
1939
  yield snp if block_given?
1939
1940
  ensure
1940
- Thread.current[:transaction_id] = nil
1941
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = nil
1941
1942
  end
1942
1943
  nil
1943
1944
  end
@@ -2272,6 +2273,18 @@ module Google
2272
2273
  nil
2273
2274
  end
2274
2275
 
2276
+ ##
2277
+ # Determines if a transaction error should be propagated to the user.
2278
+ # And re-raises the error accordingly
2279
+ def check_and_propagate_err! err, deadline_passed
2280
+ raise err if internal_error_and_not_retryable? err
2281
+ return unless deadline_passed
2282
+ if err.is_a? GRPC::BadStatus
2283
+ raise Google::Cloud::Error.from_error err
2284
+ end
2285
+ raise err
2286
+ end
2287
+
2275
2288
  def internal_error_and_not_retryable? error
2276
2289
  (error.instance_of?(Google::Cloud::InternalError) ||
2277
2290
  error.instance_of?(GRPC::Internal)) &&
@@ -29,19 +29,15 @@ module Google
29
29
  # {Google::Cloud::Spanner::Session} instances.
30
30
  #
31
31
  class Pool
32
- attr_accessor :all_sessions
33
- attr_accessor :session_stack
34
- attr_accessor :transaction_stack
32
+ attr_accessor :sessions_available
33
+ attr_accessor :sessions_in_use
35
34
 
36
35
  def initialize client, min: 10, max: 100, keepalive: 1800,
37
- write_ratio: 0.3, fail: true, threads: nil
36
+ fail: true, threads: nil
38
37
  @client = client
39
38
  @min = min
40
39
  @max = max
41
40
  @keepalive = keepalive
42
- @write_ratio = write_ratio
43
- @write_ratio = 0 if write_ratio.negative?
44
- @write_ratio = 1 if write_ratio > 1
45
41
  @fail = fail
46
42
  @threads = threads || [2, Concurrent.processor_count * 2].max
47
43
 
@@ -69,10 +65,11 @@ module Google
69
65
 
70
66
  # Use LIFO to ensure sessions are used from backend caches, which
71
67
  # will reduce the read / write latencies on user requests.
72
- read_session = session_stack.pop # LIFO
73
- return read_session if read_session
74
- write_transaction = transaction_stack.pop # LIFO
75
- return write_transaction.session if write_transaction
68
+ read_session = sessions_available.pop # LIFO
69
+ if read_session
70
+ sessions_in_use << read_session
71
+ return read_session
72
+ end
76
73
 
77
74
  if can_allocate_more_sessions?
78
75
  action = :new
@@ -85,73 +82,25 @@ module Google
85
82
  end
86
83
  end
87
84
 
88
- return new_session! if action == :new
89
- end
90
-
91
- def checkin_session session
92
- @mutex.synchronize do
93
- unless all_sessions.include? session
94
- raise ArgumentError, "Cannot checkin session"
85
+ if action == :new
86
+ session = new_session!
87
+ @mutex.synchronize do
88
+ sessions_in_use << session
95
89
  end
96
-
97
- session_stack.push session
98
-
99
- @resource.signal
90
+ return session
100
91
  end
101
92
 
102
93
  nil
103
94
  end
104
95
 
105
- def with_transaction
106
- tx = checkout_transaction
107
- begin
108
- yield tx
109
- ensure
110
- future do
111
- # Create and checkin a new transaction
112
- tx = tx.session.create_transaction
113
- checkin_transaction tx
114
- end
115
- end
116
- end
117
-
118
- def checkout_transaction
119
- action = nil
120
- @mutex.synchronize do
121
- loop do
122
- raise ClientClosedError if @closed
123
-
124
- write_transaction = transaction_stack.pop # LIFO
125
- return write_transaction if write_transaction
126
- read_session = session_stack.pop
127
- if read_session
128
- action = read_session
129
- break
130
- end
131
-
132
- if can_allocate_more_sessions?
133
- action = :new
134
- break
135
- end
136
-
137
- raise SessionLimitError if @fail
138
-
139
- @resource.wait @mutex
140
- end
141
- end
142
- if action.is_a? Google::Cloud::Spanner::Session
143
- return action.create_transaction
144
- end
145
- return new_transaction! if action == :new
146
- end
147
-
148
- def checkin_transaction txn
96
+ def checkin_session session
149
97
  @mutex.synchronize do
150
- unless all_sessions.include? txn.session
98
+ unless sessions_in_use.include? session
151
99
  raise ArgumentError, "Cannot checkin session"
152
100
  end
153
101
 
154
- transaction_stack.push txn
102
+ sessions_available.push session
103
+ sessions_in_use.delete_if { |s| s.session_id == session.session_id }
155
104
 
156
105
  @resource.signal
157
106
  end
@@ -182,22 +131,20 @@ module Google
182
131
  to_release = []
183
132
 
184
133
  @mutex.synchronize do
185
- available_count = session_stack.count + transaction_stack.count
134
+ available_count = sessions_available.count
186
135
  release_count = @min - available_count
187
136
  release_count = 0 if release_count.negative?
188
137
 
189
- to_keepalive += (session_stack + transaction_stack).select do |x|
138
+ to_keepalive += sessions_available.select do |x|
190
139
  x.idle_since? @keepalive
191
140
  end
192
141
 
193
- # Remove a random portion of the sessions and transactions
142
+ # Remove a random portion of the sessions
194
143
  to_release = to_keepalive.sample release_count
195
144
  to_keepalive -= to_release
196
145
 
197
146
  # Remove those to be released from circulation
198
- @all_sessions -= to_release.map(&:session)
199
- @session_stack -= to_release
200
- @transaction_stack -= to_release
147
+ @sessions_available -= to_release
201
148
  end
202
149
 
203
150
  to_release.each { |x| future { x.release! } }
@@ -212,19 +159,11 @@ module Google
212
159
  max_threads: @threads
213
160
  # init the stacks
214
161
  @new_sessions_in_process = 0
215
- @transaction_stack = []
216
162
  # init the keepalive task
217
163
  create_keepalive_task!
218
164
  # init session stack
219
- @all_sessions = @client.batch_create_new_sessions @min
220
- sessions = @all_sessions.dup
221
- num_transactions = (@min * @write_ratio).round
222
- pending_transactions = sessions.shift num_transactions
223
- # init transaction stack
224
- pending_transactions.each do |transaction|
225
- future { checkin_transaction transaction.create_transaction }
226
- end
227
- @session_stack = sessions
165
+ @sessions_available = @client.batch_create_new_sessions @min
166
+ @sessions_in_use = []
228
167
  end
229
168
 
230
169
  def shutdown
@@ -236,10 +175,10 @@ module Google
236
175
  @resource.broadcast
237
176
  # Delete all sessions
238
177
  @mutex.synchronize do
239
- @all_sessions.each { |s| future { s.release! } }
240
- @all_sessions = []
241
- @session_stack = []
242
- @transaction_stack = []
178
+ sessions_available.each { |s| future { s.release! } }
179
+ sessions_in_use.each { |s| future { s.release! } }
180
+ @sessions_available = []
181
+ @sessions_in_use = []
243
182
  end
244
183
  # shutdown existing thread pool
245
184
  @thread_pool.shutdown
@@ -261,19 +200,14 @@ module Google
261
200
 
262
201
  @mutex.synchronize do
263
202
  @new_sessions_in_process -= 1
264
- all_sessions << session
265
203
  end
266
204
 
267
205
  session
268
206
  end
269
207
 
270
- def new_transaction!
271
- new_session!.create_transaction
272
- end
273
-
274
208
  def can_allocate_more_sessions?
275
209
  # This is expected to be called from within a synchronize block
276
- all_sessions.size + @new_sessions_in_process < @max
210
+ sessions_available.size + sessions_in_use.size + @new_sessions_in_process < @max
277
211
  end
278
212
 
279
213
  def create_keepalive_task!
@@ -515,17 +515,14 @@ module Google
515
515
  # before an attempt is made to prevent the idle sessions from being
516
516
  # closed by the Cloud Spanner service. The default is 1800 (30
517
517
  # minutes).
518
- # * `:write_ratio` (Float) The ratio of sessions with pre-allocated
519
- # transactions to those without. Pre-allocating transactions
520
- # improves the performance of writes made by the client. The higher
521
- # the value, the more transactions are pre-allocated. The value must
522
- # be >= 0 and <= 1. The default is 0.3.
523
518
  # * `:fail` (true/false) When `true` the client raises a
524
519
  # {SessionLimitError} when the client has allocated the `max` number
525
520
  # of sessions. When `false` the client blocks until a session
526
521
  # becomes available. The default is `true`.
527
522
  # * `:threads` (Integer) The number of threads in the thread pool. The
528
523
  # default is twice the number of available CPUs.
524
+ # * `:write_ratio` (Float) Deprecated. This field is no longer needed
525
+ # and will be removed in a future release.
529
526
  # @param [Hash] labels The labels to be applied to all sessions
530
527
  # created by the client. Cloud Labels are a flexible and lightweight
531
528
  # mechanism for organizing cloud resources into groups that reflect a
@@ -674,8 +671,7 @@ module Google
674
671
  def valid_session_pool_options opts = {}
675
672
  {
676
673
  min: opts[:min], max: opts[:max], keepalive: opts[:keepalive],
677
- write_ratio: opts[:write_ratio], fail: opts[:fail],
678
- threads: opts[:threads]
674
+ fail: opts[:fail], threads: opts[:threads]
679
675
  }.compact
680
676
  end
681
677
  end
@@ -41,6 +41,11 @@ module Google
41
41
  # end
42
42
  #
43
43
  class Results
44
+ ##
45
+ # @private Object of type
46
+ # Google::Cloud::Spanner::V1::ResultSetMetadata
47
+ attr_reader :metadata
48
+
44
49
  ##
45
50
  # The read timestamp chosen for single-use snapshots (read-only
46
51
  # transactions).
@@ -73,6 +78,13 @@ module Google
73
78
  @fields ||= Fields.from_grpc @metadata.row_type.fields
74
79
  end
75
80
 
81
+ ##
82
+ # @private
83
+ # Returns a transaction if available
84
+ def transaction
85
+ @metadata&.transaction
86
+ end
87
+
76
88
  # rubocop:disable all
77
89
 
78
90
  ##
@@ -297,49 +309,20 @@ module Google
297
309
  end
298
310
 
299
311
  # @private
300
-
301
- def self.execute_query service, session_path, sql, params: nil,
302
- types: nil, transaction: nil,
303
- partition_token: nil, seqno: nil,
304
- query_options: nil, request_options: nil,
305
- call_options: nil, data_boost_enabled: nil
306
- execute_query_options = {
307
- transaction: transaction, params: params, types: types,
308
- partition_token: partition_token, seqno: seqno,
309
- query_options: query_options, request_options: request_options,
310
- call_options: call_options
311
- }
312
- execute_query_options[:data_boost_enabled] = data_boost_enabled unless data_boost_enabled.nil?
313
- enum = service.execute_streaming_sql session_path, sql,
314
- **execute_query_options
315
- from_enum(enum, service).tap do |results|
312
+ def self.from_execute_query_response response, service, session_path, sql, execute_query_options
313
+ from_enum(response, service).tap do |results|
316
314
  results.instance_variable_set :@session_path, session_path
317
- results.instance_variable_set :@sql, sql
318
- results.instance_variable_set :@execute_query_options,
319
- execute_query_options
315
+ results.instance_variable_set :@sql, sql
316
+ results.instance_variable_set :@execute_query_options, execute_query_options
320
317
  end
321
318
  end
322
319
 
323
320
  # @private
324
- def self.read service, session_path, table, columns, keys: nil,
325
- index: nil, limit: nil, transaction: nil,
326
- partition_token: nil, request_options: nil,
327
- call_options: nil, data_boost_enabled: nil
328
- read_options = {
329
- keys: keys, index: index, limit: limit,
330
- transaction: transaction,
331
- partition_token: partition_token,
332
- request_options: request_options,
333
- call_options: call_options,
334
- data_boost_enabled: data_boost_enabled
335
- }
336
- read_options[:data_boost_enabled] = data_boost_enabled unless data_boost_enabled.nil?
337
- enum = service.streaming_read_table \
338
- session_path, table, columns, **read_options
339
- from_enum(enum, service).tap do |results|
321
+ def self.from_read_response response, service, session_path, table, columns, read_options
322
+ from_enum(response, service).tap do |results|
340
323
  results.instance_variable_set :@session_path, session_path
341
- results.instance_variable_set :@table, table
342
- results.instance_variable_set :@columns, columns
324
+ results.instance_variable_set :@table, table
325
+ results.instance_variable_set :@columns, columns
343
326
  results.instance_variable_set :@read_options, read_options
344
327
  end
345
328
  end
@@ -360,17 +360,7 @@ module Google
360
360
  seqno: seqno,
361
361
  request_options: request_options
362
362
  }
363
- results = service.execute_batch_dml request, opts
364
-
365
- if results.status.code.zero?
366
- results.result_sets.map { |rs| rs.stats.row_count_exact }
367
- else
368
- begin
369
- raise Google::Cloud::Error.from_error results.status
370
- rescue Google::Cloud::Error
371
- raise Google::Cloud::Spanner::BatchUpdateError.from_grpc results
372
- end
373
- end
363
+ service.execute_batch_dml request, opts
374
364
  end
375
365
 
376
366
  def streaming_read_table session_name, table_name, columns, keys: nil,
@@ -345,16 +345,18 @@ module Google
345
345
  else
346
346
  query_options = @query_options.merge query_options unless @query_options.nil?
347
347
  end
348
- results = Results.execute_query service, path, sql,
349
- params: params,
350
- types: types,
351
- transaction: transaction,
352
- partition_token: partition_token,
353
- seqno: seqno,
354
- query_options: query_options,
355
- request_options: request_options,
356
- call_options: call_options,
357
- data_boost_enabled: data_boost_enabled
348
+
349
+ execute_query_options = {
350
+ transaction: transaction, params: params, types: types,
351
+ partition_token: partition_token, seqno: seqno,
352
+ query_options: query_options, request_options: request_options,
353
+ call_options: call_options
354
+ }
355
+ execute_query_options[:data_boost_enabled] = data_boost_enabled unless data_boost_enabled.nil?
356
+
357
+ response = service.execute_streaming_sql path, sql, **execute_query_options
358
+
359
+ results = Results.from_execute_query_response response, service, path, sql, execute_query_options
358
360
  @last_updated_at = Time.now
359
361
  results
360
362
  end
@@ -500,14 +502,23 @@ module Google
500
502
  call_options: nil, data_boost_enabled: nil
501
503
  ensure_service!
502
504
 
503
- results = Results.read service, path, table, columns,
504
- keys: keys, index: index, limit: limit,
505
- transaction: transaction,
506
- partition_token: partition_token,
507
- request_options: request_options,
508
- call_options: call_options,
509
- data_boost_enabled: data_boost_enabled
505
+ read_options = {
506
+ keys: keys, index: index, limit: limit,
507
+ transaction: transaction,
508
+ partition_token: partition_token,
509
+ request_options: request_options,
510
+ call_options: call_options,
511
+ data_boost_enabled: data_boost_enabled
512
+ }
513
+ read_options[:data_boost_enabled] = data_boost_enabled unless data_boost_enabled.nil?
514
+
515
+ response = service.streaming_read_table \
516
+ path, table, columns, **read_options
517
+
518
+ results = Results.from_read_response response, service, path, table, columns, read_options
519
+
510
520
  @last_updated_at = Time.now
521
+
511
522
  results
512
523
  end
513
524
 
@@ -1173,6 +1184,14 @@ module Google
1173
1184
  Transaction.from_grpc tx_grpc, self
1174
1185
  end
1175
1186
 
1187
+ ##
1188
+ # @private
1189
+ # Creates a new transaction object without the grpc object
1190
+ # within it. Use it for inline-begin of a transaction.
1191
+ def create_empty_transaction
1192
+ Transaction.from_grpc nil, self
1193
+ end
1194
+
1176
1195
  ##
1177
1196
  # Reloads the session resource. Useful for determining if the session is
1178
1197
  # still valid on the Spanner API.
@@ -17,6 +17,7 @@ require "google/cloud/spanner/errors"
17
17
  require "google/cloud/spanner/convert"
18
18
  require "google/cloud/spanner/results"
19
19
  require "google/cloud/spanner/commit"
20
+ require "google/cloud/spanner/batch_update_results"
20
21
 
21
22
  module Google
22
23
  module Cloud
@@ -74,6 +75,9 @@ module Google
74
75
  # end
75
76
  #
76
77
  class Transaction
78
+ # @private The `Google::Cloud::Spanner::V1::Transaction` object.
79
+ attr_reader :grpc
80
+
77
81
  # @private The Session object.
78
82
  attr_accessor :session
79
83
 
@@ -83,13 +87,31 @@ module Google
83
87
  def initialize
84
88
  @commit = Commit.new
85
89
  @seqno = 0
90
+
91
+ # Mutex to enfore thread safety for transaction creation and query executions.
92
+ #
93
+ # This mutex protects two things:
94
+ # (1) the generation of sequence numbers
95
+ # (2) the creation of transactions.
96
+ #
97
+ # Specifically, @seqno is protected so it always reflects the last sequence number
98
+ # generated and provided to an operation in any thread. Any acquisition of a
99
+ # sequence number must be synchronized.
100
+ #
101
+ # Furthermore, @grpc is protected such that it either is nil if the
102
+ # transaction has not yet been created, or references the transaction
103
+ # resource if the transaction has been created. Any operation that could
104
+ # create a transaction must be synchronized, and any logic that depends on
105
+ # the state of transaction creation must also be synchronized.
106
+ @mutex = Mutex.new
86
107
  end
87
108
 
88
109
  ##
89
110
  # Identifier of the transaction results were run in.
90
111
  # @return [String] The transaction id.
91
112
  def transaction_id
92
- return nil if @grpc.nil?
113
+ return @grpc.id if existing_transaction?
114
+ safe_begin_transaction
93
115
  @grpc.id
94
116
  end
95
117
 
@@ -337,15 +359,18 @@ module Google
337
359
  request_options: nil, call_options: nil
338
360
  ensure_session!
339
361
 
340
- @seqno += 1
341
-
342
362
  params, types = Convert.to_input_params_and_types params, types
343
363
  request_options = build_request_options request_options
344
- session.execute_query sql, params: params, types: types,
345
- transaction: tx_selector, seqno: @seqno,
346
- query_options: query_options,
347
- request_options: request_options,
348
- call_options: call_options
364
+
365
+ safe_execute do |seqno|
366
+ results = session.execute_query sql, params: params, types: types,
367
+ transaction: tx_selector, seqno: seqno,
368
+ query_options: query_options,
369
+ request_options: request_options,
370
+ call_options: call_options
371
+ @grpc ||= results.transaction
372
+ results
373
+ end
349
374
  end
350
375
  alias execute execute_query
351
376
  alias query execute_query
@@ -612,12 +637,24 @@ module Google
612
637
  #
613
638
  def batch_update request_options: nil, call_options: nil, &block
614
639
  ensure_session!
615
- @seqno += 1
616
640
 
617
641
  request_options = build_request_options request_options
618
- session.batch_update tx_selector, @seqno,
619
- request_options: request_options,
620
- call_options: call_options, &block
642
+ safe_execute do |seqno|
643
+ batch_update_results = nil
644
+ begin
645
+ response = session.batch_update tx_selector, seqno,
646
+ request_options: request_options,
647
+ call_options: call_options, &block
648
+ batch_update_results = BatchUpdateResults.new response
649
+ row_counts = batch_update_results.row_counts
650
+ @grpc ||= batch_update_results.transaction
651
+ return row_counts
652
+ rescue Google::Cloud::Spanner::BatchUpdateError
653
+ @grpc ||= batch_update_results.transaction
654
+ # Re-raise after extracting transaction
655
+ raise
656
+ end
657
+ end
621
658
  end
622
659
 
623
660
  ##
@@ -686,10 +723,15 @@ module Google
686
723
  columns = Array(columns).map(&:to_s)
687
724
  keys = Convert.to_key_set keys
688
725
  request_options = build_request_options request_options
689
- session.read table, columns, keys: keys, index: index, limit: limit,
690
- transaction: tx_selector,
691
- request_options: request_options,
692
- call_options: call_options
726
+
727
+ safe_execute do
728
+ results = session.read table, columns, keys: keys, index: index, limit: limit,
729
+ transaction: tx_selector,
730
+ request_options: request_options,
731
+ call_options: call_options
732
+ @grpc ||= results.transaction
733
+ results
734
+ end
693
735
  end
694
736
 
695
737
  ##
@@ -1111,12 +1153,69 @@ module Google
1111
1153
  end
1112
1154
  end
1113
1155
 
1156
+ ##
1157
+ # @private Checks if a transaction is already created.
1158
+ def existing_transaction?
1159
+ !no_existing_transaction?
1160
+ end
1161
+
1162
+ ##
1163
+ # @private Checks if transaction is not already created.
1164
+ def no_existing_transaction?
1165
+ @grpc.nil?
1166
+ end
1167
+
1114
1168
  protected
1115
1169
 
1116
- # The TransactionSelector to be used for queries
1170
+ ##
1171
+ # @private Facilitates a thread-safe execution of an rpc
1172
+ # for inline-begin of a transaction. This method is optimised to
1173
+ # use mutexes only when necessary, while still acheiving thread-safety.
1174
+ #
1175
+ # Note: Do not use @seqno directly while using this method. Instead, use
1176
+ # the seqno variable passed to the block.
1177
+ def safe_execute
1178
+ loop do
1179
+ if existing_transaction?
1180
+ # Create a local copy of @seqno to avoid concurrent
1181
+ # operations overriding the incremented value.
1182
+ seqno = safe_next_seqno
1183
+ # If a transaction already exists, execute rpc without mutex
1184
+ return yield seqno
1185
+ end
1186
+
1187
+ @mutex.synchronize do
1188
+ next if existing_transaction?
1189
+ @seqno += 1
1190
+ return yield @seqno
1191
+ end
1192
+ end
1193
+ end
1194
+
1195
+ ##
1196
+ # Create a new transaction in a thread-safe manner.
1197
+ def safe_begin_transaction
1198
+ @mutex.synchronize do
1199
+ return if existing_transaction?
1200
+ ensure_session!
1201
+ @grpc = service.begin_transaction session.path
1202
+ end
1203
+ end
1204
+
1205
+ ##
1206
+ # @private The TransactionSelector to be used for queries. This method must
1207
+ # be called from within a synchronized block, since the value returned
1208
+ # depends on the state of @grpc field.
1209
+ #
1210
+ # This method is expected to be called from within `safe_execute()` method's block,
1211
+ # since it provides synchronization and gurantees thread safety.
1117
1212
  def tx_selector
1118
- return nil if transaction_id.nil?
1119
- V1::TransactionSelector.new id: transaction_id
1213
+ return V1::TransactionSelector.new id: transaction_id if existing_transaction?
1214
+ V1::TransactionSelector.new(
1215
+ begin: V1::TransactionOptions.new(
1216
+ read_write: V1::TransactionOptions::ReadWrite.new
1217
+ )
1218
+ )
1120
1219
  end
1121
1220
 
1122
1221
  ##
@@ -1133,6 +1232,15 @@ module Google
1133
1232
  options
1134
1233
  end
1135
1234
 
1235
+ ##
1236
+ # @private Generates the next seqno in a thread-safe manner.
1237
+ def safe_next_seqno
1238
+ @mutex.synchronize do
1239
+ @seqno += 1
1240
+ return @seqno
1241
+ end
1242
+ end
1243
+
1136
1244
  ##
1137
1245
  # @private Raise an error unless an active connection to the service is
1138
1246
  # available.
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module Spanner
19
- VERSION = "2.17.0".freeze
19
+ VERSION = "2.18.0".freeze
20
20
  end
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-cloud-spanner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.17.0
4
+ version: 2.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-06-23 00:00:00.000000000 Z
12
+ date: 2023-09-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-cloud-core
@@ -295,6 +295,7 @@ files:
295
295
  - lib/google/cloud/spanner/batch_client.rb
296
296
  - lib/google/cloud/spanner/batch_snapshot.rb
297
297
  - lib/google/cloud/spanner/batch_update.rb
298
+ - lib/google/cloud/spanner/batch_update_results.rb
298
299
  - lib/google/cloud/spanner/client.rb
299
300
  - lib/google/cloud/spanner/column_value.rb
300
301
  - lib/google/cloud/spanner/commit.rb
@@ -347,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
347
348
  - !ruby/object:Gem::Version
348
349
  version: '0'
349
350
  requirements: []
350
- rubygems_version: 3.4.2
351
+ rubygems_version: 3.4.19
351
352
  signing_key:
352
353
  specification_version: 4
353
354
  summary: API Client library for Google Cloud Spanner API