google-cloud-spanner 2.17.0 → 2.18.1

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: 86e6ca163f96c37fa118d05ecf481289318c707f1de83ffb401c20b40afacb69
4
+ data.tar.gz: 12cbaad3945956e2da31862d23d0ef66658539fc30daee432879650d2ab3d028
5
5
  SHA512:
6
- metadata.gz: 84ceadbf10e41390c371177d5fedfb846ea83e2eabd0017b94f83a08b9ded5fff51de65f64c3cd7ee52112066593a156fb944d8b895f18665f1f90aec53dbf17
7
- data.tar.gz: 226d5e9d43e373aa736ea58f0b4fe49ea846f995996f91448fbf387b2ba12e574c91c5bf313d37a0625324b725de46a3eee7092cbded4e23b1c6956a1674c48d
6
+ metadata.gz: b866a7a87beb50165b5b1593a4e92a1e0a4684f7a5e7210647aae4ecbe05fcf9f61b1309b00d37deb94e592bc98e541ea1f1104e411a32f6ed9bc4bbde1ee624
7
+ data.tar.gz: d8c40babae83b334f95e15ea242f7b1271ecf510cd19722619e759b2a3082df04400b1fc3e78f572a0708ecca76ee68a71192fab92a425d1c27f3c8d58a58aca
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Release History
2
2
 
