with_advisory_lock 7.0.2 → 7.5.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: 7a82d92e5b44f2cf65a9af5ac2d00baacab6d47b3b47be9fc2db82bd5677e4c7
4
- data.tar.gz: 4a9b77cd24fad228812b9eff2f65079e6be7b95130a8e8d628d71525045ef902
3
+ metadata.gz: b1b30155d5acce8e77bf5fa733d0b4f257d34d3ccf4a40a3f2112314a34b83d9
4
+ data.tar.gz: 6c844c642c330044dd1e05355678fb862a5549ce9173fb794ee1d269cbe0cde9
5
5
  SHA512:
6
- metadata.gz: 77996aa445351903666828d916b8bd02ad4357496c2240ac847f937f7b3adff7af83993b10d6af8bcdb33825992dc6391cc211f97428c36b2bd102e65e5c26b2
7
- data.tar.gz: 94ec70a26f7c336f9dba44a384bc16d9d680d7495e43cb0c9fc22fee2380317f5d5263f36d3998837ca21f3e05424c7fc0fdb37ed75daf441ce534f0b4d67339
6
+ metadata.gz: bb0d2c8836c60963bc5c6dab3aaf3d7831ee1a2c8e86346970b77bf0de64779e0a5cef6a8965d89c0674a5daf0e892584d64116d9825e0e1bb9983d3e5698910
7
+ data.tar.gz: cc763d1ff02720ece882a9c9d0973818ac72f8774538c8d91848b4f0f9eac943b125fbecc01e9ca9d24a033977740ac189988e8225b878ad2010ac70a02826d7
@@ -36,16 +36,38 @@ jobs:
36
36
  MYSQL_PASSWORD: with_advisory_pass
37
37
  MYSQL_DATABASE: with_advisory_lock_test
38
38
  MYSQL_ROOT_HOST: '%'
39
+ mariadb:
40
+ image: mariadb:12
41
+ ports:
42
+ - 3306
43
+ env:
44
+ MARIADB_ROOT_PASSWORD: root
45
+ MARIADB_DATABASE: with_advisory_lock_trilogy_test
46
+ MARIADB_USER: with_advisory
47
+ MARIADB_PASSWORD: with_advisory_pass
48
+ MARIADB_ROOT_HOST: '%'
49
+ options: >-
50
+ --health-cmd "healthcheck.sh --su-mysql --connect --innodb_initialized"
51
+ --health-interval 10s
52
+ --health-timeout 5s
53
+ --health-retries 5
39
54
  strategy:
40
55
  fail-fast: false
41
56
  matrix:
42
57
  ruby:
43
58
  - '3.3'
44
59
  - '3.4'
60
+ - '4.0'
45
61
  - 'truffleruby'
46
62
  rails:
47
63
  - 7.2
48
64
  - "8.0"
65
+ - "8.1"
66
+ - "edge"
67
+ exclude:
68
+ # TruffleRuby doesn't support Rails edge yet
69
+ - ruby: 'truffleruby'
70
+ rails: "edge"
49
71
  env:
50
72
  ACTIVERECORD_VERSION: ${{ matrix.rails }}
51
73
  RAILS_ENV: test
@@ -60,10 +82,14 @@ jobs:
60
82
  bundler-cache: true
61
83
  rubygems: latest
62
84
 
85
+
63
86
  - name: Setup test databases
87
+ timeout-minutes: 5
64
88
  env:
65
89
  DATABASE_URL_PG: postgres://with_advisory:with_advisory_pass@localhost:${{ job.services.postgres.ports[5432] }}/with_advisory_lock_test
66
90
  DATABASE_URL_MYSQL: mysql2://with_advisory:with_advisory_pass@127.0.0.1:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test
91
+ # Trilogy doesn't support TruffleRuby
92
+ DATABASE_URL_TRILOGY: ${{ matrix.ruby != 'truffleruby' && format('trilogy://with_advisory:with_advisory_pass@127.0.0.1:{0}/with_advisory_lock_trilogy_test', job.services.mariadb.ports[3306]) || '' }}
67
93
  run: |
68
94
  cd test/dummy
69
95
  bundle exec rake db:test:prepare
@@ -72,5 +98,7 @@ jobs:
72
98
  env:
73
99
  DATABASE_URL_PG: postgres://with_advisory:with_advisory_pass@localhost:${{ job.services.postgres.ports[5432] }}/with_advisory_lock_test
74
100
  DATABASE_URL_MYSQL: mysql2://with_advisory:with_advisory_pass@127.0.0.1:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test
101
+ # Trilogy doesn't support TruffleRuby
102
+ DATABASE_URL_TRILOGY: ${{ matrix.ruby != 'truffleruby' && format('trilogy://with_advisory:with_advisory_pass@127.0.0.1:{0}/with_advisory_lock_trilogy_test', job.services.mariadb.ports[3306]) || '' }}
75
103
  WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
76
104
  run: bin/rails test
@@ -1 +1 @@
1
- {".":"7.0.2"}
1
+ {".":"7.5.0"}
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.4.6
1
+ ruby 4.0.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## Changelog
2
2
 
