google-cloud-spanner 2.16.1 → 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: eb26c1ee1cc71cf984fecb01463a35384ee50e68cafd41399c064cc810a298f8
4
- data.tar.gz: 5d663434c4a3766acf13c3a03ef851cb81ec839a83bfd4e83340b9b5b22231f4
3
+ metadata.gz: 37f9332aedaf9d53b1eb13596dafa4b3571393449b85ce5e63750b5c57381e3f
4
+ data.tar.gz: b137394c7819e4795a98727e626772ece2404ef40bc7ef84153014768ddbfb89
5
5
  SHA512:
6
- metadata.gz: f8638c29b54887f38252cc916f077d4ea5776602d331c0da853215e80b62b06fe176d6a3972b339cb650a3e936c78bab2ea2776fb0f12cc4911fd57f4c1ae33d
7
- data.tar.gz: 7bd143477dbc7b67dde5f9a9cd2a6ae6b77a5b9079117bcae3cbf3006321a3476468d79889c7bb0ffe8cca6c9de4674c7d36f8bc878430bdf6934c2b6064f645
6
+ metadata.gz: 0ea41265627415904e066549390d59ad72a2be577c3b6f7ac67d8a47197e4f2df4a79f9c2c14735bcd383989b2f3bfa602fe68e3ebde1d7cc055252d8644f36f
7
+ data.tar.gz: 2cc9bf3bbe521e85c72ad2dd255eedf09225c0bbcc67bc4c319a4036aa7beac01c18f73a0fb6ff1fd203b96b15ff2b3bcbb82f2f8ddfd0f2aa72e1d2f31f03e9
data/AUTHENTICATION.md CHANGED
@@ -32,7 +32,7 @@ client = Google::Cloud::Spanner.new
32
32
  ## Project and Credential Lookup
33
33
 
34
34
  The google-cloud-spanner library aims to make authentication
35
- as simple as possible, and provides several mechanisms to configure your system
35
+ as simple as possible and provides several mechanisms to configure your system
36
36
  without providing **Project ID** and **Service Account Credentials** directly in
37
37
  code.
38
38
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ### 2.17.0 (2023-06-23)
10
+
11
+ #### Features
12
+
13
+ * support data_boost_enabled for partitioned query and read ([#46](https://github.com/googleapis/ruby-spanner/issues/46))
14
+
3
15
  ### 2.16.1 (2022-11-02)
4
16
 
5
17
  #### Documentation
@@ -188,6 +188,9 @@ module Google
188
188
  # * `:multiplier` (`Numeric`) - The incremental backoff multiplier.
189
189
  # * `:retry_codes` (`Array<String>`) - The error codes that should
190
190
  # trigger a retry.
191
+ # @param [Boolean] data_boost_enabled If this field is
192
+ # set `true`, the request will be executed via offline access.
193
+ # Defaults to `false`.
191
194
  #
192
195
  # @return [Array<Google::Cloud::Spanner::Partition>] The partitions
193
196
  # created by the query partition.
@@ -211,7 +214,7 @@ module Google
211
214
  #
212
215
  def partition_query sql, params: nil, types: nil,
213
216
  partition_size_bytes: nil, max_partitions: nil,
214
- query_options: nil, call_options: nil
217
+ query_options: nil, call_options: nil, data_boost_enabled: false
215
218
  ensure_session!
216
219
 
217
220
  params, types = Convert.to_input_params_and_types params, types
@@ -231,8 +234,9 @@ module Google
231
234
  param_types: types,
232
235
  transaction: tx_selector,
233
236
  partition_token: grpc.partition_token,
234
- query_options: query_options
235
- }.delete_if { |_, v| v.nil? }
237
+ query_options: query_options,
238
+ data_boost_enabled: data_boost_enabled
239
+ }.compact
236
240
  )
237
241
  Partition.from_execute_sql_grpc execute_sql_grpc
238
242
  end
@@ -277,6 +281,9 @@ module Google
277
281
  # * `:multiplier` (`Numeric`) - The incremental backoff multiplier.
