Skip to content

Commit a03d44e

Browse files
committed
feat: transaction isolation level
Add support for :repeatable_read transaction isolation level. This can be used in the following ways: - By passing in a value for the :isolation argument when starting a transaction. - By setting the :isolation_level property of a connection. - By setting the :isolation_level property in the database config.
1 parent e9a830f commit a03d44e

File tree

13 files changed

+189
-39
lines changed

13 files changed

+189
-39
lines changed

.github/workflows/acceptance-tests-on-emulator.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
- name: Install dependencies
3939
run: bundle install
4040
- name: Run acceptance tests on emulator
41-
run: bundle exec rake acceptance
41+
run: bundle exec rake acceptance TESTOPTS="-v"
4242
env:
4343
SPANNER_EMULATOR_HOST: localhost:9010
4444
SPANNER_TEST_PROJECT: test-project

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ jobs:
3030
- name: Install dependencies
3131
run: bundle install
3232
- name: Run tests
33-
run: bundle exec rake test
33+
run: bundle exec rake test TESTOPTS="-v"

acceptance/test_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ def teardown
188188
unless @skip_test_table_create
189189
connection.drop_table :test_models, if_exists: true
190190
end
191+
ActiveRecord::Base.connection_pool.disconnect!
191192

192193
super
193194
end
@@ -275,6 +276,11 @@ def call(name, start, finish, message_id, values)
275276

276277
Minitest.after_run do
277278
drop_test_database
279+
ActiveRecordSpannerAdapter::Connection.spanners_map.each do |_, spanner|
280+
channel = spanner.service.service.spanner_stub.grpc_stub.instance_variable_get(:@ch)
281+
channel.close
282+
end
283+
ActiveRecordSpannerAdapter::Connection.spanners_map.clear
278284
end
279285

280286
create_test_database

activerecord-spanner-adapter.gemspec

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ Gem::Specification.new do |spec|
2424

2525
spec.required_ruby_version = ">= 3.1"
2626

27-
spec.add_dependency "google-cloud-spanner", "~> 2.18"
28-
# Pin gRPC to 1.64.3, as 1.65 and 1.66 cause test runs to hang randomly.
29-
spec.add_dependency "grpc", "1.64.3"
27+
spec.add_dependency "google-cloud-spanner", "~> 2.25"
28+
spec.add_dependency "google-cloud-spanner-v1", "~> 1.7"
3029
spec.add_runtime_dependency "activerecord", [">= 7.0", "< 9"]
3130

3231
spec.add_development_dependency "autotest-suffix", "~> 1.1"

lib/active_record/connection_adapters/spanner/database_statements.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def begin_isolated_db_transaction isolation
292292
if isolation.count != 1
293293
else
294294
raise "Unsupported isolation level: #{isolation}" unless
295-
[:serializable, :read_only, :buffered_mutations, :pdml].include? isolation
295+
[:serializable, :repeatable_read, :read_only, :buffered_mutations, :pdml].include? isolation
296296
end
297297

298298
log "BEGIN #{isolation}" do

lib/active_record/connection_adapters/spanner_adapter.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ def spanner_schema_cache
150150
end
151151

152152
# Spanner Connection API
153-
delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch, to: :@connection
153+
delegate :ddl_batch, :ddl_batch?, :start_batch_ddl, :abort_batch, :run_batch,
154+
:isolation_level, :isolation_level=, to: :@connection
154155

155156
def current_spanner_transaction
156157
@connection.current_transaction
@@ -170,6 +171,10 @@ def supports_explain?
170171
false
171172
end
172173

174+
def supports_transaction_isolation?
175+
true
176+
end
177+
173178
def supports_foreign_keys?
174179
true
175180
end

lib/activerecord_spanner_adapter/connection.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ class Connection
1414
attr_reader :database_id
1515
attr_reader :spanner
1616
attr_accessor :current_transaction
17+
attr_accessor :isolation_level
1718

1819
def initialize config
1920
@instance_id = config[:instance]
2021
@database_id = config[:database]
22+
@isolation_level = config[:isolation_level]
2123
@spanner = self.class.spanners config
2224
end
2325

@@ -38,6 +40,10 @@ def self.spanners config
3840
end
3941
end
4042

43+
def self.spanners_map
44+
@spanners
45+
end
46+
4147
# Clears the cached information about the underlying information schemas.
4248
# Call this method if you drop and recreate a database with the same name
4349
# to prevent the cached information to be used for the new database.
@@ -271,7 +277,7 @@ def create_transaction_after_failed_first_statement original_error
271277

272278
def begin_transaction isolation = nil
273279
raise "Nested transactions are not allowed" if current_transaction&.active?
274-
self.current_transaction = Transaction.new self, isolation
280+
self.current_transaction = Transaction.new self, isolation || @isolation_level
275281
current_transaction.begin
276282
current_transaction
277283
end

lib/activerecord_spanner_adapter/transaction.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ def begin
5555
when :pdml
5656
@grpc_transaction = @connection.session.create_pdml
5757
else
58+
grpc_isolation = _transaction_isolation_level_to_grpc @isolation
5859
@begin_transaction_selector = Google::Cloud::Spanner::V1::TransactionSelector.new \
5960
begin: Google::Cloud::Spanner::V1::TransactionOptions.new(
60-
read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new
61+
read_write: Google::Cloud::Spanner::V1::TransactionOptions::ReadWrite.new,
62+
isolation_level: grpc_isolation
6163
)
62-
6364
end
6465
@state = :STARTED
6566
rescue Google::Cloud::NotFoundError => e
@@ -75,6 +76,15 @@ def begin
7576
end
7677
end
7778

79+
def _transaction_isolation_level_to_grpc isolation
80+
case isolation
81+
when :serializable
82+
Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::SERIALIZABLE
83+
when :repeatable_read
84+
Google::Cloud::Spanner::V1::TransactionOptions::IsolationLevel::REPEATABLE_READ
85+
end
86+
end
87+
7888
# Forces a BeginTransaction RPC for a read/write transaction. This is used by a
7989
# connection if the first statement of a transaction failed.
8090
def force_begin_read_write

test/activerecord_spanner_interleaved_table/interleaved_tables_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ def setup
5454

5555
def teardown
5656
ActiveRecord::Base.connection_pool.disconnect!
57+
ActiveRecordSpannerAdapter::Connection.spanners_map.each do |_, spanner|
58+
channel = spanner.service.service.spanner_stub.grpc_stub.instance_variable_get(:@ch)
59+
channel.close
60+
end
61+
ActiveRecordSpannerAdapter::Connection.spanners_map.clear
5762
@server.stop
5863
@server_thread.exit
5964
super

test/activerecord_spanner_interleaved_table_version_7_1_and_higher/interleaved_tables_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ def setup
5858

5959
def teardown
6060
ActiveRecord::Base.connection_pool.disconnect!
61+
ActiveRecordSpannerAdapter::Connection.spanners_map.each do |_, spanner|
62+
channel = spanner.service.service.spanner_stub.grpc_stub.instance_variable_get(:@ch)
63+
channel.close
64+
end
65+
ActiveRecordSpannerAdapter::Connection.spanners_map.clear
6166
@server.stop
6267
@server_thread.exit
6368
super

0 commit comments

Comments
 (0)