with_advisory_lock 5.0.0 → 5.3.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-mysql5.yml +61 -0
  3. data/.github/workflows/ci-mysql8.yml +62 -0
  4. data/.github/workflows/ci-postgresql.yml +64 -0
  5. data/.github/workflows/ci-sqlite3.yml +54 -0
  6. data/.github/workflows/release.yml +17 -0
  7. data/.release-please-manifest.json +1 -0
  8. data/.ruby-version +2 -0
  9. data/.tool-versions +1 -1
  10. data/CHANGELOG.md +41 -0
  11. data/LICENSE.txt +4 -4
  12. data/Makefile +14 -0
  13. data/docker-compose.yml +20 -0
  14. data/lib/with_advisory_lock/base.rb +3 -3
  15. data/lib/with_advisory_lock/concern.rb +5 -5
  16. data/lib/with_advisory_lock/database_adapter_support.rb +5 -8
  17. data/lib/with_advisory_lock/failed_to_acquire_lock.rb +3 -1
  18. data/lib/with_advisory_lock/mysql.rb +8 -3
  19. data/lib/with_advisory_lock/postgresql.rb +38 -15
  20. data/lib/with_advisory_lock/version.rb +1 -1
  21. data/lib/with_advisory_lock.rb +9 -9
  22. data/release-please-config.json +9 -0
  23. data/test/test_helper.rb +20 -6
  24. data/test/test_models.rb +8 -4
  25. data/test/with_advisory_lock/base_test.rb +9 -0
  26. data/test/{concern_test.rb → with_advisory_lock/concern_test.rb} +2 -2
  27. data/test/{lock_test.rb → with_advisory_lock/lock_test.rb} +31 -0
  28. data/test/{parallelism_test.rb → with_advisory_lock/parallelism_test.rb} +3 -3
  29. data/test/{shared_test.rb → with_advisory_lock/shared_test.rb} +2 -2
  30. data/test/{thread_test.rb → with_advisory_lock/thread_test.rb} +2 -2
  31. data/test/{transaction_test.rb → with_advisory_lock/transaction_test.rb} +1 -1
  32. data/with_advisory_lock.gemspec +7 -2
  33. metadata +49 -24
  34. data/.github/workflows/ci.yml +0 -80
  35. /data/test/{nesting_test.rb → with_advisory_lock/nesting_test.rb} +0 -0
  36. /data/test/{options_test.rb → with_advisory_lock/options_test.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 784812e261b5a57a5d3da8c878e536949245b29372b40b2baf15a52d7c65b854
4
- data.tar.gz: ba2f6fc8bf78c89d56635c4f56500117f79a443404d3574d8b54b2a45d1811de
3
+ metadata.gz: d65989d7502660ac5cbebd51a589fc49fb76bbeae5abb13814cb79a711fdaae8
4
+ data.tar.gz: 2eea1e741e2fdb100d3170ddce8c38b1d0fc59dc98f6340be51d6d5bde0591a4
5
5
  SHA512:
6
- metadata.gz: bb7dc11c1f206639943daa363a4d917bfe93792e7db764c7860ef68151e99defaa03f58640fae0480b566e3d4a0697695f5b60ae8af09838bb176979c328d8ae
7
- data.tar.gz: 10132a0c47ed8070a0661a7b6ae0409be76ef14693be65c12a8e8cdd9c5a506ba25c60bda264f7c60bd92952e4cd4c5513a5e566a3e0d757f0ab6fc4311ffcd5
6
+ metadata.gz: 8358a8783df5bcf9bd49507fc6e59d56aeaec8febd3978a4f9178950c88b254ec76925e3caa9f9f2ee18d7bc15d186512b3790dd1ba602a38fa6d1a877570887
7
+ data.tar.gz: d179cc20743f15a671ad822fd21e04e808336382d2eed318439e7b9c5ae9283ff4abf0576e5b0c274946863033f403d27280dfec84b3e3e4ff20f832ca62bcb5
@@ -0,0 +1,61 @@
1
+ name: CI Mysql 5.7
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - master
6
+ concurrency:
7
+ group: ci-mysql5-${{ github.head_ref }}
8
+ cancel-in-progress: true
9
+
10
+ jobs:
11
+ minitest:
12
+ runs-on: ubuntu-latest
13
+ name: CI Mysql 5.7 Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }}
14
+ services:
15
+ mysql:
16
+ image: mysql/mysql-server:5.7
17
+ ports:
18
+ - 3306
19
+ env:
20
+ MYSQL_USER: with_advisory
21
+ MYSQL_PASSWORD: with_advisory_pass
22
+ MYSQL_DATABASE: with_advisory_lock_test
23
+ MYSQL_ROOT_HOST: '%'
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ ruby:
28
+ # - '3.2'
29
+ # - '3.1'
30
+ # - '3.0'
31
+ # - '2.7'
32
+ - '3.3'
33
+ - 'truffleruby'
34
+ rails:
35
+ - 7.1
36
+ - "7.0"
37
+ - 6.1
38
+ adapter:
39
+ - mysql2
40
+ - trilogy
41
+ include:
42
+ - ruby: jruby
43
+ rails: 6.1
44
+ adapter: jdbcmysql
45
+ steps:
46
+ - name: Checkout
47
+ uses: actions/checkout@v4
48
+ - name: Setup Ruby
49
+ uses: ruby/setup-ruby@v1
50
+ with:
51
+ ruby-version: ${{ matrix.ruby }}
52
+ bundler-cache: true
53
+ rubygems: latest
54
+ env:
55
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
56
+ - name: Test
57
+ env:
58
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
59
+ DATABASE_URL: ${{ matrix.adapter }}://with_advisory:with_advisory_pass@0:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test
60
+ WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
61
+ run: bundle exec rake
@@ -0,0 +1,62 @@
1
+ name: CI Mysql 8.0
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - master
6
+
7
+ concurrency:
8
+ group: ci-mysql8-${{ github.head_ref }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ minitest:
13
+ runs-on: ubuntu-latest
14
+ name: CI Mysql 8.0 Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }}
15
+ services:
16
+ mysql:
17
+ image: mysql/mysql-server
18
+ ports:
19
+ - 3306
20
+ env:
21
+ MYSQL_USER: with_advisory
22
+ MYSQL_PASSWORD: with_advisory_pass
23
+ MYSQL_DATABASE: with_advisory_lock_test
24
+ MYSQL_ROOT_HOST: '%'
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ ruby:
29
+ # - '3.2'
30
+ # - '3.1'
31
+ # - '3.0'
32
+ # - '2.7'
33
+ - '3.3'
34
+ - 'truffleruby'
35
+ rails:
36
+ - 7.1
37
+ - "7.0"
38
+ - 6.1
39
+ adapter:
40
+ - mysql2
41
+ # - trilogy://with_advisory:with_advisory_pass@0/with_advisory_lock_test Trilogy is not supported by mysql 8 with new encryption
42
+ include:
43
+ - ruby: jruby
44
+ rails: 6.1
45
+ adapter: jdbcmysql
46
+ steps:
47
+ - name: Checkout
48
+ uses: actions/checkout@v4
49
+ - name: Setup Ruby
50
+ uses: ruby/setup-ruby@v1
51
+ with:
52
+ ruby-version: ${{ matrix.ruby }}
53
+ bundler-cache: true
54
+ rubygems: latest
55
+ env:
56
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
57
+ - name: Test
58
+ env:
59
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
60
+ DATABASE_URL: ${{ matrix.adapter }}://with_advisory:with_advisory_pass@0:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test
61
+ WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
62
+ run: bundle exec rake
@@ -0,0 +1,64 @@
1
+ name: CI Postgresql
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - master
6
+ concurrency:
7
+ group: ci-postgresql-${{ github.head_ref }}
8
+ cancel-in-progress: true
9
+
10
+ jobs:
11
+ minitest:
12
+ runs-on: ubuntu-latest
13
+ name: CI Postgresql Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }}
14
+ services:
15
+ postgres:
16
+ image: 'postgres:16-alpine'
17
+ ports:
18
+ - '5432'
19
+ env:
20
+ POSTGRES_USER: with_advisory
21
+ POSTGRES_PASSWORD: with_advisory_pass
22
+ POSTGRES_DB: with_advisory_lock_test
23
+ options: >-
24
+ --health-cmd pg_isready
25
+ --health-interval 10s
26
+ --health-timeout 5s
27
+ --health-retries 5
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ ruby:
32
+ # - '3.2'
33
+ # - '3.1'
34
+ # - '3.0'
35
+ # - '2.7'
36
+ - '3.3'
37
+ - 'truffleruby'
38
+ rails:
39
+ - 7.1
40
+ - "7.0"
41
+ - 6.1
42
+ adapter:
43
+ - postgres
44
+ include:
45
+ - ruby: jruby
46
+ rails: 6.1
47
+ adapter: jdbcpostgresql
48
+ steps:
49
+ - name: Checkout
50
+ uses: actions/checkout@v4
51
+ - name: Setup Ruby
52
+ uses: ruby/setup-ruby@v1
53
+ with:
54
+ ruby-version: ${{ matrix.ruby }}
55
+ bundler-cache: true
56
+ rubygems: latest
57
+ env:
58
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
59
+ - name: Test
60
+ env:
61
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
62
+ DATABASE_URL: ${{ matrix.adapter }}://with_advisory:with_advisory_pass@localhost:${{ job.services.postgres.ports[5432] }}/with_advisory_lock_test
63
+ WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
64
+ run: bundle exec rake
@@ -0,0 +1,54 @@
1
+ name: CI Sqlite3
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - master
7
+
8
+ concurrency:
9
+ group: ci-sqlite3-${{ github.head_ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ minitest:
14
+ runs-on: ubuntu-latest
15
+ name: CI Sqlite3 Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }}
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ ruby:
20
+ # - '3.2'
21
+ # - '3.1'
22
+ # - '3.0'
23
+ # - '2.7'
24
+ - '3.3'
25
+ - 'truffleruby'
26
+ rails:
27
+ - 7.1
28
+ - "7.0"
29
+ - 6.1
30
+ adapter:
31
+ - sqlite3
32
+ include:
33
+ - ruby: jruby
34
+ rails: 6.1
35
+ adapter: jdbcsqlite3
36
+ steps:
37
+ - name: Checkout
38
+ uses: actions/checkout@v4
39
+
40
+ - name: Setup Ruby
41
+ uses: ruby/setup-ruby@v1
42
+ with:
43
+ ruby-version: ${{ matrix.ruby }}
44
+ bundler-cache: true
45
+ rubygems: latest
46
+ env:
47
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
48
+
49
+ - name: Test
50
+ env:
51
+ BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile
52
+ DATABASE_URL: ${{ matrix.adapter }}:///tmp/test.sqlite3
53
+ WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
54
+ run: bundle exec rake
@@ -0,0 +1,17 @@
1
+ name: release-please
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: write
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ release-please:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: googleapis/release-please-action@v4
@@ -0,0 +1 @@
1
+ {".":"5.3.0"}
data/.ruby-version ADDED
@@ -0,0 +1,2 @@
1
+ 3.4.4
2
+
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.0.5
1
+ ruby 3.4.4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  ## Changelog
2
2
 