278
282
  # * `:retry_codes` (`Array<String>`) - The error codes that should
279
283
  # trigger a retry.
284
+ # @param [Boolean] data_boost_enabled If this field is
285
+ # set `true`, the request will be executed via offline access.
286
+ # Defaults to `false`.
280
287
  #
281
288
  # @return [Array<Google::Cloud::Spanner::Partition>] The partitions
282
289
  # created by the read partition.
@@ -298,7 +305,7 @@ module Google
298
305
  #
299
306
  def partition_read table, columns, keys: nil, index: nil,
300
307
  partition_size_bytes: nil, max_partitions: nil,
301
- call_options: nil
308
+ call_options: nil, data_boost_enabled: false
302
309
  ensure_session!
303
310
 
304
311
  columns = Array(columns).map(&:to_s)
@@ -321,8 +328,9 @@ module Google
321
328
  key_set: keys,
322
329
  index: index,
323
330
  transaction: tx_selector,
324
- partition_token: grpc.partition_token
325
- }.delete_if { |_, v| v.nil? }
331
+ partition_token: grpc.partition_token,
332
+ data_boost_enabled: data_boost_enabled
333
+ }.compact
326
334
  )
327
335
  Partition.from_read_grpc read_grpc
328
336
  end
@@ -804,7 +812,8 @@ module Google
804
812
  transaction: partition.execute.transaction,
805
813
  partition_token: partition.execute.partition_token,
806
814
  query_options: query_options,
807
- call_options: call_options
815
+ call_options: call_options,
816
+ data_boost_enabled: partition.execute.data_boost_enabled
808
817
  end
809
818
 
810
819
  def execute_partition_read partition, call_options: nil
@@ -814,7 +823,8 @@ module Google
814
823
  index: partition.read.index,
815
824
  transaction: partition.read.transaction,
816
825
  partition_token: partition.read.partition_token,
817
- call_options: call_options
826
+ call_options: call_options,
827
+ data_boost_enabled: partition.read.data_boost_enabled
818
828
  end
819
829
  end
820
830
  end
@@ -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,44 +1804,44 @@ 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
1816
1824
  resp = CommitResponse.from_grpc commit_resp
1817
1825
  commit_options ? resp : resp.timestamp
1818
- rescue GRPC::Aborted, Google::Cloud::AbortedError => e
1819
- # Re-raise if deadline has passed
1820
- if current_time - start_time > deadline
1821
- if e.is_a? GRPC::BadStatus
1822
- e = Google::Cloud::Error.from_error e
1823
- end
1824
- raise e
1825
- end
1826
+ rescue GRPC::Aborted,
1827
+ Google::Cloud::AbortedError,
1828
+ GRPC::Internal,
1829
+ Google::Cloud::InternalError => e
1830
+ check_and_propagate_err! e, (current_time - start_time > deadline)
1826
1831
  # Sleep the amount from RetryDelay, or incremental backoff
1827
1832
  sleep(delay_from_aborted(e) || backoff *= 1.3)
1828
1833
  # Create new transaction on the session and retry the block
1829
- tx = tx.session.create_transaction
1834
+ tx = session.create_transaction
1830
1835
  retry
1831
1836
  rescue StandardError => e
1832
1837
  # Rollback transaction when handling unexpected error
1833
- tx.session.rollback tx.transaction_id
1838
+ tx.session.rollback tx.transaction_id if tx.existing_transaction?
1834
1839
  # Return nil if raised with rollback.
1835
1840
  return nil if e.is_a? Rollback
1836
1841
  # Re-raise error.
1837
1842
  raise e
1838
1843
  ensure
1839
- Thread.current[:transaction_id] = nil
1844
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = nil
1840
1845
  end
1841
1846
  end
1842
1847
  end
@@ -1919,7 +1924,7 @@ module Google
1919
1924
  exact_staleness: exact_staleness
1920
1925
 
1921
1926
  ensure_service!
