google-cloud-spanner 2.16.1 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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