3
+ ## [7.5.0](https://github.com/ClosureTree/with_advisory_lock/compare/with_advisory_lock/v7.0.2...with_advisory_lock/v7.5.0) (2026-01-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * add blocking advisory locks with deadlock detection for PostgreSQL ([#140](https://github.com/ClosureTree/with_advisory_lock/issues/140)) ([f7f9aff](https://github.com/ClosureTree/with_advisory_lock/commit/f7f9aff545381107a632a25511e8fc08654a28b6))
9
+ * Add Trilogy adapter support with MariaDB 12.0+ ([#134](https://github.com/ClosureTree/with_advisory_lock/issues/134)) ([b7764cd](https://github.com/ClosureTree/with_advisory_lock/commit/b7764cd9432b25b37c6da9160f980da29a5cdaa6))
10
+ * bump version for new features ([9a8c4be](https://github.com/ClosureTree/with_advisory_lock/commit/9a8c4be5cf51147e60df7e5733360e3dfd8d009e))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Use monotonic clock so postgres timeouts are unaffected by system clock changes ([#141](https://github.com/ClosureTree/with_advisory_lock/issues/141)) ([929e010](https://github.com/ClosureTree/with_advisory_lock/commit/929e0103e5ffc4c66f9088942441f764de1bc650))
16
+
3
17
  ## [7.0.2](https://github.com/ClosureTree/with_advisory_lock/compare/with_advisory_lock/v7.0.1...with_advisory_lock/v7.0.2) (2025-09-20)
4
18
 
5
19
 
data/Gemfile CHANGED
@@ -11,12 +11,17 @@ gem 'benchmark'
11
11
  gem 'logger'
12
12
  gem 'ostruct'
13
13
 
14
- activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '7.2')
15
-
16
- gem 'activerecord', "~> #{activerecord_version}.0"
14
+ activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '8.1')
15
+
16
+ if activerecord_version == 'edge'
17
+ gem 'activerecord', github: 'rails/rails', branch: 'main'
18
+ gem 'railties', github: 'rails/rails', branch: 'main'
19
+ else
20
+ gem 'activerecord', "~> #{activerecord_version}.0"
21
+ gem 'railties'
22
+ end
17
23
 
18
24
  gem 'dotenv'
19
- gem 'railties'
20
25
 
21
26
  platforms :ruby do
22
27
  gem 'mysql2'
data/README.md CHANGED
@@ -48,7 +48,7 @@ will be yielded to. If the lock is currently being held, the block will not be
48
48
  called.
49
49
 
50
50
  > **Note**
51
- >
51
+ >
52
52
  > If a non-nil value is provided for `timeout_seconds`, the block will
53
53
  *not* be invoked if the lock cannot be acquired within that time-frame. In this case, `with_advisory_lock` will return `false`, while `with_advisory_lock!` will raise a `WithAdvisoryLock::FailedToAcquireLock` error.
54
54
 
@@ -72,6 +72,32 @@ to `true`.
72
72
  Note: transaction-level locks will not be reflected by `.current_advisory_lock`
73
73
  when the block has returned.
74
74
 
75
+ ### Blocking locks (PostgreSQL only)
76
+
77
+ By default, PostgreSQL advisory locks use a polling strategy with Ruby-level
78
+ retries and sleeps. Setting `blocking: true` switches to database-level blocking
79
+ locks that enable PostgreSQL's deadlock detection:
80
+
81
+ ```ruby
82
+ User.with_advisory_lock("lock_name", blocking: true, transaction: true) do
83
+ # PostgreSQL will detect circular lock waits and raise an error
84
+ # instead of sleeping forever
85
+ end
86
+ ```
87
+
88
+ **Benefits:**
89
+ - **Deadlock detection**: PostgreSQL detects circular waits and raises `PG::TRDeadlockDetected` after ~1 second (configurable via `deadlock_timeout`)
90
+ - **No polling overhead**: The database handles the wait queue instead of Ruby sleep/retry loops
91
+ - **Clean failure**: Returns `false` on deadlock instead of infinite retries
92
+
93
+ **When to use:**
94
+ - When acquiring multiple locks in your application (risk of deadlock)
95
+ - When you need PostgreSQL to detect and break circular lock dependencies
96
+ - When you want to avoid Ruby-level polling overhead
97
+
98
+ **Note:** MySQL ignores this option since `GET_LOCK` already provides native
99
+ timeout and deadlock detection via the MDL subsystem.
100
+
75
101
  ### Return values
76
102
 
77
103
  The return value of `with_advisory_lock_result` is a `WithAdvisoryLock::Result`
@@ -84,7 +110,7 @@ block, if the lock was able to be acquired and the block yielded, or `false`, if
84
110
  you provided a timeout_seconds value and the lock was not able to be acquired in
85
111
  time.
86
112
 
87
- `with_advisory_lock!` is similar to `with_advisory_lock`, but raises a `WithAdvisoryLock::FailedToAcquireLock` error if the lock was not able to be acquired in time.
113
+ `with_advisory_lock!` is similar to `with_advisory_lock`, but raises a `WithAdvisoryLock::FailedToAcquireLock` error if the lock was not able to be acquired in time.
88
114
 
89
115
  ### Testing for the current lock status
90
116
 
@@ -147,6 +173,16 @@ concurrent access to **any instance of a model**. Their coarseness means they
147
173
  aren't going to be commonly applicable, and they can be a source of
148
174
  [deadlocks](http://en.wikipedia.org/wiki/Deadlock).
149
175
 
176
+ ## Running Tests
177
+
178
+ To setup the project and run the whole test suite:
179
+
180
+ 1. Have Docker running
181
+ 2. `echo -e "DB_USER=with_advisory\nDB_PASSWORD=with_advisory_pass\nDB_NAME=with_advisory_lock_test\nDATABASE_URL_PG=postgres://\$DB_USER:\$DB_PASSWORD@localhost:5433/\$DB_NAME\nDATABASE_URL_MYSQL=mysql2://\$DB_USER:\$DB_PASSWORD@127.0.0.1:3366/\$DB_NAME" > .env`
182
+ 3. `make`
183
+
184
+ Alternatively to `make`, run `bin/rails test` to skip database and dependency setup.
185
+
150
186
  ## FAQ
151
187
 
152
188
  ### Transactions and Advisory Locks
data/bin/setup_test_db CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  require 'bundler/setup'
5
5
  require 'active_record'
6
+ require 'dotenv'
7
+ Dotenv.load
6
8
 
7
9
  # Setup PostgreSQL database
8
10
  puts 'Setting up PostgreSQL test database...'
@@ -10,9 +12,9 @@ ActiveRecord::Base.establish_connection(
10
12
  adapter: 'postgresql',
11
13
  host: 'localhost',
12
14
  port: 5433,
13
- database: 'with_advisory_lock_test',
14
- username: 'with_advisory',
15
- password: 'with_advisory_pass'
15
+ database: ENV['DB_NAME'],
16
+ username: ENV['DB_USER'],
17
+ password: ENV['DB_PASSWORD']
16
18
  )
17
19
 
18
20
  ActiveRecord::Schema.define(version: 1) do
@@ -36,9 +38,9 @@ ActiveRecord::Base.establish_connection(
36
38
  adapter: 'mysql2',
37
39
  host: '127.0.0.1',
38
40
  port: 3366,
39
- database: 'with_advisory_lock_test',
40
- username: 'with_advisory',
41
- password: 'with_advisory_pass'
41
+ database: ENV['DB_NAME'],
42
+ username: ENV['DB_USER'],
43
+ password: ENV['DB_PASSWORD']
42
44
  )
43
45
 
44
46
  ActiveRecord::Schema.define(version: 1) do
data/docker-compose.yml CHANGED
@@ -2,18 +2,28 @@ services:
2
2
  pg:
3
3
  image: postgres:17-alpine
4
4
  environment:
5
- POSTGRES_USER: with_advisory
6
- POSTGRES_PASSWORD: with_advisory_pass
5
+ POSTGRES_USER: test
6
+ POSTGRES_PASSWORD: test
7
7
  POSTGRES_DB: with_advisory_lock_test
8
8
  ports:
9
9
  - "5433:5432"
10
10
  mysql:
11
11
  image: mysql:8
12
12
  environment:
13
- MYSQL_USER: with_advisory
14
- MYSQL_PASSWORD: with_advisory_pass
13
+ MYSQL_USER: test
14
+ MYSQL_PASSWORD: test
15
15
  MYSQL_DATABASE: with_advisory_lock_test
16
16
  MYSQL_RANDOM_ROOT_PASSWORD: "yes"
17
17
  MYSQL_ROOT_HOST: '%'
18
18
  ports:
19
19
  - "3366:3306"
20
+ mariadb:
21
+ image: mariadb:12
22
+ environment:
23
+ MARIADB_USER: test
24
+ MARIADB_PASSWORD: test
25
+ MARIADB_DATABASE: with_advisory_lock_test_trilogy
26
+ MARIADB_RANDOM_ROOT_PASSWORD: "yes"
27
+ MARIADB_ROOT_HOST: '%'
28
+ ports:
29
+ - "3368:3306"
@@ -15,7 +15,7 @@ module WithAdvisoryLock
15
15
 
16
16
  def with_advisory_lock_if_needed(lock_name, options = {}, &block)
17
17
  options = { timeout_seconds: options } unless options.respond_to?(:fetch)
18
- options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache
18
+ options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache, :blocking
19
19
 
20
20
  # Validate transaction-level locks are used within a transaction
21
21
  if options.fetch(:transaction, false) && !transaction_open?
@@ -56,12 +56,14 @@ module WithAdvisoryLock
56
56
  timeout_seconds = options.fetch(:timeout_seconds, nil)
57
57
  shared = options.fetch(:shared, false)
58
58
  transaction = options.fetch(:transaction, false)
59
+ blocking = options.fetch(:blocking, false)
59
60
 
60
61
  lock_keys = lock_keys_for(lock_name)
61
62
 
62
63
  # MySQL supports database-level timeout in GET_LOCK, skip Ruby-level polling
63
- if supports_database_timeout? || timeout_seconds&.zero?
64
- yield_with_lock(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction, timeout_seconds, &)
64
+ # PostgreSQL blocking locks also skip polling and let the database handle waiting
65
+ if supports_database_timeout? || timeout_seconds&.zero? || blocking
66
+ yield_with_lock(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction, timeout_seconds, blocking, &)
65
67
  else
66
68
  yield_with_lock_and_timeout(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction,
67
69
  timeout_seconds, &)
@@ -70,9 +72,9 @@ module WithAdvisoryLock
70
72
 
71
73
  def yield_with_lock_and_timeout(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction,
72
74
  timeout_seconds, &)
73
- give_up_at = timeout_seconds ? Time.now + timeout_seconds : nil
74
- while give_up_at.nil? || Time.now < give_up_at
75
- r = yield_with_lock(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction, 0, &)
75
+ give_up_at = timeout_seconds ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout_seconds : nil
76
+ while give_up_at.nil? || Process.clock_gettime(Process::CLOCK_MONOTONIC) < give_up_at
77
+ r = yield_with_lock(lock_keys, lock_name, lock_str, lock_stack_item, shared, transaction, 0, false, &)
76
78
  return r if r.lock_was_acquired?
77
79
 
78
80
  # Randomizing sleep time may help reduce contention.
@@ -81,9 +83,9 @@ module WithAdvisoryLock
81
83
  Result.new(lock_was_acquired: false)
82
84
  end
83
85
 
84
- def yield_with_lock(lock_keys, lock_name, _lock_str, lock_stack_item, shared, transaction, timeout_seconds = nil)
86
+ def yield_with_lock(lock_keys, lock_name, _lock_str, lock_stack_item, shared, transaction, timeout_seconds = nil, blocking = false)
85
87
  if try_advisory_lock(lock_keys, lock_name: lock_name, shared: shared, transaction: transaction,
86
- timeout_seconds: timeout_seconds)
88
+ timeout_seconds: timeout_seconds, blocking: blocking)
87
89
  begin
88
90
  advisory_lock_stack.push(lock_stack_item)
89
91
  result = block_given? ? yield : nil
@@ -8,16 +8,23 @@ module WithAdvisoryLock
8
8
 
9
9
  LOCK_PREFIX_ENV = 'WITH_ADVISORY_LOCK_PREFIX'
10
10
 
11
- def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil)
11
+ def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false)
12
12
  raise ArgumentError, 'shared locks are not supported on MySQL' if shared