1922
- unless Thread.current[:transaction_id].nil?
1927
+ unless Thread.current[IS_TRANSACTION_RUNNING_KEY].nil?
1923
1928
  raise "Nested snapshots are not allowed"
1924
1929
  end
1925
1930
 
@@ -1929,11 +1934,11 @@ module Google
1929
1934
  timestamp: (timestamp || read_timestamp),
1930
1935
  staleness: (staleness || exact_staleness),
1931
1936
  call_options: call_options
1932
- Thread.current[:transaction_id] = snp_grpc.id
1937
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = true
1933
1938
  snp = Snapshot.from_grpc snp_grpc, session
1934
1939
  yield snp if block_given?
1935
1940
  ensure
1936
- Thread.current[:transaction_id] = nil
1941
+ Thread.current[IS_TRANSACTION_RUNNING_KEY] = nil
1937
1942
  end
1938
1943
  nil
1939
1944
  end
@@ -2215,7 +2220,7 @@ module Google
2215
2220
  min_read_timestamp: bounded_timestamp,
2216
2221
  max_staleness: bounded_staleness,
2217
2222
  return_read_timestamp: true
2218
- }.delete_if { |_, v| v.nil? })))
2223
+ }.compact)))
2219
2224
  end
2220
2225
 
2221
2226
  def pdml_transaction session
@@ -2267,6 +2272,24 @@ module Google
2267
2272
  # Any error indicates the backoff should be handled elsewhere
2268
2273
  nil
2269
2274
  end
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
+
2288
+ def internal_error_and_not_retryable? error
2289
+ (error.instance_of?(Google::Cloud::InternalError) ||
2290
+ error.instance_of?(GRPC::Internal)) &&
2291
+ !@project.service.retryable?(error)
2292
+ end
2270
2293
  end
2271
2294
  end
2272
2295
  end
@@ -94,7 +94,7 @@ module Google
94
94
  def upsert table, *rows
95
95
  rows = Array(rows).flatten
96
96
  return rows if rows.empty?
97
- rows.delete_if(&:nil?)
97
+ rows.compact
98
98
  rows.delete_if(&:empty?)
99
99
  @mutations += rows.map do |row|
100
100
  V1::Mutation.new(
@@ -153,7 +153,7 @@ module Google
153
153
  def insert table, *rows
154
154
  rows = Array(rows).flatten
155
155
  return rows if rows.empty?
156
- rows.delete_if(&:nil?)
156
+ rows.compact
157
157
  rows.delete_if(&:empty?)
158
158
  @mutations += rows.map do |row|
159
159
  V1::Mutation.new(
@@ -211,7 +211,7 @@ module Google
211
211
  def update table, *rows
212
212
  rows = Array(rows).flatten
213
213
  return rows if rows.empty?
214
- rows.delete_if(&:nil?)
214
+ rows.compact
215
215
  rows.delete_if(&:empty?)
216
216
  @mutations += rows.map do |row|
217
217
  V1::Mutation.new(
@@ -271,7 +271,7 @@ module Google
271
271
  def replace table, *rows
272
272
  rows = Array(rows).flatten
273
273
  return rows if rows.empty?
274
- rows.delete_if(&:nil?)
274
+ rows.compact
275
275
  rows.delete_if(&:empty?)
276
276
  @mutations += rows.map do |row|
277
277
  V1::Mutation.new(
@@ -29,26 +29,22 @@ module Google
29
29
  # {Google::Cloud::Spanner::Session} instances.
30
30
  #
31
31
  class Pool
32
- attr_accessor :all_sessions
33
- attr_accessor :session_queue
34
- attr_accessor :transaction_queue
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
 
48
44
  @mutex = Mutex.new
49
45
  @resource = ConditionVariable.new
50
46
 
51
- # initialize pool and availability queue
47
+ # initialize pool and availability stack
52
48
  init
53
49
  end
54
50
 
@@ -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_queue.pop # LIFO
73
- return read_session if read_session
74
- write_transaction = transaction_queue.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_queue.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_queue.pop # LIFO
125
- return write_transaction if write_transaction
126
- read_session = session_queue.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_queue.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_queue.count + transaction_queue.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_queue + transaction_queue).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_queue -= to_release
200
- @transaction_queue -= to_release
147
+ @sessions_available -= to_release
201
148
  end
202
149
 
203
150
  to_release.each { |x| future { x.release! } }
@@ -210,21 +157,13 @@ module Google
210
157
  # init the thread pool
211
158
  @thread_pool = Concurrent::ThreadPoolExecutor.new \
212
159
  max_threads: @threads
213
- # init the queues
160
+ # init the stacks
214
161
  @new_sessions_in_process = 0
215
- @transaction_queue = []
216
162
  # init the keepalive task
217
163
  create_keepalive_task!
218
- # init session queue
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 queue
224
- pending_transactions.each do |transaction|
225
- future { checkin_transaction transaction.create_transaction }
226
- end
227
- @session_queue = sessions
164
+ # init session stack
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_queue = []
242
- @transaction_queue = []
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,9 +671,8 @@ 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]
679
- }.delete_if { |_k, v| v.nil? }
674
+ fail: opts[:fail], threads: opts[:threads]
675
+ }.compact
680
676
  end