3
+ ## [5.3.0](https://github.com/ClosureTree/with_advisory_lock/compare/with_advisory_lock/v5.2.0...with_advisory_lock/v5.3.0) (2025-04-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * add #current_advisory_locks method ([#111](https://github.com/ClosureTree/with_advisory_lock/issues/111)) ([ccbd3b2](https://github.com/ClosureTree/with_advisory_lock/commit/ccbd3b23465f7fa1fc3800334159986c31d5c351))
9
+
10
+ ## [5.2.0](https://github.com/ClosureTree/with_advisory_lock/compare/with_advisory_lock/v5.1.0...with_advisory_lock/v5.2.0) (2025-04-24)
11
+
12
+
13
+ ### Features
14
+
15
+ * use current connnection instead of the one in ActiveRecord::Base ([#90](https://github.com/ClosureTree/with_advisory_lock/issues/90)) ([c28a172](https://github.com/ClosureTree/with_advisory_lock/commit/c28a172a5a64594448b6090501fc0b8cbace06f6))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * Removed MySQL unused lock variable and broaden SQLite detection. ([#94](https://github.com/ClosureTree/with_advisory_lock/issues/94)) ([f818a18](https://github.com/ClosureTree/with_advisory_lock/commit/f818a181dde6711c8439c4cbf67c4525a09d346e))
21
+
22
+ ## [5.1.0](https://github.com/ClosureTree/with_advisory_lock/compare/with_advisory_lock/v5.0.1...with_advisory_lock/v5.1.0) (2024-01-21)
23
+
24
+
25
+ ### Features
26
+
27
+ * use zeitwerk loader instead of ActiveSupport::Autoload ([b5082fd](https://github.com/ClosureTree/with_advisory_lock/commit/b5082fddacacacff48139f5bf509601a37945a0e))
28
+
29
+ ## 5.0.1 (2024-01-21)
30
+
31
+
32
+ ### Features
33
+
34
+ * add release workflow ([5d32520](https://github.com/ClosureTree/with_advisory_lock/commit/5d325201c82974991381a9fbc4d1714c9739dc4f))
35
+ * add ruby 3.1 test/support ([#60](https://github.com/ClosureTree/with_advisory_lock/issues/60)) ([514f042](https://github.com/ClosureTree/with_advisory_lock/commit/514f0420d957ef30911a00d54685385bec5867c3))
36
+ * Add testing for activerecord 7.1 and support for trilogy adapter ([#77](https://github.com/ClosureTree/with_advisory_lock/issues/77)) ([69c23fe](https://github.com/ClosureTree/with_advisory_lock/commit/69c23fe09887fc5d97ac7b0194825c21efe244a5))
37
+ * add truffleruby support ([#62](https://github.com/ClosureTree/with_advisory_lock/issues/62)) ([ec34bd4](https://github.com/ClosureTree/with_advisory_lock/commit/ec34bd448e3505e5df631daaf47bb83f2f5316dc))
38
+
39
+
40
+ ### Bug Fixes
41
+
42
+ * User may sometimes pass in non-strings, such as integers ([#55](https://github.com/ClosureTree/with_advisory_lock/issues/55)) ([9885597](https://github.com/ClosureTree/with_advisory_lock/commit/988559747363ef00958fcf782317e76c40ffa2a3))
43
+
3
44
  ### 5.0.0
4
45
  - Drop support for EOL rubies and activerecord (ruby below 2.7 and activerecord below 6.1).
5
46
  - Allow lock name to be integer
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
- Copyright (c) 2013 Matthew McEachen
2
-
3
- MIT License
1
+ SPDX-License-Identifier: MIT
2
+ SPDX-FileCopyrightText: 2013 Matthew McEachen
3
+ SPDX-FileCopyrightText: 2013-2025 Abdelkader Boudih
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
@@ -19,4 +19,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
19
  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
20
  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
21
  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Makefile ADDED
@@ -0,0 +1,14 @@
1
+ .PHONY: test-pg test-mysql
2
+
3
+ test-pg:
4
+ docker compose up -d pg
5
+ sleep 10 # give some time for the service to start
6
+ DATABASE_URL=postgres://with_advisory:with_advisory_pass@localhost/with_advisory_lock_test appraisal rake test
7
+
8
+ test-mysql:
9
+ docker compose up -d mysql
10
+ sleep 10 # give some time for the service to start
11
+ DATABASE_URL=mysql2://with_advisory:with_advisory_pass@0.0.0.0:3306/with_advisory_lock_test appraisal rake test
12
+
13
+
14
+ test: test-pg test-mysql
@@ -0,0 +1,20 @@
1
+ version: "3.9"
2
+ services:
3
+ pg:
4
+ image: postgres:16
5
+ environment:
6
+ POSTGRES_USER: with_advisory
7
+ POSTGRES_PASSWORD: with_advisory_pass
8
+ POSTGRES_DB: with_advisory_lock_test
9
+ ports:
10
+ - "5432:5432"
11
+ mysql:
12
+ image: mysql:8
13
+ environment:
14
+ MYSQL_USER: with_advisory
15
+ MYSQL_PASSWORD: with_advisory_pass
16
+ MYSQL_DATABASE: with_advisory_lock_test
17
+ MYSQL_RANDOM_ROOT_PASSWORD: "yes"
18
+ MYSQL_ROOT_HOST: '%'
19
+ ports:
20
+ - "3306:3306"
@@ -36,7 +36,7 @@ module WithAdvisoryLock
36
36
  end
37
37
 
38
38
  def lock_str
39
- @lock_str ||= "#{ENV['WITH_ADVISORY_LOCK_PREFIX']}#{lock_name}"
39
+ @lock_str ||= "#{ENV[LOCK_PREFIX_ENV]}#{lock_name}"
40
40
  end
41
41
 
42
42
  def lock_stack_item
@@ -56,7 +56,7 @@ module WithAdvisoryLock
56
56
  def with_advisory_lock_if_needed(&block)
57
57
  if disable_query_cache
58
58
  return lock_and_yield do
59
- ActiveRecord::Base.uncached(&block)
59
+ connection.uncached(&block)
60
60
  end
61
61
  end
62
62
 
@@ -79,7 +79,7 @@ module WithAdvisoryLock
79
79
  else
80
80
  # Ruby MRI's String#hash is randomly seeded as of Ruby 1.9 so
81
81
  # make sure we use a deterministic hash.
82
- Zlib.crc32(input.to_s)
82
+ Zlib.crc32(input.to_s, 0)
83
83
  end
84
84
  end
85
85
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/concern'
4
-
5
3
  module WithAdvisoryLock
6
4
  module Concern
7
5
  extend ActiveSupport::Concern
@@ -15,9 +13,7 @@ module WithAdvisoryLock
15
13
 
16
14
  def with_advisory_lock!(lock_name, options = {}, &block)
17
15
  result = with_advisory_lock_result(lock_name, options, &block)
18
- unless result.lock_was_acquired?
19
- raise WithAdvisoryLock::FailedToAcquireLock, lock_name
20
- end
16
+ raise WithAdvisoryLock::FailedToAcquireLock, lock_name unless result.lock_was_acquired?
21
17
 
22
18
  result.result
23
19
  end
@@ -37,6 +33,10 @@ module WithAdvisoryLock
37
33
  lock_stack_key && lock_stack_key[0]
38
34
  end
39
35
 
36
+ def current_advisory_locks
37
+ WithAdvisoryLock::Base.lock_stack.map(&:name)
38
+ end
39
+
40
40
  private
41
41
 
42
42
  def impl_class
@@ -2,25 +2,22 @@
2
2
 
3
3
  module WithAdvisoryLock
4
4
  class DatabaseAdapterSupport
5
- # Caches nested lock support by MySQL reported version
6
- @@mysql_nl_cache = {}
7
- @@mysql_nl_cache_mutex = Mutex.new
8
-
5
+ attr_reader :adapter_name
9
6
  def initialize(connection)
10
7
  @connection = connection
11
- @sym_name = connection.adapter_name.downcase.to_sym
8
+ @adapter_name = connection.adapter_name.downcase.to_sym
12
9
  end
13
10
 
14
11
  def mysql?
15
- %i[mysql2 trilogy].include? @sym_name
12
+ %i[mysql2 trilogy].include? adapter_name
16
13
  end
17
14
 
18
15
  def postgresql?
19
- %i[postgresql empostgresql postgis].include? @sym_name
16
+ %i[postgresql empostgresql postgis].include? adapter_name
20
17
  end
21
18
 
22
19
  def sqlite?
23
- @sym_name == :sqlite3
20
+ [:sqlite3, :sqlite].include? adapter_name
24
21
  end
25
22
  end
26
23
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WithAdvisoryLock
2
4
  class FailedToAcquireLock < StandardError
3
5
  def initialize(lock_name)
4
6
  super("Failed to acquire lock #{lock_name}")
5
7
  end
6
8
  end
7
- end
9
+ end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module WithAdvisoryLock
4
4
  class MySQL < Base
5
- # See https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
5
+ # See https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html
6
+ # See https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html
6
7
  def try_lock
7
8
  raise ArgumentError, 'shared locks are not supported on MySQL' if shared
8
9
  raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction
@@ -15,8 +16,12 @@ module WithAdvisoryLock
15
16
  end
16
17
 
17
18
  def execute_successful?(mysql_function)
18
- sql = "SELECT #{mysql_function} AS #{unique_column_name}"
19
- connection.select_value(sql).to_i.positive?
19
+ execute_query(mysql_function) == 1
20
+ end
21
+
22
+ def execute_query(mysql_function)
23
+ sql = "SELECT #{mysql_function}"
24
+ connection.query_value(sql)
20
25
  end
21
26
 
22
27
  # MySQL wants a string as the lock key.
@@ -2,42 +2,65 @@
2
2
 
3
3
  module WithAdvisoryLock
4
4
  class PostgreSQL < Base
5
- # See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
5
+ # See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
6
+
7
+ # MRI returns 't', jruby returns true. YAY!
8
+ LOCK_RESULT_VALUES = ['t', true].freeze
9
+ PG_ADVISORY_UNLOCK = 'pg_advisory_unlock'
10
+ PG_TRY_ADVISORY = 'pg_try_advisory'
11
+ ERROR_MESSAGE_REGEX = / ERROR: +current transaction is aborted,/
12
+
6
13
  def try_lock
7
- pg_function = "pg_try_advisory#{transaction ? '_xact' : ''}_lock#{shared ? '_shared' : ''}"
8
- execute_successful?(pg_function)
14
+ execute_successful?(advisory_try_lock_function(transaction))
9
15
  end
10
16
 
11
17
  def release_lock
12
18
  return if transaction
13
19
 
14
- pg_function = "pg_advisory_unlock#{shared ? '_shared' : ''}"
15
- execute_successful?(pg_function)
20
+ execute_successful?(advisory_unlock_function)
16
21
  rescue ActiveRecord::StatementInvalid => e
17
- raise unless e.message =~ / ERROR: +current transaction is aborted,/
22
+ raise unless e.message =~ ERROR_MESSAGE_REGEX
18
23
 
19
24
  begin
20
25
  connection.rollback_db_transaction
21
- execute_successful?(pg_function)
26
+ execute_successful?(advisory_unlock_function)
22
27
  ensure
23
28
  connection.begin_db_transaction
24
29
  end
25
30
  end
26
31
 
32
+ def advisory_try_lock_function(transaction_scope)
33
+ [
34
+ 'pg_try_advisory',
35
+ transaction_scope ? '_xact' : nil,
36
+ '_lock',
37
+ shared ? '_shared' : nil
38
+ ].compact.join
39
+ end
40
+
41
+ def advisory_unlock_function
42
+ [
43
+ 'pg_advisory_unlock',
44
+ shared ? '_shared' : nil
45
+ ].compact.join
46
+ end
47
+
27
48
  def execute_successful?(pg_function)
49
+ result = connection.select_value(prepare_sql(pg_function))
50
+ LOCK_RESULT_VALUES.include?(result)
51
+ end
52
+
53
+ def prepare_sql(pg_function)
28
54
  comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--')
29
- sql = "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
30
- result = connection.select_value(sql)
31
- # MRI returns 't', jruby returns true. YAY!
32
- ['t', true].include?(result)
55
+ "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */"
33
56
  end
34
57
 
35
58
  # PostgreSQL wants 2 32bit integers as the lock key.
36
59
  def lock_keys
37
- @lock_keys ||= [stable_hashcode(lock_name), ENV['WITH_ADVISORY_LOCK_PREFIX']].map do |ea|
38
- # pg advisory args must be 31 bit ints
39
- ea.to_i & 0x7fffffff
40
- end
60
+ @lock_keys ||= [
61
+ stable_hashcode(lock_name),
62
+ ENV[LOCK_PREFIX_ENV]
63
+ ].map { |ea| ea.to_i & 0x7fffffff }
41
64
  end
42
65
  end
43
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithAdvisoryLock
4
- VERSION = Gem::Version.new('5.0.0')
4
+ VERSION = Gem::Version.new('5.3.0')
5
5
  end
@@ -1,16 +1,16 @@
1
1
  require 'with_advisory_lock/version'
2
2
  require 'active_support'
3
- require_relative 'with_advisory_lock/failed_to_acquire_lock'
3
+ require 'zeitwerk'
4
4
 
5
- module WithAdvisoryLock
6
- extend ActiveSupport::Autoload
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.inflector.inflect(
7
+ 'mysql' => 'MySQL',
8
+ 'postgresql' => 'PostgreSQL',
9
+ )
10
+ loader.setup
7
11
 
8
- autoload :Concern
9
- autoload :Base
10
- autoload :DatabaseAdapterSupport
11
- autoload :Flock
12
- autoload :MySQL, 'with_advisory_lock/mysql'
13
- autoload :PostgreSQL, 'with_advisory_lock/postgresql'
12
+ module WithAdvisoryLock
13
+ LOCK_PREFIX_ENV = 'WITH_ADVISORY_LOCK_PREFIX'.freeze
14
14
  end
15
15
 
16
16
  ActiveSupport.on_load :active_record do
@@ -0,0 +1,9 @@
1
+ {
2
+ "release-type": "ruby",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "ruby",
6
+ "package-name": "with_advisory_lock"
7
+ }
8
+ }
9
+ }
data/test/test_helper.rb CHANGED
@@ -17,7 +17,8 @@ end
17
17
 
18
18
  ActiveRecord::Base.configurations = {
19
19
  default_env: {
20
- url: ENV.fetch('DATABASE_URL', "sqlite3://#{Dir.tmpdir}/#{SecureRandom.hex}.sqlite3"),
20
+ url: ENV.fetch('DATABASE_URL', "sqlite3://#{Dir.tmpdir}/with_advisory_lock_test#{RUBY_VERSION}-#{ActiveRecord.gem_version}.sqlite3"),
21
+ pool: 20,
21
22
  properties: { allowPublicKeyRetrieval: true } # for JRuby madness
22
23
  }
23
24
  }
@@ -38,15 +39,28 @@ require 'maxitest/autorun'
38
39
  require 'mocha/minitest'
39
40
 
40
41
  class GemTestCase < ActiveSupport::TestCase
42
+
43
+ parallelize(workers: 1)
44
+ def adapter_support
45
+ @adapter_support ||= WithAdvisoryLock::DatabaseAdapterSupport.new(ActiveRecord::Base.connection)
46
+ end
47
+ def is_sqlite3_adapter?; adapter_support.sqlite?; end
48
+ def is_mysql_adapter?; adapter_support.mysql?; end
49
+ def is_postgresql_adapter?; adapter_support.postgresql?; end
50
+
41
51
  setup do
42
- ENV['FLOCK_DIR'] = Dir.mktmpdir
43
- Tag.delete_all
44
- TagAudit.delete_all
45
- Label.delete_all
52
+ ENV['FLOCK_DIR'] = Dir.mktmpdir if is_sqlite3_adapter?
53
+ ApplicationRecord.connection.truncate_tables(
54
+ Tag.table_name,
55
+ TagAudit.table_name,
56
+ Label.table_name
57
+ )
46
58
  end
59
+
47
60
  teardown do
48
- FileUtils.remove_entry_secure ENV['FLOCK_DIR']
61
+ FileUtils.remove_entry_secure(ENV['FLOCK_DIR'], true) if is_sqlite3_adapter?
49
62
  end
50
63
  end
51
64
 
52
65
  puts "Testing with #{env_db} database, ActiveRecord #{ActiveRecord.gem_version} and #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} as #{RUBY_VERSION}"
66
+ puts "Connection Pool size: #{ActiveRecord::Base.connection_pool.size}"
data/test/test_models.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ActiveRecord::Schema.define(version: 0) do
3
+ ActiveRecord::Schema.define(version: 1) do
4
4
  create_table 'tags', force: true do |t|
5
5
  t.string 'name'
6
6
  end
@@ -12,15 +12,19 @@ ActiveRecord::Schema.define(version: 0) do
12
12
  end
13
13
  end
14
14
 
15
- class Tag < ActiveRecord::Base
15
+ class ApplicationRecord < ActiveRecord::Base
16
+ self.abstract_class = true
17
+ end
18
+
19
+ class Tag < ApplicationRecord
16
20
  after_save do
17
21
  TagAudit.create(tag_name: name)
18
22
  Label.create(name: name)
19
23
  end
20
24
  end
21
25
 
22
- class TagAudit < ActiveRecord::Base
26
+ class TagAudit < ApplicationRecord
23
27
  end
24
28
 
25
- class Label < ActiveRecord::Base
29
+ class Label < ApplicationRecord
26
30
  end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class WithAdvisoryLockBaseTest < GemTestCase
4
+ test 'should support advisory_locks_enabled' do
5
+ skip if is_sqlite3_adapter?
6
+
7
+ assert Tag.connection.advisory_locks_enabled?
8
+ end
9
+ end
@@ -22,12 +22,12 @@ end
22
22
 
23
23
  class ActiveRecordQueryCacheTest < GemTestCase
24
24
  test 'does not disable quary cache by default' do
25
- ActiveRecord::Base.expects(:uncached).never
25
+ Tag.connection.expects(:uncached).never
26
26
  Tag.with_advisory_lock('lock') { Tag.first }
27
27
  end
28
28
 
29
29
  test 'can disable ActiveRecord query cache' do
30
- ActiveRecord::Base.expects(:uncached).once
30
+ Tag.connection.expects(:uncached).once
31
31
  Tag.with_advisory_lock('a-lock', disable_query_cache: true) { Tag.first }
32
32
  end
33
33
  end
@@ -77,4 +77,35 @@ class LockTest < GemTestCase
77
77
 
78
78
  assert_equal(expected, actual)
79
79
  end
80
+
81
+ test 'current_advisory_locks returns empty array outside an advisory lock request' do
82
+ assert_equal([], Tag.current_advisory_locks)
83
+ end
84
+
85
+ test 'current_advisory_locks returns an array with names of the acquired locks' do
86
+ Tag.with_advisory_lock(@lock_name) do
87
+ locks = Tag.current_advisory_locks
88
+ assert_equal(1, locks.size)
89
+ assert_match(/#{@lock_name}/, locks.first)
90
+ end
91
+ end
92
+
93
+ test 'current_advisory_locks returns array of all nested lock names' do
94
+ first_lock = 'outer lock'
95
+ second_lock = 'inner lock'
96
+
97
+ Tag.with_advisory_lock(first_lock) do
98
+ Tag.with_advisory_lock(second_lock) do
99
+ locks = Tag.current_advisory_locks
100
+ assert_equal(2, locks.size)
101
+ assert_match(/#{first_lock}/, locks.first)
102
+ assert_match(/#{second_lock}/, locks.last)
103
+ end
104
+
105
+ locks = Tag.current_advisory_locks
106
+ assert_equal(1, locks.size)
107
+ assert_match(/#{first_lock}/, locks.first)
108
+ end
109
+ assert_equal([], Tag.current_advisory_locks)
110
+ end
80
111
  end
@@ -15,7 +15,7 @@ class FindOrCreateWorker
15
15
 
16
16
  def work_later
17
17
  sleep
18
- ActiveRecord::Base.connection_pool.with_connection do
18
+ ApplicationRecord.connection_pool.with_connection do
19
19
  if @use_advisory_lock
20
20
  Tag.with_advisory_lock(@name) { work }
21
21
  else
@@ -46,11 +46,11 @@ class ParallelismTest < GemTestCase
46
46
  workers.each(&:join)
47
47
  end
48
48
  # Ensure we're still connected:
49
- ActiveRecord::Base.connection_pool.connection
49
+ ApplicationRecord.connection_pool.connection
50
50
  end
51
51
 
52
52
  setup do
53
- ActiveRecord::Base.connection.reconnect!
53
+ ApplicationRecord.connection.reconnect!
54
54
  @workers = 10
55
55
  end
56
56
 
@@ -24,7 +24,7 @@ class SharedTestWorker
24
24
  private
25
25
 
26
26
  def work
27
- ActiveRecord::Base.connection_pool.with_connection do
27
+ Tag.connection_pool.with_connection do
28
28
  Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do
29
29
  @locked = true
30
30
  sleep 0.01 until @cleanup
@@ -117,7 +117,7 @@ class SupportedEnvironmentTest < SharedLocksTest
117
117
  end
118
118
 
119
119
  def pg_lock_modes
120
- ActiveRecord::Base.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
120
+ Tag.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';")
121
121
  end
122
122
 
123
123
  test 'allows shared lock to be upgraded to an exclusive lock' do
@@ -10,7 +10,7 @@ class SeparateThreadTest < GemTestCase
10
10
  @t1_return_value = nil
11
11
 
12
12
  @t1 = Thread.new do
13
- ActiveRecord::Base.connection_pool.with_connection do
13
+ Label.connection_pool.with_connection do
14
14
  @t1_return_value = Label.with_advisory_lock(@lock_name) do
15
15
  @mutex.synchronize { @t1_acquired_lock = true }
16
16
  sleep
@@ -21,7 +21,7 @@ class SeparateThreadTest < GemTestCase
21
21
 
22
22
  # Wait for the thread to acquire the lock:
23
23
  sleep(0.1) until @mutex.synchronize { @t1_acquired_lock }
24
- ActiveRecord::Base.connection.reconnect!
24
+ Label.connection.reconnect!
25
25
  end
26
26
 
27
27
  teardown do
@@ -25,7 +25,7 @@ class TransactionScopingTest < GemTestCase
25
25
  setup do
26
26
  skip unless env_db == :postgresql
27
27
  @pg_lock_count = lambda do
28
- ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i
28
+ ApplicationRecord.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i
29
29
  end
30
30
  end
31
31
 
@@ -16,11 +16,16 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
17
  spec.test_files = spec.files.grep(%r{^test/})
18
18
  spec.require_paths = %w[lib]
19
- spec.metadata = { "rubyspecs_mfa_required" => "true" }
19
+ spec.metadata = { 'rubygems_mfa_required' => 'true' }
20
20
  spec.required_ruby_version = '>= 2.7.0'
21
- spec.metadata["yard.run"] = "yri"
21
+ spec.metadata['yard.run'] = 'yri'
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/ClosureTree/with_advisory_lock'
25
+ spec.metadata['changelog_uri'] = 'https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md'
22
26
 
23
27
  spec.add_runtime_dependency 'activerecord', '>= 6.1'
28
+ spec.add_runtime_dependency 'zeitwerk', '>= 2.6'
24
29
 
25
30
  spec.add_development_dependency 'appraisal'
26
31
  spec.add_development_dependency 'maxitest'
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: with_advisory_lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
8
8
  - Abdelkader Boudih
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2023-10-29 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: activerecord
@@ -25,6 +24,20 @@ dependencies:
25
24
  - - ">="
26
25
  - !ruby/object:Gem::Version
27
26
  version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: zeitwerk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '2.6'
28
41
  - !ruby/object:Gem::Dependency
29
42
  name: appraisal
30
43
  requirement: !ruby/object:Gem::Requirement
@@ -103,15 +116,23 @@ executables: []
103
116
  extensions: []
104
117
  extra_rdoc_files: []
105
118
  files:
106
- - ".github/workflows/ci.yml"
119
+ - ".github/workflows/ci-mysql5.yml"
120
+ - ".github/workflows/ci-mysql8.yml"
121
+ - ".github/workflows/ci-postgresql.yml"
122
+ - ".github/workflows/ci-sqlite3.yml"
123
+ - ".github/workflows/release.yml"
107
124
  - ".gitignore"
125
+ - ".release-please-manifest.json"
126
+ - ".ruby-version"
108
127
  - ".tool-versions"
109
128
  - Appraisals
110
129
  - CHANGELOG.md
111
130
  - Gemfile
112
131
  - LICENSE.txt
132
+ - Makefile
113
133
  - README.md
114
134
  - Rakefile
135
+ - docker-compose.yml
115
136
  - gemfiles/activerecord_6.1.gemfile
116
137
  - gemfiles/activerecord_7.0.gemfile
117
138
  - gemfiles/activerecord_7.1.gemfile
@@ -124,24 +145,28 @@ files:
124
145
  - lib/with_advisory_lock/mysql.rb
125
146
  - lib/with_advisory_lock/postgresql.rb
126
147
  - lib/with_advisory_lock/version.rb
127
- - test/concern_test.rb
128
- - test/lock_test.rb
129
- - test/nesting_test.rb
130
- - test/options_test.rb
131
- - test/parallelism_test.rb
132
- - test/shared_test.rb
148
+ - release-please-config.json
133
149
  - test/test_helper.rb
134
150
  - test/test_models.rb
135
- - test/thread_test.rb
136
- - test/transaction_test.rb
151
+ - test/with_advisory_lock/base_test.rb
152
+ - test/with_advisory_lock/concern_test.rb
153
+ - test/with_advisory_lock/lock_test.rb
154
+ - test/with_advisory_lock/nesting_test.rb
155
+ - test/with_advisory_lock/options_test.rb
156
+ - test/with_advisory_lock/parallelism_test.rb
157
+ - test/with_advisory_lock/shared_test.rb
158
+ - test/with_advisory_lock/thread_test.rb
159
+ - test/with_advisory_lock/transaction_test.rb
137
160
  - with_advisory_lock.gemspec
138
161
  homepage: https://github.com/ClosureTree/with_advisory_lock
139
162
  licenses:
140
163
  - MIT
141
164
  metadata:
142
- rubyspecs_mfa_required: 'true'
165
+ rubygems_mfa_required: 'true'
143
166
  yard.run: yri
144
- post_install_message:
167
+ homepage_uri: https://github.com/ClosureTree/with_advisory_lock
168
+ source_code_uri: https://github.com/ClosureTree/with_advisory_lock
169
+ changelog_uri: https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md
145
170
  rdoc_options: []
146
171
  require_paths:
147
172
  - lib
@@ -156,18 +181,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
181
  - !ruby/object:Gem::Version
157
182
  version: '0'
158
183
  requirements: []
159
- rubygems_version: 3.4.12
160
- signing_key:
184
+ rubygems_version: 3.6.7
161
185
  specification_version: 4
162
186
  summary: Advisory locking for ActiveRecord
163
187
  test_files:
164
- - test/concern_test.rb
165
- - test/lock_test.rb
166
- - test/nesting_test.rb
167
- - test/options_test.rb
168
- - test/parallelism_test.rb
169
- - test/shared_test.rb
170
188
  - test/test_helper.rb
171
189
  - test/test_models.rb
172
- - test/thread_test.rb
173
- - test/transaction_test.rb
190
+ - test/with_advisory_lock/base_test.rb
191
+ - test/with_advisory_lock/concern_test.rb
192
+ - test/with_advisory_lock/lock_test.rb
193
+ - test/with_advisory_lock/nesting_test.rb
194
+ - test/with_advisory_lock/options_test.rb
195
+ - test/with_advisory_lock/parallelism_test.rb
196
+ - test/with_advisory_lock/shared_test.rb
197
+ - test/with_advisory_lock/thread_test.rb
198
+ - test/with_advisory_lock/transaction_test.rb
@@ -1,80 +0,0 @@
1
- ---
2
- name: CI
3
-
4
- on:
5
- pull_request:
6
- branches:
7
- - master
8
-
9
- jobs:
10
- minitest:
11
- runs-on: ubuntu-latest
12
- services:
13
- mysql:
14
- image: mysql/mysql-server:5.7
15
- ports:
16
- - "3306:3306"
17
- env:
18
- MYSQL_ROOT_PASSWORD: root
19
- MYSQL_DATABASE: with_advisory_lock_test
20
- MYSQL_ROOT_HOST: '%'
21
- postgres:
22
- image: 'postgres:14-alpine'
23
- ports: ['5432:5432']
24
- env:
25
- POSTGRES_USER: closure_tree
26
- POSTGRES_PASSWORD: closure_tree
27
- POSTGRES_DB: with_advisory_lock_test
28
- options: >-
29
- --health-cmd pg_isready
30
- --health-interval 10s
31
- --health-timeout 5s
32
- --health-retries 5
33
-
34
- strategy:
35
- fail-fast: false
36
- matrix:
37
- ruby:
38
- - '3.2'
39
- - '3.1'
40
- - '3.0'
41
- - '2.7'
42
- - 'truffleruby'
43
- rails:
44
- - activerecord_7.1
45
- - activerecord_7.0
46
- - activerecord_6.1
47
- adapter:
48
- - sqlite3:///tmp/test.sqlite3
49
- - mysql2://root:root@0/with_advisory_lock_test
50
- - trilogy://root:root@0/with_advisory_lock_test
51
- - postgres://closure_tree:closure_tree@0/with_advisory_lock_test
52
- include:
53
- - ruby: jruby
54
- rails: activerecord_6.1
55
- adapter: jdbcmysql://root:root@0/with_advisory_lock_test
56
- - ruby: jruby
57
- rails: activerecord_6.1
58
- adapter: jdbcsqlite3:///tmp/test.sqlite3
59
- - ruby: jruby
60
- rails: activerecord_6.1
61
- adapter: jdbcpostgresql://closure_tree:closure_tree@0/with_advisory_lock_test
62
- steps:
63
- - name: Checkout
64
- uses: actions/checkout@v4
65
-
66
- - name: Setup Ruby
67
- uses: ruby/setup-ruby@v1
68
- with:
69
- ruby-version: ${{ matrix.ruby }}
70
- bundler-cache: true
71
- rubygems: latest
72
- env:
73
- BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
74
-
75
- - name: Test
76
- env:
77
- BUNDLE_GEMFILE: gemfiles/${{ matrix.rails }}.gemfile
78
- DATABASE_URL: ${{ matrix.adapter }}
79
- WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }}
80
- run: bundle exec rake