13
13
  raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
14
14
 
15
- # MySQL GET_LOCK supports native timeout:
16
- # - timeout_seconds = nil: wait indefinitely (-1)
15
+ # Note: blocking parameter is accepted for API compatibility but ignored for MySQL
16
+ # MySQL's GET_LOCK already provides native timeout support, making the blocking
17
+ # parameter redundant. MySQL doesn't have separate try/blocking functions like PostgreSQL.
18
+
19
+ # MySQL/MariaDB GET_LOCK supports native timeout:
20
+ # - timeout_seconds = nil: wait indefinitely
17
21
  # - timeout_seconds = 0: try once, no wait (0)
18
22
  # - timeout_seconds > 0: wait up to timeout_seconds
23
+ #
24
+ # Note: MySQL accepts -1 for infinite wait, but MariaDB does not.
25
+ # Using a large value (1 year) for cross-compatibility.
19
26
  mysql_timeout = case timeout_seconds
20
- when nil then -1
27
+ when nil then 31_536_000 # 1 year in seconds
21
28
  when 0 then 0
22
29
  else timeout_seconds.to_i
23
30
  end
@@ -10,11 +10,33 @@ module WithAdvisoryLock
10
10
  LOCK_RESULT_VALUES = ['t', true].freeze
11
11
  ERROR_MESSAGE_REGEX = / ERROR: +current transaction is aborted,/
12
12
 
13
- def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil)
13
+ def try_advisory_lock(lock_keys, lock_name:, shared:, transaction:, timeout_seconds: nil, blocking: false)
14
14
  # timeout_seconds is accepted for compatibility but ignored - PostgreSQL doesn't support
15
15
  # native timeouts with pg_try_advisory_lock, requiring Ruby-level polling instead
