google-cloud-spanner 2.17.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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