Skip to content

Commit c06eafa

Browse files
committed
Make start/clean more threadsafe
* Prevent other threads from modifying the tables we're deleting from * Used read_committed since it's the lowest level that works * Tested various isolations: * read_uncommitted - works, but PG treats it as read_committed[1] * read_committed - works * repeatable_read - skipped * serializable - works (highest isolation level) * Don't allow changes to table_max_id_cache by other threads while we're accessing it [1] https://www.postgresql.org/docs/13/transaction-iso.html "In PostgreSQL, you can request any of the four standard transaction isolation levels, but internally only three distinct isolation levels are implemented, i.e., PostgreSQL's Read Uncommitted mode behaves like Read Committed. This is because it is the only sensible way to map the standard isolation levels to PostgreSQL's multiversion concurrency control architecture."
1 parent 5b1208e commit c06eafa

File tree

1 file changed

+12
-4
lines changed

1 file changed

+12
-4
lines changed

lib/extensions/database_cleaner-activerecord-seeded_deletion.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
require 'database_cleaner-active_record'
2+
require 'thread'
23

34
module DatabaseCleaner
45
module ActiveRecord
56
# SeededDeletion is a strategy that deletes all records from tables except those that existed before it was instantiated
67
# This is useful for tests that need the seeded data to be present.
78
class SeededDeletion < Deletion
9+
# Class level mutex for thread safety around start/clean actions.
10+
@mutex = Mutex.new
11+
812
def clean
9-
connection.transaction(:requires_new => true) do
13+
# Use a transaction isolation to prevent other threads
14+
# from modifying the tables during deletion
15+
connection.transaction(:requires_new => true, :isolation => :read_committed) do
1016
super
1117
end
1218
end
@@ -17,12 +23,14 @@ def start
1723
end
1824

1925
def self.table_max_id_cache
20-
@table_max_id_cache ||= {}
26+
# wrap the cache initialization in a mutex to ensure thread safety
27+
@mutex.synchronize { @table_max_id_cache ||= {} }
2128
end
2229

23-
# Memoize the maximum ID for each class table with non-zero number of rows
2430
def self.table_max_id_cache=(table_id_hash)
25-
@table_max_id_cache ||= table_id_hash
31+
@mutex.synchronize do
32+
@table_max_id_cache = table_id_hash
33+
end
2634
end
2735

2836
delegate :table_max_id_cache, to: :class

0 commit comments

Comments
 (0)