16
- function = advisory_try_lock_function(transaction, shared)
17
- execute_advisory(function, lock_keys, lock_name)
16
+ function = if blocking
17
+ advisory_lock_function(transaction, shared)
18
+ else
19
+ advisory_try_lock_function(transaction, shared)
20
+ end
21
+ execute_advisory(function, lock_keys, lock_name, blocking: blocking)
22
+ rescue ActiveRecord::Deadlocked
23
+ # Rails 8.2+ raises ActiveRecord::Deadlocked directly for PostgreSQL deadlocks
24
+ # When using blocking locks, treat deadlocks as lock acquisition failure
25
+ return false if blocking
26
+
27
+ raise
28
+ rescue ActiveRecord::StatementInvalid => e
29
+ # PostgreSQL deadlock detection raises PG::TRDeadlockDetected (SQLSTATE 40P01)
30
+ # When using blocking locks, treat deadlocks as lock acquisition failure.
31
+ # Rails 8.2+ may also retry after deadlock and get "current transaction is aborted"
32
+ # when the transaction was rolled back by PostgreSQL's deadlock detection.
33
+ if blocking && (e.cause.is_a?(PG::TRDeadlockDetected) ||
34
+ e.message.include?('deadlock detected') ||
35
+ e.message =~ ERROR_MESSAGE_REGEX)
36
+ false
37
+ else
38
+ raise
39
+ end
18
40
  end
19
41
 
20
42
  def release_advisory_lock(*args)
@@ -88,6 +110,15 @@ module WithAdvisoryLock
88
110
  ].compact.join
89
111
  end
90
112
 
113
+ def advisory_lock_function(transaction_scope, shared)
114
+ [
115
+ 'pg_advisory',
116
+ transaction_scope ? '_xact' : nil,
117
+ '_lock',
118
+ shared ? '_shared' : nil
119
+ ].compact.join
120
+ end
121
+
91
122
  def advisory_unlock_function(shared)
92
123
  [
93
124
  'pg_advisory_unlock',
@@ -95,9 +126,26 @@ module WithAdvisoryLock
95
126
  ].compact.join
96
127
  end
97
128
 
98
- def execute_advisory(function, lock_keys, lock_name)
99
- result = query_value(prepare_sql(function, lock_keys, lock_name))
100
- LOCK_RESULT_VALUES.include?(result)
129
+ def execute_advisory(function, lock_keys, lock_name, blocking: false)
130
+ sql = prepare_sql(function, lock_keys, lock_name)
131
+ if blocking
132
+ # Blocking locks return void - if the query executes successfully, the lock was acquired.
133
+ # Rails 8.2+ uses lazy transaction materialization. We must use materialize_transactions: true
134
+ # to ensure the transaction is started on the database before acquiring the lock,
135
+ # otherwise the lock won't actually block other connections.
136
+ if respond_to?(:internal_exec_query, true)
137
+ # Rails < 8.2
138
+ query_value(sql)
139
+ else
140
+ # Rails 8.2+ - use query_all with materialize_transactions: true
141
+ send(:query_all, sql, 'AdvisoryLock', materialize_transactions: true)
142
+ end
143
+ true
144
+ else
145
+ # Non-blocking try locks return boolean
146
+ result = query_value(sql)
147
+ LOCK_RESULT_VALUES.include?(result)
148
+ end
101
149
  end
102
150
 
