google-cloud-spanner 2.33.0 → 2.34.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: 5432cd1029cb3077b9eee5511329297355428098e40afa349d93fb0272b2c092
4
- data.tar.gz: a70271c85a8aa155d2e66e50733c388ca7005f4ce924fb921c253e14a98b3d86
3
+ metadata.gz: d1c4886322cdaa07eb1c80b4a50e92d7e6abda79ac5eacb959680406637aea82
4
+ data.tar.gz: b1c62c5fcc2ad7ab9f91e948d6c08a6831eddf0560b35e986a4ac9d7bc208976
5
5
  SHA512:
6
- metadata.gz: 2191fcfe9aad61cca23733bbdfa78f359d10aa140e3fc77a53c3c4dafb5aa72ac99c94ae09437ab4b8080922610d709c634e4023657c706759180202143a00e1
7
- data.tar.gz: 682e7a8b1a85c680cfaad9dc4af31c426dad9c80ac19345a034531338a6201173769e161a986a5d53a3382b0fb653f21e9274610800ee931e8d3cd7d72b62c55
6
+ metadata.gz: aa9eb6272e353e42377b18c610bb143b1f2cea9d5c94313e1748d7bc401260932e9cc8546791d86d0819843350a9e1546cd648aa6df52b2f533d8949c2659fae
7
+ data.tar.gz: 87a0c66ec2881cea201af2e716c08714bd5b7abeaa44d16efee8f1d25a33cda0773fff894f6075583dd6416dfe9c1233a5fccb9e6f7329734941fa997cb878f2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Release History
2
2
 