3
+ ### 2.18.1 (2023-09-19)
4
+
5
+ #### Bug Fixes
6
+
7
+ * Use hash to track the sessions in use ([#60](https://github.com/googleapis/ruby-spanner/issues/60))
8
+
9
+ ### 2.18.0 (2023-09-05)
10
+
11
+ #### Features
12
+
13
+ * Implement "Inline Begin Transaction" ([#54](https://github.com/googleapis/ruby-spanner/issues/54))
14
+
3
15
  ### 2.17.0 (2023-06-23)
4
16
 
5
17
  #### 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,19 @@ 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
+ # @return [Array<Session>] A stack of `Session` objects.
33
+ attr_accessor :sessions_available
34
+
35
+ # @return [Hash{String => Session}] A hash with session_id as keys,
36
+ # and `Session` objects as values.
37
+ attr_accessor :sessions_in_use
35
38
 
36
39
  def initialize client, min: 10, max: 100, keepalive: 1800,
37
- write_ratio: 0.3, fail: true, threads: nil
40
+ fail: true, threads: nil
38
41
  @client = client
39
42
  @min = min
40
43
  @max = max
41
44
  @keepalive = keepalive
42
- @write_ratio = write_ratio
43
- @write_ratio = 0 if write_ratio.negative?
44
- @write_ratio = 1 if write_ratio > 1
45
45
  @fail = fail
46
46
  @threads = threads || [2, Concurrent.processor_count * 2].max
47
47
 
@@ -69,10 +69,11 @@ module Google
69
69
 
70
70
  # Use LIFO to ensure sessions are used from backend caches, which
71
71
  # 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
72
+ session = sessions_available.pop # LIFO
73
+ if session
74
+ sessions_in_use[session.session_id] = session
75
+ return session
76
+ end
76
77
 
77
78
  if can_allocate_more_sessions?
78
79
  action = :new
@@ -85,73 +86,25 @@ module Google
85
86
  end
86
87
  end
87
88
 
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"
89
+ if action == :new
90
+ session = new_session!
91
+ @mutex.synchronize do
92
+ sessions_in_use[session.session_id] = session
95
93
  end
96
-
97
- session_stack.push session
98
-
99
- @resource.signal
94
+ return session
100
95
  end
101
96
 
102
97
  nil
103
98
  end
104
99
 
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
100
+ def checkin_session session
149
101
  @mutex.synchronize do
150
- unless all_sessions.include? txn.session
102
+ unless sessions_in_use.key? session.session_id
151
103
  raise ArgumentError, "Cannot checkin session"
152
104
  end
153
105
 
154
- transaction_stack.push txn
106
+ sessions_available.push session
107
+ sessions_in_use.delete session.session_id
155
108
 
156
109
  @resource.signal
157
110
  end
@@ -182,22 +135,20 @@ module Google
182
135
  to_release = []
183
136
 
184
137
  @mutex.synchronize do
185
- available_count = session_stack.count + transaction_stack.count
138
+ available_count = sessions_available.count
186
139
  release_count = @min - available_count
187
140
  release_count = 0 if release_count.negative?
188
141
 
189
- to_keepalive += (session_stack + transaction_stack).select do |x|
142
+ to_keepalive += sessions_available.select do |x|
190
143
  x.idle_since? @keepalive
191
144
  end
192
145
 
193
- # Remove a random portion of the sessions and transactions
146
+ # Remove a random portion of the sessions
194
147
  to_release = to_keepalive.sample release_count
195
148
  to_keepalive -= to_release
196
149
 
197
150
  # Remove those to be released from circulation
198
- @all_sessions -= to_release.map(&:session)
199
- @session_stack -= to_release
200
- @transaction_stack -= to_release
151
+ @sessions_available -= to_release
201
152
  end
202
153
 
203
154
  to_release.each { |x| future { x.release! } }
@@ -212,19 +163,11 @@ module Google
212
163
  max_threads: @threads
213
164
  # init the stacks
214
165
  @new_sessions_in_process = 0
215
- @transaction_stack = []
216
166
  # init the keepalive task
217
167
  create_keepalive_task!
218
168
  # 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
169
+ @sessions_available = @client.batch_create_new_sessions @min
170
+ @sessions_in_use = {}
228
171
  end
229
172
 
230
173
  def shutdown
@@ -236,10 +179,10 @@ module Google
236
179
  @resource.broadcast
237
180
  # Delete all sessions
238
181
  @mutex.synchronize do
239
- @all_sessions.each { |s| future { s.release! } }
240
- @all_sessions = []
241
- @session_stack = []
242
- @transaction_stack = []
182
+ sessions_available.each { |s| future { s.release! } }
183
+ sessions_in_use.each_value { |s| future { s.release! } }
184
+ @sessions_available = []
185
+ @sessions_in_use = {}
243
186
  end
244
187
  # shutdown existing thread pool
245
188
  @thread_pool.shutdown
@@ -261,19 +204,14 @@ module Google
261
204
 
262
205
  @mutex.synchronize do
263
206
  @new_sessions_in_process -= 1
264
- all_sessions << session
265
207
  end
266
208
 
267
209
  session
268
210
  end
269
211
 
270
- def new_transaction!
271
- new_session!.create_transaction
272
- end
273
-
274
212
  def can_allocate_more_sessions?
275
213
  # This is expected to be called from within a synchronize block
276
- all_sessions.size + @new_sessions_in_process < @max
214
+ sessions_available.size + sessions_in_use.size + @new_sessions_in_process < @max
277
215
  end
278
216
 
279
217
  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,16 @@ 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
+ response = session.batch_update tx_selector, seqno,
644
+ request_options: request_options,
645
+ call_options: call_options, &block
646
+ batch_update_results = BatchUpdateResults.new response
647
+ @grpc ||= batch_update_results.transaction
648
+ batch_update_results.row_counts
649
+ end
621
650
  end
622
651
 
623
652
  ##
@@ -686,10 +715,15 @@ module Google
686
715
  columns = Array(columns).map(&:to_s)
687
716
  keys = Convert.to_key_set keys
688
717
  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
718
+
719
+ safe_execute do
720
+ results = session.read table, columns, keys: keys, index: index, limit: limit,
721
+ transaction: tx_selector,
722
+ request_options: request_options,
723
+ call_options: call_options
724
+ @grpc ||= results.transaction
725
+ results
726
+ end
693
727
  end
694
728
 
695
729
  ##
@@ -1111,12 +1145,69 @@ module Google
1111
1145
  end
1112
1146
  end
1113
1147
 
1148
+ ##
1149
+ # @private Checks if a transaction is already created.
1150
+ def existing_transaction?
1151
+ !no_existing_transaction?
1152
+ end
1153
+
1154
+ ##
1155
+ # @private Checks if transaction is not already created.
1156
+ def no_existing_transaction?
1157
+ @grpc.nil?
1158
+ end
1159
+
1114
1160
  protected
1115
1161
 
1116
- # The TransactionSelector to be used for queries
1162
+ ##
1163
+ # @private Facilitates a thread-safe execution of an rpc
1164
+ # for inline-begin of a transaction. This method is optimised to
1165
+ # use mutexes only when necessary, while still acheiving thread-safety.
1166
+ #
1167
+ # Note: Do not use @seqno directly while using this method. Instead, use
1168
+ # the seqno variable passed to the block.
1169
+ def safe_execute
1170
+ loop do
1171
+ if existing_transaction?
1172
+ # Create a local copy of @seqno to avoid concurrent
1173
+ # operations overriding the incremented value.
1174
+ seqno = safe_next_seqno
1175
+ # If a transaction already exists, execute rpc without mutex
1176
+ return yield seqno
1177
+ end
1178
+
1179
+ @mutex.synchronize do
1180
+ next if existing_transaction?
1181
+ @seqno += 1
1182
+ return yield @seqno
1183
+ end
1184
+ end
1185
+ end
1186
+
1187
+ ##
1188
+ # Create a new transaction in a thread-safe manner.
1189
+ def safe_begin_transaction
1190
+ @mutex.synchronize do
1191
+ return if existing_transaction?
1192
+ ensure_session!
1193
+ @grpc = service.begin_transaction session.path
1194
+ end
1195
+ end
1196
+
1197
+ ##
1198
+ # @private The TransactionSelector to be used for queries. This method must
1199
+ # be called from within a synchronized block, since the value returned
1200
+ # depends on the state of @grpc field.
1201
+ #
1202
+ # This method is expected to be called from within `safe_execute()` method's block,
1203
+ # since it provides synchronization and gurantees thread safety.
1117
1204
  def tx_selector
1118
- return nil if transaction_id.nil?
1119
- V1::TransactionSelector.new id: transaction_id
1205
+ return V1::TransactionSelector.new id: transaction_id if existing_transaction?
1206
+ V1::TransactionSelector.new(
1207
+ begin: V1::TransactionOptions.new(
1208
+ read_write: V1::TransactionOptions::ReadWrite.new
1209
+ )
1210
+ )
1120
1211
  end
1121
1212
 
1122
1213
  ##
@@ -1133,6 +1224,15 @@ module Google
1133
1224
  options
1134
1225
  end
1135
1226
 
1227
+ ##
1228
+ # @private Generates the next seqno in a thread-safe manner.
1229
+ def safe_next_seqno
1230
+ @mutex.synchronize do
1231
+ @seqno += 1
1232
+ return @seqno
1233
+ end
1234
+ end
1235
+
1136
1236
  ##
1137
1237
  # @private Raise an error unless an active connection to the service is
1138
1238
  # 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.1".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.1
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-19 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