681
677
  end
682
678
  end
@@ -41,8 +41,11 @@ module Google
41
41
  # end
42
42
  #
43
43
  class Results
44
- RST_STREAM_INTERNAL_ERROR = "Received RST_STREAM".freeze
45
- EOS_INTERNAL_ERROR = "Received unexpected EOS on DATA frame from server".freeze
44
+ ##
45
+ # @private Object of type
46
+ # Google::Cloud::Spanner::V1::ResultSetMetadata
47
+ attr_reader :metadata
48
+
46
49
  ##
47
50
  # The read timestamp chosen for single-use snapshots (read-only
48
51
  # transactions).
@@ -75,6 +78,13 @@ module Google
75
78
  @fields ||= Fields.from_grpc @metadata.row_type.fields
76
79
  end
77
80
 
81
+ ##
82
+ # @private
83
+ # Returns a transaction if available
84
+ def transaction
85
+ @metadata&.transaction
86
+ end
87
+
78
88
  # rubocop:disable all
79
89
 
80
90
  ##
@@ -177,7 +187,7 @@ module Google
177
187
 
178
188
  if resumable?(resume_token)
179
189
  should_resume_request = true
180
- elsif retryable?(err)
190
+ elsif @service.retryable?(err)
181
191
  should_retry_request = true
182
192
  elsif err.is_a?(Google::Cloud::Error)
183
193
  raise err
@@ -227,22 +237,6 @@ module Google
227
237
  resume_token && !resume_token.empty?
228
238
  end
229
239
 
230
- ##
231
- # @private
232
- # Checks if a request can be retried. This is based on the error returned.
233
- # Retryable errors are:
234
- # - Unavailable error
235
- # - Internal EOS error
236
- # - Internal RST_STREAM error
237
- def retryable? err
238
- err.instance_of?(Google::Cloud::UnavailableError) ||
239
- err.instance_of?(GRPC::Unavailable) ||
240
- (err.instance_of?(Google::Cloud::InternalError) && err.message.include?(EOS_INTERNAL_ERROR)) ||
241
- (err.instance_of?(GRPC::Internal) && err.details.include?(EOS_INTERNAL_ERROR)) ||
242
- (err.instance_of?(Google::Cloud::InternalError) && err.message.include?(RST_STREAM_INTERNAL_ERROR)) ||
243
- (err.instance_of?(GRPC::Internal) && err.details.include?(RST_STREAM_INTERNAL_ERROR))
244
- end
245
-
246
240
  ##
247
241
  # @private
248
242
  # Resumes a request, by re-executing it with a resume token.
@@ -315,46 +309,20 @@ module Google
315
309
  end
316
310
 
317
311
  # @private