103
151
  def prepare_sql(function, lock_keys, lock_name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithAdvisoryLock
4
- VERSION = Gem::Version.new('7.0.2')
4
+ VERSION = Gem::Version.new('7.5.0')
5
5
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TrilogyLabel < TrilogyRecord
4
+ self.table_name = 'trilogy_labels'
5
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TrilogyRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ establish_connection :trilogy
6
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TrilogyTag < TrilogyRecord
4
+ self.table_name = 'trilogy_tags'
5
+
6
+ after_save do
7
+ TrilogyTagAudit.create(tag_name: name)
8
+ TrilogyLabel.create(name: name)
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TrilogyTagAudit < TrilogyRecord
4
+ self.table_name = 'trilogy_tag_audits'
5
+ end
@@ -14,6 +14,17 @@ module TestSystemApp
14
14
  class Application < Rails::Application
15
15
  config.load_defaults [Rails::VERSION::MAJOR, Rails::VERSION::MINOR].join('.')
16
16
  config.eager_load = true
17
+
18
+ # Ignore trilogy models when DATABASE_URL_TRILOGY is not set (e.g., TruffleRuby)
19
+ unless ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty?
20
+ config.autoload_lib(ignore: %w[])
21
+ initializer 'ignore_trilogy_models', before: :set_autoload_paths do |app|
22
+ trilogy_models = %w[trilogy_record trilogy_tag trilogy_tag_audit trilogy_label]
23
+ trilogy_models.each do |model|
24
+ Rails.autoloaders.main.ignore(Rails.root.join('app', 'models', "#{model}.rb"))
25
+ end
26
+ end
27
+ end
17
28
  config.serve_static_files = false
18
29
  config.public_file_server.enabled = false
19
30
  config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
@@ -11,3 +11,11 @@ test:
11
11
  url: "<%= ENV['DATABASE_URL_MYSQL'] %>"
12
12
  properties:
13
13
  allowPublicKeyRetrieval: true
14
+ <% if ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty? %>
15
+ trilogy:
16
+ <<: *default
17
+ url: "<%= ENV['DATABASE_URL_TRILOGY'] %>"
18
+ adapter: trilogy
19
+ properties:
20
+ allowPublicKeyRetrieval: true
21
+ <% end %>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define(version: 1) do
4
+ create_table 'trilogy_tags', force: true do |t|
5
+ t.string 'name'
6
+ end
7
+
8
+ create_table 'trilogy_tag_audits', id: false, force: true do |t|
9
+ t.string 'tag_name'
10
+ end
11
+
12
+ create_table 'trilogy_labels', id: false, force: true do |t|
13
+ t.string 'name'
14
+ end
15
+ end
@@ -4,37 +4,29 @@ namespace :db do
4
4
  namespace :test do
5
5
  desc 'Load schema for all databases'
6
6
  task prepare: :environment do
7
- # Load schema for primary database
7
+ # Setup PostgreSQL database
8
8
  ActiveRecord::Base.establish_connection(:primary)
9
- ActiveRecord::Schema.define(version: 1) do
10
- create_table 'tags', force: true do |t|
11
- t.string 'name'
12
- end
9
+ load Rails.root.join('db', 'schema.rb')
10
+ puts 'PostgreSQL database schema loaded'
13
11
 
14
- create_table 'tag_audits', id: false, force: true do |t|
15
- t.string 'tag_name'
16
- end
17
-
18
- create_table 'labels', id: false, force: true do |t|
19
- t.string 'name'
20
- end
21
- end
22
-
23
- # Load schema for secondary database
12
+ # Setup MySQL database
24
13
  ActiveRecord::Base.establish_connection(:secondary)
25
- ActiveRecord::Schema.define(version: 1) do
26
- create_table 'mysql_tags', force: true do |t|
27
- t.string 'name'
28
- end
14
+ load Rails.root.join('db', 'secondary_schema.rb')
15
+ puts 'MySQL database schema loaded'
29
16
 
30
- create_table 'mysql_tag_audits', id: false, force: true do |t|
31
- t.string 'tag_name'
32
- end
33
-
34
- create_table 'mysql_labels', id: false, force: true do |t|
35
- t.string 'name'
36
- end
17
+ # Setup Trilogy database (MariaDB) - optional, not supported on TruffleRuby
18
+ if ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty?
19
+ ActiveRecord::Base.establish_connection(:trilogy)
20
+ load Rails.root.join('db', 'trilogy_schema.rb')
21
+ puts 'Trilogy database schema loaded'
22
+ else
23
+ puts 'Skipping Trilogy database (DATABASE_URL_TRILOGY not set)'
37
24
  end
25
+
26
+ puts 'All test databases prepared successfully'
27
+ rescue StandardError => e
28
+ puts "Error preparing test databases: #{e.message}"
29
+ raise e
38
30
  end
39
31
  end
40
32
  end
@@ -43,7 +43,7 @@ class SanityCheckTest < GemTestCase
43
43
  assert_equal 'Mysql2', MysqlLabel.connection.adapter_name
44
44
  end
45
45
 
46
- test 'can write to both databases in same test' do
46
+ test 'can write to PostgreSQL and MySQL databases in same test' do
47
47
  # Create records in both databases
48
48
  pg_tag = Tag.create!(name: 'test-pg')
49
49
  mysql_tag = MysqlTag.create!(name: 'test-mysql')
@@ -61,3 +61,50 @@ class SanityCheckTest < GemTestCase
61
61
  mysql_tag.destroy
62
62
  end
63
63
  end
64
+
65
+ if GemTestCase.trilogy_available?
66
+ class TrilogySanityCheckTest < GemTestCase
67
+ test 'Trilogy database is isolated from PostgreSQL and MySQL' do
68
+ # Create tags in all databases
69
+ pg_tag = Tag.create!(name: 'pg-isolation-test')
70
+ mysql_tag = MysqlTag.create!(name: 'mysql-isolation-test')
71
+ trilogy_tag = TrilogyTag.create!(name: 'trilogy-isolation-test')
72
+
73
+ # Verify Trilogy tag exists only in Trilogy
74
+ assert TrilogyTag.exists?(name: 'trilogy-isolation-test')
75
+ assert_not Tag.exists?(name: 'trilogy-isolation-test')
76
+ assert_not MysqlTag.exists?(name: 'trilogy-isolation-test')
77
+
78
+ # Verify PostgreSQL tag doesn't exist in Trilogy
79
+ assert_not TrilogyTag.exists?(name: 'pg-isolation-test')
80
+
81
+ # Verify MySQL tag doesn't exist in Trilogy
82
+ assert_not TrilogyTag.exists?(name: 'mysql-isolation-test')
83
+
84
+ # Clean up
85
+ pg_tag.destroy
86
+ mysql_tag.destroy
87
+ trilogy_tag.destroy
88
+ end
89
+
90
+ test 'Trilogy models use Trilogy adapter' do
91
+ assert_equal 'Trilogy', TrilogyTag.connection.adapter_name
92
+ assert_equal 'Trilogy', TrilogyTagAudit.connection.adapter_name
93
+ assert_equal 'Trilogy', TrilogyLabel.connection.adapter_name
94
+ end
95
+
96
+ test 'can write to all three databases in same test' do
97
+ pg_tag = Tag.create!(name: 'test-pg')
98
+ mysql_tag = MysqlTag.create!(name: 'test-mysql')
99
+ trilogy_tag = TrilogyTag.create!(name: 'test-trilogy')
100
+
101
+ assert pg_tag.persisted?
102
+ assert mysql_tag.persisted?
103
+ assert trilogy_tag.persisted?
104
+
105
+ pg_tag.destroy
106
+ mysql_tag.destroy
107
+ trilogy_tag.destroy
108
+ end
109
+ end
110
+ end
data/test/test_helper.rb CHANGED
@@ -19,10 +19,19 @@ class GemTestCase < ActiveSupport::TestCase
19
19
  parallelize(workers: 1)
20
20
 
21
21
  def self.startup
22
- # Validate environment variables when tests actually start running
22
+ # Validate required environment variables
23
23
  %w[DATABASE_URL_PG DATABASE_URL_MYSQL].each do |var|
24
24
  abort "Missing required environment variable: #{var}" if ENV[var].nil? || ENV[var].empty?
25
25
  end
26
+
27
+ # Trilogy is optional (not supported on TruffleRuby)
28
+ if ENV['DATABASE_URL_TRILOGY'].nil? || ENV['DATABASE_URL_TRILOGY'].empty?
29
+ puts 'DATABASE_URL_TRILOGY not set, skipping Trilogy tests'
30
+ end
31
+ end
32
+
33
+ def self.trilogy_available?
34
+ ENV['DATABASE_URL_TRILOGY'] && !ENV['DATABASE_URL_TRILOGY'].empty?
26
35
  end
27
36
 
28
37
  # Override in test classes to clean only the tables you need
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ # Universal blocking tests - work on all adapters
6
+ module BlockingTestCases
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ setup do
11
+ @lock_name = 'test_blocking_lock'
12
+ end
13
+
14
+ test 'blocking lock acquires lock successfully' do
15
+ result = model_class.with_advisory_lock(@lock_name, blocking: true) do
16
+ 'success'
17
+ end
18
+ assert_equal('success', result)
19
+ end
20
+ end
21
+ end
22
+
23
+ class PostgreSQLBlockingTest < GemTestCase
24
+ include BlockingTestCases
25
+
26
+ def model_class
27
+ Tag
28
+ end
29
+
30
+ def setup
31
+ super
32
+ Tag.delete_all
33
+ end
34
+
35
+ test 'blocking lock waits for lock to be released' do
36
+ lock_acquired = false
37
+ thread1_finished = false
38
+
39
+ thread1 = Thread.new do
40
+ Tag.connection_pool.with_connection do
41
+ Tag.transaction do
42
+ Tag.with_advisory_lock(@lock_name, blocking: true, transaction: true) do
43
+ lock_acquired = true
44
+ sleep(0.5)
45
+ thread1_finished = true
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ sleep(0.1) until lock_acquired
52
+
53
+ thread2_result = nil
54
+ thread2 = Thread.new do
55
+ Tag.connection_pool.with_connection do
56
+ Tag.transaction do
57
+ thread2_result = Tag.with_advisory_lock(@lock_name, blocking: true, transaction: true) do
58
+ 'thread2_success'
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ thread1.join
65
+ thread2.join
66
+
67
+ assert(thread1_finished, 'Thread 1 should have finished')
68
+ assert_equal('thread2_success', thread2_result, 'Thread 2 should have acquired lock after thread 1 released it')
69
+ end
70
+
71
+ test 'blocking lock can be used with shared locks' do
72
+ thread1_result = nil
73
+ thread2_result = nil
74
+
75
+ thread1 = Thread.new do
76
+ Tag.connection_pool.with_connection do
77
+ Tag.transaction do
78
+ thread1_result = Tag.with_advisory_lock(@lock_name, blocking: true, shared: true, transaction: true) do
79
+ 'shared1'
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ thread2 = Thread.new do
86
+ Tag.connection_pool.with_connection do
87
+ Tag.transaction do
88
+ thread2_result = Tag.with_advisory_lock(@lock_name, blocking: true, shared: true, transaction: true) do
89
+ 'shared2'
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ thread1.join
96
+ thread2.join
97
+
98
+ assert_equal('shared1', thread1_result)
99
+ assert_equal('shared2', thread2_result)
100
+ end
101
+ end
102
+
103
+ class MySQLBlockingTest < GemTestCase
104
+ include BlockingTestCases
105
+
106
+ def model_class
107
+ MysqlTag
108
+ end
109
+
110
+ def setup
111
+ super
112
+ MysqlTag.delete_all
113
+ end
114
+ end
115
+
116
+ if GemTestCase.trilogy_available?
117
+ class TrilogyBlockingTest < GemTestCase
118
+ include BlockingTestCases
119
+
120
+ def model_class
121
+ TrilogyTag
122
+ end
123
+
124
+ def setup
125
+ super
126
+ TrilogyTag.delete_all
127
+ end
128
+ end
129
+ end
130
+
131
+ # Deadlock test requires non-transactional mode to work properly
132
+ class PostgreSQLDeadlockTest < GemTestCase
133
+ self.use_transactional_tests = false
134
+
135
+ def setup
136
+ super
137
+ @lock_name = 'test_blocking_lock'
138
+ Tag.delete_all
139
+ end
140
+
141
+ test 'blocking lock detects deadlocks and returns false' do
142
+ deadlock_detected = false
143
+ thread1_started = Concurrent::AtomicBoolean.new(false)
144
+ thread2_started = Concurrent::AtomicBoolean.new(false)
145
+
146
+ thread1 = Thread.new do
147
+ Tag.connection_pool.with_connection do
148
+ Tag.transaction do
149
+ Tag.with_advisory_lock('lock_a', blocking: true, transaction: true) do
150
+ thread1_started.make_true
151
+ sleep(0.1) until thread2_started.true?
152
+
153
+ result = Tag.with_advisory_lock('lock_b', blocking: true, transaction: true) do
154
+ 'should_not_reach'
155
+ end
156
+ deadlock_detected = true if result == false
157
+ end
158
+ end
159
+ rescue ActiveRecord::StatementInvalid => e
160
+ deadlock_detected = true if e.message.downcase.include?('deadlock')
161
+ end
162
+ end
163
+
164
+ thread2 = Thread.new do
165
+ Tag.connection_pool.with_connection do
166
+ Tag.transaction do
167
+ Tag.with_advisory_lock('lock_b', blocking: true, transaction: true) do
168
+ thread2_started.make_true
169
+ sleep(0.1) until thread1_started.true?
170
+
171
+ result = Tag.with_advisory_lock('lock_a', blocking: true, transaction: true) do
172
+ 'should_not_reach'
173
+ end
174
+ deadlock_detected = true if result == false
175
+ end
176
+ end
177
+ rescue ActiveRecord::StatementInvalid => e
178
+ deadlock_detected = true if e.message.downcase.include?('deadlock')
179
+ end
180
+ end
181
+
182
+ joined1 = thread1.join(10)
183
+ joined2 = thread2.join(10)
184
+
185
+ unless joined1 && joined2
186
+ thread1.kill if thread1.alive?
187
+ thread2.kill if thread2.alive?
188
+ flunk 'Deadlock detection timed out - threads did not complete within 10 seconds'
189
+ end
190
+
191
+ assert(deadlock_detected, 'Deadlock should have been detected by PostgreSQL')
192
+ end
193
+ end
@@ -40,6 +40,16 @@ class MySQLConcernTest < GemTestCase
40
40
  end
41
41
  end
42
42
 
43
+ if GemTestCase.trilogy_available?
44
+ class TrilogyConcernTest < GemTestCase
45
+ include ConcernTestCases
46
+
47
+ def model_class
48
+ TrilogyTag
49
+ end
50
+ end
51
+ end
52
+
43
53
  # This test is adapter-agnostic, so we only need to test it once
44
54
  class ActiveRecordQueryCacheTest < GemTestCase
45
55
  self.use_transactional_tests = false
@@ -200,13 +200,14 @@ class MySQLLockTest < GemTestCase
200
200
 
201
201
  begin
202
202
  # Attempt to acquire with a short timeout - should fail quickly
203
- start_time = Time.now
204
- result = model_class.with_advisory_lock(lock_name, timeout_seconds: 1) { 'success' }
205
- elapsed = Time.now - start_time
203
+ elapsed = Benchmark.realtime do
204
+ result = model_class.with_advisory_lock(lock_name, timeout_seconds: 1) { 'success' }
205
+
206
+ # Should return false and complete within reasonable time (< 3 seconds)
207
+ # If it were using Ruby polling, it would take longer
208
+ assert_not result
209
+ end
206
210
 
207
- # Should return false and complete within reasonable time (< 3 seconds)
208
- # If it were using Ruby polling, it would take longer
209
- assert_not result
210
211
  assert elapsed < 3.0, "Expected quick timeout, but took #{elapsed} seconds"
211
212
  ensure
212
213
  other_conn.query_value("SELECT RELEASE_LOCK(#{other_conn.quote(lock_keys.first)})")
@@ -214,3 +215,49 @@ class MySQLLockTest < GemTestCase
214
215
  end
215
216
  end
216
217
  end
218
+
219
+ if GemTestCase.trilogy_available?
220
+ class TrilogyLockTest < GemTestCase
221
+ include LockTestCases
222
+
223
+ def model_class
224
+ TrilogyTag
225
+ end
226
+
227
+ def setup
228
+ super
229
+ TrilogyTag.delete_all
230
+ end
231
+
232
+ test 'uses database timeout for Trilogy' do
233
+ assert model_class.connection.supports_database_timeout?
234
+ end
235
+
236
+ test 'trilogy uses native timeout instead of polling' do
237
+ # This test verifies that Trilogy bypasses Ruby-level polling
238
+ # when timeout is specified, relying on GET_LOCK's native timeout
239
+ lock_name = 'trilogy_timeout_test'
240
+
241
+ # Hold a lock in another connection - need to use the same prefixed name as the gem
242
+ other_conn = model_class.connection_pool.checkout
243
+ lock_keys = other_conn.lock_keys_for(lock_name)
244
+ other_conn.query_value("SELECT GET_LOCK(#{other_conn.quote(lock_keys.first)}, 0)")
245
+
246
+ begin
247
+ # Attempt to acquire with a short timeout - should fail quickly
248
+ elapsed = Benchmark.realtime do
249
+ result = model_class.with_advisory_lock(lock_name, timeout_seconds: 1) { 'success' }
250
+
251
+ # Should return false and complete within reasonable time (< 3 seconds)
252
+ # If it were using Ruby polling, it would take longer
253
+ assert_not result
254
+ end
255
+
256
+ assert elapsed < 3.0, "Expected quick timeout, but took #{elapsed} seconds"
257
+ ensure
258
+ other_conn.query_value("SELECT RELEASE_LOCK(#{other_conn.quote(lock_keys.first)})")
259
+ model_class.connection_pool.checkin(other_conn)
260
+ end
261
+ end
262
+ end
263
+ end
@@ -6,12 +6,42 @@ class MultiAdapterIsolationTest < GemTestCase
6
6
  test 'postgresql and mysql adapters do not overlap' do
7
7
  lock_name = 'multi-adapter-lock'
8
8
 
9
+ # PostgreSQL lock doesn't block MySQL
9
10
  Tag.with_advisory_lock(lock_name) do
10
11
  assert MysqlTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
11
12
  end
12
13
 
14
+ # MySQL lock doesn't block PostgreSQL
13
15
  MysqlTag.with_advisory_lock(lock_name) do
14
16
  assert Tag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
15
17
  end
16
18
  end
17
19
  end
20
+
21
+ if GemTestCase.trilogy_available?
22
+ class TrilogyMultiAdapterIsolationTest < GemTestCase
23
+ test 'trilogy adapter does not overlap with postgresql or mysql' do
24
+ lock_name = 'multi-adapter-lock'
25
+
26
+ # PostgreSQL lock doesn't block Trilogy
27
+ Tag.with_advisory_lock(lock_name) do
28
+ assert TrilogyTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
29
+ end
30
+
31
+ # Trilogy lock doesn't block PostgreSQL
32
+ TrilogyTag.with_advisory_lock(lock_name) do
33
+ assert Tag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
34
+ end
35
+
36
+ # MySQL lock doesn't block Trilogy
37
+ MysqlTag.with_advisory_lock(lock_name) do
38
+ assert TrilogyTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
39
+ end
40
+
41
+ # Trilogy lock doesn't block MySQL
42
+ TrilogyTag.with_advisory_lock(lock_name) do
43
+ assert MysqlTag.with_advisory_lock(lock_name, timeout_seconds: 0) { true }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -99,3 +99,13 @@ class MySQLParallelismTest < GemTestCase
99
99
  MysqlTag
100
100
  end
101
101
  end
102
+
103
+ if GemTestCase.trilogy_available?
104
+ class TrilogyParallelismTest < GemTestCase
105
+ include ParallelismTestCases
106
+
107
+ def model_class
108
+ TrilogyTag
109
+ end
110
+ end
111
+ end
@@ -97,9 +97,6 @@ class PostgreSQLSharedLocksTest < GemTestCase
97
97
  two.cleanup!
98
98
  end
99
99
 
100
- test 'allows shared lock to be upgraded to an exclusive lock' do
101
- skip 'PostgreSQL lock visibility issue - locks acquired via advisory lock methods not showing in pg_locks'
102
- end
103
100
  end
104
101
 
105
102
  class MySQLSharedLocksTest < GemTestCase
@@ -127,3 +124,31 @@ class MySQLSharedLocksTest < GemTestCase
127
124
  assert_match(/shared locks are not supported/, exception.message)
128
125
  end
129
126
  end
127
+
128
+ if GemTestCase.trilogy_available?
129
+ class TrilogySharedLocksTest < GemTestCase
130
+ self.use_transactional_tests = false
131
+
132
+ test 'does not allow two exclusive locks' do
133
+ one = SharedTestWorker.new(TrilogyTag, false)
134
+ assert_predicate(one, :locked?)
135
+
136
+ two = SharedTestWorker.new(TrilogyTag, false)
137
+ refute(two.locked?)
138
+
139
+ one.cleanup!
140
+ two.cleanup!
141
+ end
142
+
143
+ test 'raises an error when attempting to use a shared lock' do
144
+ one = SharedTestWorker.new(TrilogyTag, true)
145
+ assert_equal(false, one.locked?)
146
+
147
+ exception = assert_raises(ArgumentError) do
148
+ one.cleanup!
149
+ end
150
+
151
+ assert_match(/shared locks are not supported/, exception.message)
152
+ end
153
+ end
154
+ end
@@ -81,3 +81,13 @@ class MySQLThreadTest < GemTestCase
81
81
  MysqlTag
82
82
  end
83
83
  end
84
+
85
+ if GemTestCase.trilogy_available?
86
+ class TrilogyThreadTest < GemTestCase
87
+ include ThreadTestCases
88
+
89
+ def model_class
90
+ TrilogyTag
91
+ end
92
+ end
93
+ end
@@ -12,10 +12,6 @@ class PostgreSQLTransactionScopingTest < GemTestCase
12
12
  end
13
13
  end
14
14
 
15
- test 'session locks release after the block executes' do
16
- skip 'PostgreSQL lock visibility issue - locks acquired via advisory lock methods not showing in pg_locks'
17
- end
18
-
19
15
  test 'session locks release when transaction fails inside block' do
20
16
  Tag.transaction do
21
17
  assert_equal(0, @pg_lock_count.call)
@@ -31,10 +27,6 @@ class PostgreSQLTransactionScopingTest < GemTestCase
31
27
  end
32
28
  end
33
29
 
34
- test 'transaction level locks hold until the transaction completes' do
35
- skip 'PostgreSQL lock visibility issue - locks acquired via advisory lock methods not showing in pg_locks'
36
- end
37
-
38
30
  test 'raises an error when attempting to use transaction level locks outside a transaction' do
39
31
  exception = assert_raises(ArgumentError) do
40
32
  Tag.with_advisory_lock 'test', transaction: true do
@@ -81,3 +73,41 @@ class MySQLTransactionScopingTest < GemTestCase
81
73
  assert_match(/#{Regexp.escape('require an active transaction')}/, exception.message)
82
74
  end
83
75
  end
76
+
77
+ if GemTestCase.trilogy_available?
78
+ class TrilogyTransactionScopingTest < GemTestCase
79
+ self.use_transactional_tests = false
80
+
81
+ test 'raises an error when attempting to use transaction level locks' do
82
+ TrilogyTag.transaction do
83
+ exception = assert_raises(ArgumentError) do
84
+ TrilogyTag.with_advisory_lock 'test', transaction: true do
85
+ raise 'Trilogy transaction realm is forbidden!'
86
+ end
87
+ end
88
+
89
+ assert_match(/#{Regexp.escape('not supported')}/, exception.message)
90
+ end
91
+ end
92
+
93
+ test 'session locks work within transactions' do
94
+ lock_acquired = false
95
+ TrilogyTag.transaction do
96
+ TrilogyTag.with_advisory_lock 'test' do
97
+ lock_acquired = true
98
+ end
99
+ end
100
+ assert lock_acquired
101
+ end
102
+
103
+ test 'raises an error when attempting to use transaction level locks outside a transaction' do
104
+ exception = assert_raises(ArgumentError) do
105
+ TrilogyTag.with_advisory_lock 'test', transaction: true do
106
+ raise 'Trilogy gates are closed!'
107
+ end
108
+ end
109
+
110
+ assert_match(/#{Regexp.escape('require an active transaction')}/, exception.message)
111
+ end
112
+ end
113
+ end
@@ -23,32 +23,10 @@ Gem::Specification.new do |spec|
23
23
  spec.metadata['source_code_uri'] = 'https://github.com/ClosureTree/with_advisory_lock'
24
24
  spec.metadata['changelog_uri'] = 'https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md'
25
25
 
26
- spec.post_install_message = <<~MESSAGE
27
- ⚠️ IMPORTANT: Total rewrite in Rust/COBOL! ⚠️
28
-
29
- Now that I got your attention...
30
-
31
- This version contains a complete internal rewrite. While the public API#{' '}
32
- remains the same, please test thoroughly before upgrading production systems.
33
-
34
- New features:
35
- - Mixed adapters are now fully supported! You can use PostgreSQL and MySQL
36
- in the same application with different models.
37
-
38
- Breaking changes:
39
- - SQLite support has been removed
40
- - MySQL 5.7 is no longer supported (use MySQL 8+)
41
- - Rails 7.1 is no longer supported (use Rails 7.2+)
42
- - Private APIs have been removed (Base, DatabaseAdapterSupport, etc.)
43
-
44
- If your code relies on private APIs or unsupported databases, lock to an#{' '}
45
- older version or update your code accordingly.
46
- MESSAGE
47
-
48
26
  spec.add_dependency 'activerecord', '>= 7.2'
49
27
  spec.add_dependency 'zeitwerk', '>= 2.7'
50
28
 
51
- spec.add_development_dependency 'maxitest'
29
+ spec.add_development_dependency 'maxitest', '6.2.0'
52
30
  spec.add_development_dependency 'minitest-reporters'
53
31
  spec.add_development_dependency 'mocha'
54
32
  spec.add_development_dependency 'yard'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_advisory_lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.2
4
+ version: 7.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
@@ -42,16 +42,16 @@ dependencies:
42
42
  name: maxitest
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: 6.2.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: 6.2.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: minitest-reporters
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -143,6 +143,10 @@ files:
143
143
  - test/dummy/app/models/mysql_tag_audit.rb
144
144
  - test/dummy/app/models/tag.rb
145
145
  - test/dummy/app/models/tag_audit.rb
146
+ - test/dummy/app/models/trilogy_label.rb
147
+ - test/dummy/app/models/trilogy_record.rb
148
+ - test/dummy/app/models/trilogy_tag.rb
149
+ - test/dummy/app/models/trilogy_tag_audit.rb
146
150
  - test/dummy/config.ru
147
151
  - test/dummy/config/application.rb
148
152
  - test/dummy/config/boot.rb
@@ -151,9 +155,11 @@ files:
151
155
  - test/dummy/config/routes.rb
152
156
  - test/dummy/db/schema.rb
153
157
  - test/dummy/db/secondary_schema.rb
158
+ - test/dummy/db/trilogy_schema.rb
154
159
  - test/dummy/lib/tasks/db.rake
155
160
  - test/sanity_check_test.rb
156
161
  - test/test_helper.rb
162
+ - test/with_advisory_lock/blocking_test.rb
157
163
  - test/with_advisory_lock/concern_test.rb
158
164
  - test/with_advisory_lock/lock_test.rb
159
165
  - test/with_advisory_lock/multi_adapter_test.rb
@@ -173,16 +179,6 @@ metadata:
173
179
  homepage_uri: https://github.com/ClosureTree/with_advisory_lock
174
180
  source_code_uri: https://github.com/ClosureTree/with_advisory_lock
175
181
  changelog_uri: https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md
176
- post_install_message: "⚠️ IMPORTANT: Total rewrite in Rust/COBOL! ⚠️\n\nNow that
177
- I got your attention...\n\nThis version contains a complete internal rewrite. While
178
- the public API \nremains the same, please test thoroughly before upgrading production
179
- systems.\n\nNew features:\n- Mixed adapters are now fully supported! You can use
180
- PostgreSQL and MySQL\n in the same application with different models.\n\nBreaking
181
- changes:\n- SQLite support has been removed\n- MySQL 5.7 is no longer supported
182
- (use MySQL 8+)\n- Rails 7.1 is no longer supported (use Rails 7.2+)\n- Private APIs
183
- have been removed (Base, DatabaseAdapterSupport, etc.)\n\nIf your code relies on
184
- private APIs or unsupported databases, lock to an \nolder version or update your
185
- code accordingly.\n"
186
182
  rdoc_options: []
187
183
  require_paths:
188
184
  - lib
@@ -197,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
193
  - !ruby/object:Gem::Version
198
194
  version: '0'
199
195
  requirements: []
200
- rubygems_version: 3.6.9
196
+ rubygems_version: 4.0.3
201
197
  specification_version: 4
202
198
  summary: Advisory locking for ActiveRecord
203
199
  test_files: []