3
+ ### 2.34.0 (2026-01-30)
4
+
5
+ #### Features
6
+
7
+ * Support request id header feature ([#215](https://github.com/googleapis/ruby-spanner/issues/215))
8
+
3
9
  ### 2.33.0 (2025-12-12)
4
10
 
5
11
  #### Features
@@ -14,6 +14,7 @@
14
14
 
15
15
 
16
16
  require "google/cloud/errors"
17
+ require "google/cloud/spanner/spanner_error"
17
18
 
18
19
  module Google
19
20
  module Cloud
@@ -0,0 +1,207 @@
1
+ # Copyright 2026 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
+ require "grpc"
17
+ require "securerandom"
18
+ require "mutex_m"
19
+ require "google/cloud/spanner/errors"
20
+
21
+ module Google
22
+ module Cloud
23
+ module Spanner
24
+ ##
25
+ # RequestIdInterceptor is a GRPC interceptor class that captures all the rpc calls
26
+ # made by the GRPC layer inserting a new Header with a specific ID for debugging purposes.
27
+ #
28
+ class RequestIdInterceptor < GRPC::ClientInterceptor
29
+ @client_id_counter = 0
30
+ @client_mutex = Mutex.new
31
+ @channel_id_counter = 0
32
+ @channel_mutex = Mutex.new
33
+ @request_id_counter = 0
34
+ @request_id_mutex = Mutex.new
35
+ @process_id = nil
36
+ @process_id_mutex = Mutex.new
37
+
38
+ # @private
39
+ # Gets the next client ID and increments it.
40
+ #
41
+ # @return [Integer]
42
+ private_class_method def self.next_client_id
43
+ @client_mutex.synchronize do
44
+ @client_id_counter += 1
45
+ end
46
+ end
47
+
48
+ # @private
49
+ # Gets the next channel ID and increments it.
50
+ #
51
+ # @return [Integer]
52
+ private_class_method def self.next_channel_id
53
+ @channel_mutex.synchronize do
54
+ @channel_id_counter += 1
55
+ end
56
+ end
57
+
58
+ # @private
59
+ # Returns a process ID for the context of the request id header.
60
+ # A process ID is a Hex encoded 64 bit value
61
+ #
62
+ # @param [String, int] process_id A 64 bit value in Hex or Integer format
63
+ # @return [String]
64
+ private_class_method def self.get_process_id process_id = nil
65
+ @process_id_mutex.synchronize do
66
+ if process_id.nil? || !@process_id.nil?
67
+ return @process_id ||= (SecureRandom.hex 8)
68
+ end
69
+
70
+ case process_id
71
+ when Integer
72
+ if process_id >= 0 && process_id.bit_length <= 64
73
+ return process_id.to_s(16).rjust(16, "0")
74
+ end
75
+ when String
76
+ if process_id =~ /\A[0-9a-fA-F]{16}\z/
77
+ return process_id
78
+ end
79
+ end
80
+
81
+ raise ArgumentError, "process_id must be a 64-bit integer or 16-character hex string"
82
+ end
83
+ end
84
+
85
+ # Initializes a request_id_interceptor instance.
86
+ #
87
+ # @param [String, int] process_id A 64 bit value in Hex or Integer format
88
+ # @return [Google::Cloud::Spanner::RequestIdInterceptor]
89
+ def initialize process_id: nil
90
+ super
91
+ @version = 1
92
+ @process_id = self.class.send :get_process_id, process_id
93
+ @client_id = self.class.send :next_client_id
94
+ @channel_id = self.class.send :next_channel_id
95
+ @request_id_counter = 0
96
+ @request_mutex = Mutex.new
97
+ end
98
+
99
+ # Intercepts a request_response rpc call
100
+ #
101
+ # @param [String] method The RPC method name
102
+ # @param [Google::Protobuf::MessageExts] request The request to be sent to the RPC call
103
+ # @param [GRPC::ActiveCall::InterceptableView] call An interceptable view object for the call class
104
+ # @param [Hash] metadata All the metadata to be sent to the RPC call
105
+ # @return [void]
106
+ def request_response method:, request:, call:, metadata:, &block
107
+ # Unused. This is to avoid Rubocop's Lint/UnusedMethodArgument
108
+ _method = method
109
+ _request = request
110
+ _call = call
111
+ update_metadata_for_call metadata, &block
112
+ end
113
+
114
+ # Intercepts a client_streamer rpc call
115
+ #
116
+ # @param [String] method The RPC method name
117
+ # @param [Google::Protobuf::MessageExts] request The request to be sent to the RPC call
118
+ # @param [GRPC::ActiveCall::InterceptableView] call An interceptable view object for the call class
119
+ # @param [Hash] metadata All the metadata to be sent to the RPC call
120
+ # @return [void]
121
+ def client_streamer method:, request:, call:, metadata:, &block
122
+ # Unused. This is to avoid Rubocop's Lint/UnusedMethodArgument
123
+ _method = method
124
+ _request = request
125
+ _call = call
126
+ update_metadata_for_call metadata, &block
127
+ end
128
+
129
+ # Intercepts a server_streamer rpc call
130
+ #
131
+ # @param [String] method The RPC method name
132
+ # @param [Google::Protobuf::MessageExts] request The request to be sent to the RPC call
133
+ # @param [GRPC::ActiveCall::InterceptableView] call An interceptable view object for the call class
134
+ # @param [Hash] metadata All the metadata to be sent to the RPC call
135
+ # @return [void]
136
+ def server_streamer method:, request:, call:, metadata:, &block
137
+ # Unused. This is to avoid Rubocop's Lint/UnusedMethodArgument
138
+ _method = method
139
+ _request = request
140
+ _call = call
141
+ update_metadata_for_call metadata, &block
142
+ end
143
+
144
+ # Intercepts a bidi_streamer rpc call
145
+ #
146
+ # @param [String] method The RPC method name
147
+ # @param [Google::Protobuf::MessageExts] request The request to be sent to the RPC call
148
+ # @param [GRPC::ActiveCall::InterceptableView] call An interceptable view object for the call class
149
+ # @param [Hash] metadata The metadata to be sent to the RPC call
150
+ # @return [void]
151
+ def bidi_streamer method:, request:, call:, metadata:, &block
152
+ # Unused. This is to avoid Rubocop's Lint/UnusedMethodArgument
153
+ _method = method
154
+ _request = request
155
+ _call = call
156
+ update_metadata_for_call metadata, &block
157
+ end
158
+
159
+ private
160
+
161
+ # @private
162
+ # Inserts the Spanner request id header to the metadata for the RPC call
163
+ #
164
+ # @param [Hash] metadata The metadata to be sent to the RPC call
165
+ # @return [void]
166
+ def update_metadata_for_call metadata
167
+ request_id = nil
168
+ attempt = 1
169
+
170
+ if metadata.include? :"x-goog-spanner-request-id"
171
+ request_id, attempt = get_header_request_id_and_attempt metadata[:"x-goog-spanner-request-id"]
172
+ else
173
+ request_id = @request_mutex.synchronize { @request_id_counter += 1 }
174
+ end
175
+
176
+ formatted_request_id = format_request_id request_id, attempt
177
+ metadata[:"x-goog-spanner-request-id"] = formatted_request_id
178
+
179
+ yield
180
+ rescue StandardError => e
181
+ e.instance_variable_set :@spanner_header_id, formatted_request_id
182
+ raise e
183
+ end
184
+
185
+ # @private
186
+ # Creates the Spanner request id header in the correct format
187
+ #
188
+ # @param [String] request_id The request id of the Spanner request
189
+ # @param [String] attempt The attempt of the current request after retries
190
+ # @return [String]
191
+ def format_request_id request_id, attempt
192
+ "#{@version}.#{@process_id}.#{@client_id}.#{@channel_id}.#{request_id}.#{attempt}"
193
+ end
194
+
195
+ # @private
196
+ # Parses a request id header and returns the request id and the attempt
197
+ #
198
+ # @param [String] header A string representation of a Spanner request ID.
199
+ # @return [array]
200
+ def get_header_request_id_and_attempt header
201
+ _, _, _, _, request_id, attempt = header.split "."
202
+ [request_id, attempt.to_i + 1]
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -36,6 +36,7 @@ module Google
36
36
  attr_accessor :lib_name
37
37
  attr_accessor :lib_version
38
38
  attr_accessor :quota_project
39
+ attr_accessor :interceptors
39
40
  attr_accessor :enable_leader_aware_routing
40
41
 
41
42
  attr_reader :universe_domain
@@ -51,13 +52,15 @@ module Google
51
52
  # @param timeout [::Numeric, nil] Optional. Timeout for Gapic client.
52
53
  # @param lib_name [::String, nil] Optional. Library name for headers.
53
54
  # @param lib_version [::String, nil] Optional. Library version for headers.
55
+ # @param interceptors [::Array<GRPC::ClientInterceptor>, nil] Optional.
56
+ # An array of interceptors that are run before calls are executed.
54
57
  # @param enable_leader_aware_routing [::Boolean, nil] Optional. Whether Leader
55
58
  # Aware Routing should be enabled.
56
59
  # @param universe_domain [::String, nil] Optional. The domain of the universe to connect to.
57
60
  # @private
58
61
  def initialize project, credentials, quota_project: nil,
59
62
  host: nil, timeout: nil, lib_name: nil, lib_version: nil,
60
- enable_leader_aware_routing: nil, universe_domain: nil
63
+ interceptors: nil, enable_leader_aware_routing: nil, universe_domain: nil
61
64
  @project = project
62
65
  @credentials = credentials
63
66
  @quota_project = quota_project || (credentials.quota_project_id if credentials.respond_to? :quota_project_id)
@@ -73,6 +76,7 @@ module Google
73
76
  @timeout = timeout
74
77
  @lib_name = lib_name
75
78
  @lib_version = lib_version
79
+ @interceptors = interceptors
76
80
  @enable_leader_aware_routing = enable_leader_aware_routing
77
81
  end
78
82
 
@@ -106,6 +110,7 @@ module Google
106
110
  config.lib_name = lib_name_with_prefix
107
111
  config.lib_version = Google::Cloud::Spanner::VERSION
108
112
  config.metadata = { "google-cloud-resource-prefix" => "projects/#{@project}" }
113
+ config.interceptors = @interceptors if @interceptors
109
114
  end
110
115
  end
111
116
  attr_accessor :mocked_service
@@ -122,6 +127,7 @@ module Google
122
127
  config.lib_name = lib_name_with_prefix
123
128
  config.lib_version = Google::Cloud::Spanner::VERSION
124
129
  config.metadata = { "google-cloud-resource-prefix" => "projects/#{@project}" }
130
+ config.interceptors = @interceptors if @interceptors
125
131
  end
126
132
  end
127
133
  attr_accessor :mocked_instances
@@ -138,6 +144,7 @@ module Google
138
144
  config.lib_name = lib_name_with_prefix
139
145
  config.lib_version = Google::Cloud::Spanner::VERSION
140
146
  config.metadata = { "google-cloud-resource-prefix" => "projects/#{@project}" }
147
+ config.interceptors = @interceptors if @interceptors
141
148
  end
142
149
  end
143
150
  attr_accessor :mocked_databases
@@ -0,0 +1,35 @@
1
+ # Copyright 2026 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
+ require "google/cloud/errors"
17
+
18
+ # This is a monkey patch for Google::Cloud::Error to add support for the request_id method
19
+ # to keep this Spanner exclusive method inside the Spanner code.
20
+ # This may be moved into the errors gem itself based on later assessment.
21
+ module Google
22
+ module Cloud
23
+ class Error
24
+ ##
25
+ # The Spanner header ID if there was an error on the request.
26
+ #
27
+ # @return [String, nil]
28
+ #
29
+ def request_id
30
+ return nil unless cause.instance_variable_defined? :@spanner_header_id
31
+ cause.instance_variable_get :@spanner_header_id
32
+ end
33
+ end
34
+ end
35
+ end
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module Spanner
19
- VERSION = "2.33.0".freeze
19
+ VERSION = "2.34.0".freeze
20
20
  end
21
21
  end
22
22
  end
@@ -15,6 +15,8 @@
15
15
 
16
16
  require "google-cloud-spanner"
17
17
  require "google/cloud/spanner/project"
18
+ require "google/cloud/spanner/spanner_error"
19
+ require "google/cloud/spanner/request_id_interceptor"
18
20
  require "google/cloud/config"
19
21
  require "google/cloud/env"
20
22
 
@@ -96,7 +98,7 @@ module Google
96
98
  def self.new project_id: nil, credentials: nil, scope: nil, timeout: nil,
97
99
  endpoint: nil, project: nil, keyfile: nil,
98
100
  emulator_host: nil, lib_name: nil, lib_version: nil,
99
- enable_leader_aware_routing: true, universe_domain: nil
101
+ enable_leader_aware_routing: true, universe_domain: nil, process_id: nil
100
102
  project_id ||= project || default_project_id
101
103
  scope ||= configure.scope
102
104
  timeout ||= configure.timeout
@@ -105,6 +107,7 @@ module Google
105
107
  credentials ||= keyfile
106
108
  lib_name ||= configure.lib_name
107
109
  lib_version ||= configure.lib_version
110
+ interceptors = [RequestIdInterceptor.new(process_id: process_id)]
108
111
  universe_domain ||= configure.universe_domain
109
112
 
110
113
  if emulator_host
@@ -127,7 +130,8 @@ module Google
127
130
  Spanner::Service.new(
128
131
  project_id, credentials, quota_project: configure.quota_project,
129
132
  host: endpoint, timeout: timeout, lib_name: lib_name,
130
- lib_version: lib_version, universe_domain: universe_domain,
133
+ lib_version: lib_version, interceptors: interceptors,
134
+ universe_domain: universe_domain,
131
135
  enable_leader_aware_routing: enable_leader_aware_routing
132
136
  ),
133
137
  query_options: configure.query_options
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.33.0
4
+ version: 2.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore
@@ -158,12 +158,14 @@ files:
158
158
  - lib/google/cloud/spanner/pool.rb
159
159
  - lib/google/cloud/spanner/project.rb
160
160
  - lib/google/cloud/spanner/range.rb
161
+ - lib/google/cloud/spanner/request_id_interceptor.rb
161
162
  - lib/google/cloud/spanner/results.rb
162
163
  - lib/google/cloud/spanner/service.rb
163
164
  - lib/google/cloud/spanner/session.rb
164
165
  - lib/google/cloud/spanner/session_cache.rb
165
166
  - lib/google/cloud/spanner/session_creation_options.rb
166
167
  - lib/google/cloud/spanner/snapshot.rb
168
+ - lib/google/cloud/spanner/spanner_error.rb
167
169
  - lib/google/cloud/spanner/status.rb
168
170
  - lib/google/cloud/spanner/transaction.rb
169
171
  - lib/google/cloud/spanner/version.rb