318
-
319
- def self.execute_query service, session_path, sql, params: nil,
320
- types: nil, transaction: nil,
321
- partition_token: nil, seqno: nil,
322
- query_options: nil, request_options: nil,
323
- call_options: nil
324
- execute_query_options = {
325
- transaction: transaction, params: params, types: types,
326
- partition_token: partition_token, seqno: seqno,
327
- query_options: query_options, request_options: request_options,
328
- call_options: call_options
329
- }
330
- enum = service.execute_streaming_sql session_path, sql,
331
- **execute_query_options
332
- 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|
333
314
  results.instance_variable_set :@session_path, session_path
334
- results.instance_variable_set :@sql, sql
335
- results.instance_variable_set :@execute_query_options,
336
- execute_query_options
315
+ results.instance_variable_set :@sql, sql
316
+ results.instance_variable_set :@execute_query_options, execute_query_options
337
317
  end
338
318
  end
339
319
 
340
320
  # @private
341
- def self.read service, session_path, table, columns, keys: nil,
342
- index: nil, limit: nil, transaction: nil,
343
- partition_token: nil, request_options: nil,
344
- call_options: nil
345
- read_options = {
346
- keys: keys, index: index, limit: limit,
347
- transaction: transaction,
348
- partition_token: partition_token,
349
- request_options: request_options,
350
- call_options: call_options
351
- }
352
- enum = service.streaming_read_table \
353
- session_path, table, columns, **read_options
354
- 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|
355
323
  results.instance_variable_set :@session_path, session_path
356
- results.instance_variable_set :@table, table
357
- results.instance_variable_set :@columns, columns
324
+ results.instance_variable_set :@table, table
325
+ results.instance_variable_set :@columns, columns
358
326
  results.instance_variable_set :@read_options, read_options
359
327
  end
360
328
  end
@@ -36,6 +36,9 @@ module Google
36
36
  attr_accessor :lib_version
37
37
  attr_accessor :quota_project
38
38
 
39
+ RST_STREAM_INTERNAL_ERROR = "Received RST_STREAM".freeze
40
+ EOS_INTERNAL_ERROR = "Received unexpected EOS on DATA frame from server".freeze
41
+
39
42
  ##
40
43
  # Creates a new Service instance.
41
44
  def initialize project, credentials, quota_project: nil,
@@ -141,7 +144,7 @@ module Google
141
144
  display_name: name, config: instance_config_path(config),
142
145
  node_count: nodes, processing_units: processing_units,
143
146
  labels: labels
144
- }.delete_if { |_, v| v.nil? })
147
+ }.compact)
145
148
 
146
149
  request = {
147
150
  parent: project_path,
@@ -326,7 +329,7 @@ module Google
326
329
  params: nil, types: nil, resume_token: nil,
327
330
  partition_token: nil, seqno: nil,
328
331
  query_options: nil, request_options: nil,
329
- call_options: nil
332
+ call_options: nil, data_boost_enabled: nil
330
333
  opts = default_options session_name: session_name,
331
334
  call_options: call_options
332
335
  request = {
@@ -341,6 +344,7 @@ module Google
341
344
  query_options: query_options,
342
345
  request_options: request_options
343
346
  }
347
+ request[:data_boost_enabled] = data_boost_enabled unless data_boost_enabled.nil?
344
348
  service.execute_streaming_sql request, opts
345
349
  end
346
350
 
@@ -356,23 +360,14 @@ module Google
356
360
  seqno: seqno,
357
361
  request_options: request_options
358
362
  }
359
- results = service.execute_batch_dml request, opts
360
-
361
- if results.status.code.zero?
362
- results.result_sets.map { |rs| rs.stats.row_count_exact }
363
- else
364
- begin
365
- raise Google::Cloud::Error.from_error results.status
366
- rescue Google::Cloud::Error
367
- raise Google::Cloud::Spanner::BatchUpdateError.from_grpc results
368
- end
369
- end
363
+ service.execute_batch_dml request, opts
370
364
  end
371
365
 
372
366
  def streaming_read_table session_name, table_name, columns, keys: nil,
373
367
  index: nil, transaction: nil, limit: nil,
374
368
  resume_token: nil, partition_token: nil,
375
- request_options: nil, call_options: nil
369
+ request_options: nil, call_options: nil,
370
+ data_boost_enabled: nil
376
371
  opts = default_options session_name: session_name,
377
372
  call_options: call_options
378
373
  request = {
@@ -381,6 +376,7 @@ module Google
381
376
  limit: limit, resume_token: resume_token,
382
377
  partition_token: partition_token, request_options: request_options
383
378
  }
379
+ request[:data_boost_enabled] = data_boost_enabled unless data_boost_enabled.nil?
384
380
  service.streaming_read request, opts
385
381
  end
386
382
 
@@ -470,7 +466,7 @@ module Google
470
466
  read_timestamp: Convert.time_to_timestamp(timestamp),
471
467
  exact_staleness: Convert.number_to_duration(staleness),
472
468
  return_read_timestamp: true
473
- }.delete_if { |_, v| v.nil? }
469
+ }.compact
474
470
  )
475
471
  )
476
472
  opts = default_options session_name: session_name,
@@ -580,6 +576,21 @@ module Google
580
576
  databases.restore_database request, opts
581
577
  end
582
578
 
579
+ ##
580
+ # Checks if a request can be retried. This is based on the error returned.
581
+ # Retryable errors are:
582
+ # - Unavailable error
583
+ # - Internal EOS error
584
+ # - Internal RST_STREAM error
585
+ def retryable? err
586
+ err.instance_of?(Google::Cloud::UnavailableError) ||
587
+ err.instance_of?(GRPC::Unavailable) ||
588
+ (err.instance_of?(Google::Cloud::InternalError) && err.message.include?(EOS_INTERNAL_ERROR)) ||
589
+ (err.instance_of?(GRPC::Internal) && err.details.include?(EOS_INTERNAL_ERROR)) ||
590
+ (err.instance_of?(Google::Cloud::InternalError) && err.message.include?(RST_STREAM_INTERNAL_ERROR)) ||
591
+ (err.instance_of?(GRPC::Internal) && err.details.include?(RST_STREAM_INTERNAL_ERROR))
592
+ end
593
+
583
594
  def inspect
584
595
  "#{self.class}(#{@project})"
585
596
  end
@@ -338,22 +338,25 @@ module Google
338
338
  #
339
339
  def execute_query sql, params: nil, types: nil, transaction: nil,
340
340
  partition_token: nil, seqno: nil, query_options: nil,
341
- request_options: nil, call_options: nil
341
+ request_options: nil, call_options: nil, data_boost_enabled: nil
342
342
  ensure_service!
343
343
  if query_options.nil?
344
344
  query_options = @query_options
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
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
357
360
  @last_updated_at = Time.now
358
361
  results
359
362
  end
@@ -496,16 +499,26 @@ module Google
496
499
  #
497
500
  def read table, columns, keys: nil, index: nil, limit: nil,
498
501
  transaction: nil, partition_token: nil, request_options: nil,
499
- call_options: nil
502
+ call_options: nil, data_boost_enabled: nil
500
503
  ensure_service!
501
504
 
502
- results = Results.read service, path, table, columns,
503
- keys: keys, index: index, limit: limit,
504
- transaction: transaction,
505
- partition_token: partition_token,
506
- request_options: request_options,
507
- call_options: call_options
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
+
508
520
  @last_updated_at = Time.now
521
+
509
522
  results
510
523
  end
511
524
 
@@ -1171,6 +1184,14 @@ module Google
1171
1184
  Transaction.from_grpc tx_grpc, self
1172
1185
  end
1173
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
+
1174
1195
  ##
1175
1196
  # Reloads the session resource. Useful for determining if the session is
1176
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.16.1".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.16.1
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: 2022-11-02 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.3.14
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