with_advisory_lock-5.3.0/0000755000004100000410000000000015022647710015456 5ustar www-datawww-datawith_advisory_lock-5.3.0/.release-please-manifest.json0000644000004100000410000000001615022647710023117 0ustar www-datawww-data{".":"5.3.0"} with_advisory_lock-5.3.0/.gitignore0000644000004100000410000000027615022647710017453 0ustar www-datawww-data*.gem *.rbc *.history *.idea .bundle .config .yardoc *.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp *.iml test/sqlite.db .envwith_advisory_lock-5.3.0/.tool-versions0000644000004100000410000000001315022647710020274 0ustar www-datawww-dataruby 3.4.4 with_advisory_lock-5.3.0/.github/0000755000004100000410000000000015022647710017016 5ustar www-datawww-datawith_advisory_lock-5.3.0/.github/workflows/0000755000004100000410000000000015022647710021053 5ustar www-datawww-datawith_advisory_lock-5.3.0/.github/workflows/release.yml0000644000004100000410000000037415022647710023222 0ustar www-datawww-dataname: release-please on: push: branches: - master workflow_dispatch: permissions: contents: write pull-requests: write jobs: release-please: runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 with_advisory_lock-5.3.0/.github/workflows/ci-mysql5.yml0000644000004100000410000000315715022647710023427 0ustar www-datawww-dataname: CI Mysql 5.7 on: pull_request: branches: - master concurrency: group: ci-mysql5-${{ github.head_ref }} cancel-in-progress: true jobs: minitest: runs-on: ubuntu-latest name: CI Mysql 5.7 Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }} services: mysql: image: mysql/mysql-server:5.7 ports: - 3306 env: MYSQL_USER: with_advisory MYSQL_PASSWORD: with_advisory_pass MYSQL_DATABASE: with_advisory_lock_test MYSQL_ROOT_HOST: '%' strategy: fail-fast: false matrix: ruby: # - '3.2' # - '3.1' # - '3.0' # - '2.7' - '3.3' - 'truffleruby' rails: - 7.1 - "7.0" - 6.1 adapter: - mysql2 - trilogy include: - ruby: jruby rails: 6.1 adapter: jdbcmysql steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true rubygems: latest env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile - name: Test env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile DATABASE_URL: ${{ matrix.adapter }}://with_advisory:with_advisory_pass@0:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} run: bundle exec rake with_advisory_lock-5.3.0/.github/workflows/ci-mysql8.yml0000644000004100000410000000334315022647710023427 0ustar www-datawww-dataname: CI Mysql 8.0 on: pull_request: branches: - master concurrency: group: ci-mysql8-${{ github.head_ref }} cancel-in-progress: true jobs: minitest: runs-on: ubuntu-latest name: CI Mysql 8.0 Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }} services: mysql: image: mysql/mysql-server ports: - 3306 env: MYSQL_USER: with_advisory MYSQL_PASSWORD: with_advisory_pass MYSQL_DATABASE: with_advisory_lock_test MYSQL_ROOT_HOST: '%' strategy: fail-fast: false matrix: ruby: # - '3.2' # - '3.1' # - '3.0' # - '2.7' - '3.3' - 'truffleruby' rails: - 7.1 - "7.0" - 6.1 adapter: - mysql2 # - trilogy://with_advisory:with_advisory_pass@0/with_advisory_lock_test Trilogy is not supported by mysql 8 with new encryption include: - ruby: jruby rails: 6.1 adapter: jdbcmysql steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true rubygems: latest env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile - name: Test env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile DATABASE_URL: ${{ matrix.adapter }}://with_advisory:with_advisory_pass@0:${{ job.services.mysql.ports[3306] }}/with_advisory_lock_test WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} run: bundle exec rake with_advisory_lock-5.3.0/.github/workflows/ci-postgresql.yml0000644000004100000410000000335715022647710024402 0ustar www-datawww-dataname: CI Postgresql on: pull_request: branches: - master concurrency: group: ci-postgresql-${{ github.head_ref }} cancel-in-progress: true jobs: minitest: runs-on: ubuntu-latest name: CI Postgresql Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }} services: postgres: image: 'postgres:16-alpine' ports: - '5432' env: POSTGRES_USER: with_advisory POSTGRES_PASSWORD: with_advisory_pass POSTGRES_DB: with_advisory_lock_test options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 strategy: fail-fast: false matrix: ruby: # - '3.2' # - '3.1' # - '3.0' # - '2.7' - '3.3' - 'truffleruby' rails: - 7.1 - "7.0" - 6.1 adapter: - postgres include: - ruby: jruby rails: 6.1 adapter: jdbcpostgresql steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true rubygems: latest env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile - name: Test env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile DATABASE_URL: ${{ matrix.adapter }}://with_advisory:with_advisory_pass@localhost:${{ job.services.postgres.ports[5432] }}/with_advisory_lock_test WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} run: bundle exec rake with_advisory_lock-5.3.0/.github/workflows/ci-sqlite3.yml0000644000004100000410000000237715022647710023564 0ustar www-datawww-dataname: CI Sqlite3 on: pull_request: branches: - master concurrency: group: ci-sqlite3-${{ github.head_ref }} cancel-in-progress: true jobs: minitest: runs-on: ubuntu-latest name: CI Sqlite3 Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / Adapter ${{ matrix.adapter }} strategy: fail-fast: false matrix: ruby: # - '3.2' # - '3.1' # - '3.0' # - '2.7' - '3.3' - 'truffleruby' rails: - 7.1 - "7.0" - 6.1 adapter: - sqlite3 include: - ruby: jruby rails: 6.1 adapter: jdbcsqlite3 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true rubygems: latest env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile - name: Test env: BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile DATABASE_URL: ${{ matrix.adapter }}:///tmp/test.sqlite3 WITH_ADVISORY_LOCK_PREFIX: ${{ github.run_id }} run: bundle exec rake with_advisory_lock-5.3.0/lib/0000755000004100000410000000000015022647710016224 5ustar www-datawww-datawith_advisory_lock-5.3.0/lib/with_advisory_lock.rb0000644000004100000410000000057215022647710022460 0ustar www-datawww-datarequire 'with_advisory_lock/version' require 'active_support' require 'zeitwerk' loader = Zeitwerk::Loader.for_gem loader.inflector.inflect( 'mysql' => 'MySQL', 'postgresql' => 'PostgreSQL', ) loader.setup module WithAdvisoryLock LOCK_PREFIX_ENV = 'WITH_ADVISORY_LOCK_PREFIX'.freeze end ActiveSupport.on_load :active_record do include WithAdvisoryLock::Concern end with_advisory_lock-5.3.0/lib/with_advisory_lock/0000755000004100000410000000000015022647710022127 5ustar www-datawww-datawith_advisory_lock-5.3.0/lib/with_advisory_lock/mysql.rb0000644000004100000410000000161515022647710023624 0ustar www-datawww-data# frozen_string_literal: true module WithAdvisoryLock class MySQL < Base # See https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html # See https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html def try_lock raise ArgumentError, 'shared locks are not supported on MySQL' if shared raise ArgumentError, 'transaction level locks are not supported on MySQL' if transaction execute_successful?("GET_LOCK(#{quoted_lock_str}, 0)") end def release_lock execute_successful?("RELEASE_LOCK(#{quoted_lock_str})") end def execute_successful?(mysql_function) execute_query(mysql_function) == 1 end def execute_query(mysql_function) sql = "SELECT #{mysql_function}" connection.query_value(sql) end # MySQL wants a string as the lock key. def quoted_lock_str connection.quote(lock_str) end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/failed_to_acquire_lock.rb0000644000004100000410000000030615022647710027122 0ustar www-datawww-data# frozen_string_literal: true module WithAdvisoryLock class FailedToAcquireLock < StandardError def initialize(lock_name) super("Failed to acquire lock #{lock_name}") end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/database_adapter_support.rb0000644000004100000410000000076115022647710027520 0ustar www-datawww-data# frozen_string_literal: true module WithAdvisoryLock class DatabaseAdapterSupport attr_reader :adapter_name def initialize(connection) @connection = connection @adapter_name = connection.adapter_name.downcase.to_sym end def mysql? %i[mysql2 trilogy].include? adapter_name end def postgresql? %i[postgresql empostgresql postgis].include? adapter_name end def sqlite? [:sqlite3, :sqlite].include? adapter_name end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/base.rb0000644000004100000410000000572315022647710023375 0ustar www-datawww-data# frozen_string_literal: true require 'zlib' module WithAdvisoryLock class Result attr_reader :result def initialize(lock_was_acquired, result = false) @lock_was_acquired = lock_was_acquired @result = result end def lock_was_acquired? @lock_was_acquired end end FAILED_TO_LOCK = Result.new(false) LockStackItem = Struct.new(:name, :shared) class Base attr_reader :connection, :lock_name, :timeout_seconds, :shared, :transaction, :disable_query_cache def initialize(connection, lock_name, options) options = { timeout_seconds: options } unless options.respond_to?(:fetch) options.assert_valid_keys :timeout_seconds, :shared, :transaction, :disable_query_cache @connection = connection @lock_name = lock_name @timeout_seconds = options.fetch(:timeout_seconds, nil) @shared = options.fetch(:shared, false) @transaction = options.fetch(:transaction, false) @disable_query_cache = options.fetch(:disable_query_cache, false) end def lock_str @lock_str ||= "#{ENV[LOCK_PREFIX_ENV]}#{lock_name}" end def lock_stack_item @lock_stack_item ||= LockStackItem.new(lock_str, shared) end def self.lock_stack # access doesn't need to be synchronized as it is only accessed by the current thread. Thread.current[:with_advisory_lock_stack] ||= [] end delegate :lock_stack, to: 'self.class' def already_locked? lock_stack.include? lock_stack_item end def with_advisory_lock_if_needed(&block) if disable_query_cache return lock_and_yield do connection.uncached(&block) end end lock_and_yield(&block) end def lock_and_yield(&block) if already_locked? Result.new(true, yield) elsif timeout_seconds == 0 yield_with_lock(&block) else yield_with_lock_and_timeout(&block) end end def stable_hashcode(input) if input.is_a? Numeric input.to_i else # Ruby MRI's String#hash is randomly seeded as of Ruby 1.9 so # make sure we use a deterministic hash. Zlib.crc32(input.to_s, 0) end end def yield_with_lock_and_timeout(&block) give_up_at = Time.now + @timeout_seconds if @timeout_seconds while @timeout_seconds.nil? || Time.now < give_up_at r = yield_with_lock(&block) return r if r.lock_was_acquired? # Randomizing sleep time may help reduce contention. sleep(rand(0.05..0.15)) end FAILED_TO_LOCK end def yield_with_lock if try_lock begin lock_stack.push(lock_stack_item) result = block_given? ? yield : nil Result.new(true, result) ensure lock_stack.pop release_lock end else FAILED_TO_LOCK end end # Prevent AR from caching results improperly def unique_column_name "t#{SecureRandom.hex}" end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/postgresql.rb0000644000004100000410000000343415022647710024663 0ustar www-datawww-data# frozen_string_literal: true module WithAdvisoryLock class PostgreSQL < Base # See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS # MRI returns 't', jruby returns true. YAY! LOCK_RESULT_VALUES = ['t', true].freeze PG_ADVISORY_UNLOCK = 'pg_advisory_unlock' PG_TRY_ADVISORY = 'pg_try_advisory' ERROR_MESSAGE_REGEX = / ERROR: +current transaction is aborted,/ def try_lock execute_successful?(advisory_try_lock_function(transaction)) end def release_lock return if transaction execute_successful?(advisory_unlock_function) rescue ActiveRecord::StatementInvalid => e raise unless e.message =~ ERROR_MESSAGE_REGEX begin connection.rollback_db_transaction execute_successful?(advisory_unlock_function) ensure connection.begin_db_transaction end end def advisory_try_lock_function(transaction_scope) [ 'pg_try_advisory', transaction_scope ? '_xact' : nil, '_lock', shared ? '_shared' : nil ].compact.join end def advisory_unlock_function [ 'pg_advisory_unlock', shared ? '_shared' : nil ].compact.join end def execute_successful?(pg_function) result = connection.select_value(prepare_sql(pg_function)) LOCK_RESULT_VALUES.include?(result) end def prepare_sql(pg_function) comment = lock_name.to_s.gsub(%r{(/\*)|(\*/)}, '--') "SELECT #{pg_function}(#{lock_keys.join(',')}) AS #{unique_column_name} /* #{comment} */" end # PostgreSQL wants 2 32bit integers as the lock key. def lock_keys @lock_keys ||= [ stable_hashcode(lock_name), ENV[LOCK_PREFIX_ENV] ].map { |ea| ea.to_i & 0x7fffffff } end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/flock.rb0000644000004100000410000000145115022647710023553 0ustar www-datawww-data# frozen_string_literal: true require 'fileutils' module WithAdvisoryLock class Flock < Base def filename @filename ||= begin safe = lock_str.to_s.gsub(/[^a-z0-9]/i, '') fn = ".lock-#{safe}-#{stable_hashcode(lock_str)}" # Let the user specify a directory besides CWD. ENV['FLOCK_DIR'] ? File.expand_path(fn, ENV['FLOCK_DIR']) : fn end end def file_io @file_io ||= begin FileUtils.touch(filename) File.open(filename, 'r+') end end def try_lock raise ArgumentError, 'transaction level locks are not supported on SQLite' if transaction 0 == file_io.flock((shared ? File::LOCK_SH : File::LOCK_EX) | File::LOCK_NB) end def release_lock 0 == file_io.flock(File::LOCK_UN) end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/concern.rb0000644000004100000410000000315715022647710024111 0ustar www-datawww-data# frozen_string_literal: true module WithAdvisoryLock module Concern extend ActiveSupport::Concern delegate :with_advisory_lock, :with_advisory_lock!, :advisory_lock_exists?, to: 'self.class' class_methods do def with_advisory_lock(lock_name, options = {}, &block) result = with_advisory_lock_result(lock_name, options, &block) result.lock_was_acquired? ? result.result : false end def with_advisory_lock!(lock_name, options = {}, &block) result = with_advisory_lock_result(lock_name, options, &block) raise WithAdvisoryLock::FailedToAcquireLock, lock_name unless result.lock_was_acquired? result.result end def with_advisory_lock_result(lock_name, options = {}, &block) impl = impl_class.new(connection, lock_name, options) impl.with_advisory_lock_if_needed(&block) end def advisory_lock_exists?(lock_name) impl = impl_class.new(connection, lock_name, 0) impl.already_locked? || !impl.yield_with_lock.lock_was_acquired? end def current_advisory_lock lock_stack_key = WithAdvisoryLock::Base.lock_stack.first lock_stack_key && lock_stack_key[0] end def current_advisory_locks WithAdvisoryLock::Base.lock_stack.map(&:name) end private def impl_class adapter = WithAdvisoryLock::DatabaseAdapterSupport.new(connection) if adapter.postgresql? WithAdvisoryLock::PostgreSQL elsif adapter.mysql? WithAdvisoryLock::MySQL else WithAdvisoryLock::Flock end end end end end with_advisory_lock-5.3.0/lib/with_advisory_lock/version.rb0000644000004100000410000000014115022647710024135 0ustar www-datawww-data# frozen_string_literal: true module WithAdvisoryLock VERSION = Gem::Version.new('5.3.0') end with_advisory_lock-5.3.0/Appraisals0000644000004100000410000000162015022647710017477 0ustar www-datawww-data# frozen_string_literal: true appraise 'activerecord-7.1' do gem 'activerecord', '~> 7.1.0' platforms :ruby do gem 'sqlite3' gem 'mysql2' gem 'trilogy' gem 'pg' end end appraise 'activerecord-7.0' do gem 'activerecord', '~> 7.0.0' platforms :ruby do gem 'sqlite3' gem 'mysql2' gem 'trilogy' gem "activerecord-trilogy-adapter" gem 'pg' end platforms :jruby do gem "activerecord-jdbcmysql-adapter" gem "activerecord-jdbcpostgresql-adapter" gem "activerecord-jdbcsqlite3-adapter" end end appraise 'activerecord-6.1' do gem 'activerecord', '~> 6.1.0' platforms :ruby do gem 'sqlite3' gem 'mysql2' gem 'trilogy' gem "activerecord-trilogy-adapter" gem 'pg' end platforms :jruby do gem "activerecord-jdbcmysql-adapter" gem "activerecord-jdbcpostgresql-adapter" gem "activerecord-jdbcsqlite3-adapter" end end with_advisory_lock-5.3.0/LICENSE.txt0000644000004100000410000000220015022647710017273 0ustar www-datawww-dataSPDX-License-Identifier: MIT SPDX-FileCopyrightText: 2013 Matthew McEachen SPDX-FileCopyrightText: 2013-2025 Abdelkader Boudih Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. with_advisory_lock-5.3.0/test/0000755000004100000410000000000015022647710016435 5ustar www-datawww-datawith_advisory_lock-5.3.0/test/with_advisory_lock/0000755000004100000410000000000015022647710022340 5ustar www-datawww-datawith_advisory_lock-5.3.0/test/with_advisory_lock/shared_test.rb0000644000004100000410000000555015022647710025177 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class SharedTestWorker def initialize(shared) @shared = shared @locked = nil @cleanup = false @thread = Thread.new { work } end def locked? sleep 0.01 while @locked.nil? && @thread.alive? @locked end def cleanup! @cleanup = true @thread.join raise if @thread.status.nil? end private def work Tag.connection_pool.with_connection do Tag.with_advisory_lock('test', timeout_seconds: 0, shared: @shared) do @locked = true sleep 0.01 until @cleanup end @locked = false sleep 0.01 until @cleanup end end end class SharedLocksTest < GemTestCase def supported? %i[trilogy mysql2 jdbcmysql].exclude?(env_db) end test 'does not allow two exclusive locks' do one = SharedTestWorker.new(false) assert_predicate(one, :locked?) two = SharedTestWorker.new(false) refute(two.locked?) one.cleanup! two.cleanup! end end class NotSupportedEnvironmentTest < SharedLocksTest setup do skip if supported? end test 'raises an error when attempting to use a shared lock' do one = SharedTestWorker.new(true) assert_nil(one.locked?) exception = assert_raises(ArgumentError) do one.cleanup! end assert_match(/#{Regexp.escape('not supported')}/, exception.message) end end class SupportedEnvironmentTest < SharedLocksTest setup do skip unless supported? end test 'does allow two shared locks' do one = SharedTestWorker.new(true) assert_predicate(one, :locked?) two = SharedTestWorker.new(true) assert_predicate(two, :locked?) one.cleanup! two.cleanup! end test 'does not allow exclusive lock with shared lock' do one = SharedTestWorker.new(true) assert_predicate(one, :locked?) two = SharedTestWorker.new(false) refute(two.locked?) three = SharedTestWorker.new(true) assert_predicate(three, :locked?) one.cleanup! two.cleanup! three.cleanup! end test 'does not allow shared lock with exclusive lock' do one = SharedTestWorker.new(false) assert_predicate(one, :locked?) two = SharedTestWorker.new(true) refute(two.locked?) one.cleanup! two.cleanup! end class PostgreSQLTest < SupportedEnvironmentTest setup do skip unless env_db == :postgresql end def pg_lock_modes Tag.connection.select_values("SELECT mode FROM pg_locks WHERE locktype = 'advisory';") end test 'allows shared lock to be upgraded to an exclusive lock' do assert_empty(pg_lock_modes) Tag.with_advisory_lock 'test', shared: true do assert_equal(%w[ShareLock], pg_lock_modes) Tag.with_advisory_lock 'test', shared: false do assert_equal(%w[ShareLock ExclusiveLock], pg_lock_modes) end end assert_empty(pg_lock_modes) end end end with_advisory_lock-5.3.0/test/with_advisory_lock/parallelism_test.rb0000644000004100000410000000427115022647710026235 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' require 'forwardable' class FindOrCreateWorker extend Forwardable def_delegators :@thread, :join, :wakeup, :status, :to_s def initialize(name, use_advisory_lock) @name = name @use_advisory_lock = use_advisory_lock @thread = Thread.new { work_later } end def work_later sleep ApplicationRecord.connection_pool.with_connection do if @use_advisory_lock Tag.with_advisory_lock(@name) { work } else work end end end def work Tag.transaction do Tag.where(name: @name).first_or_create end end end class ParallelismTest < GemTestCase def run_workers @names = @iterations.times.map { |iter| "iteration ##{iter}" } @names.each do |name| workers = @workers.times.map do FindOrCreateWorker.new(name, @use_advisory_lock) end # Wait for all the threads to get ready: sleep(0.1) until workers.all? { |ea| ea.status == 'sleep' } # OK, GO! workers.each(&:wakeup) # Then wait for them to finish: workers.each(&:join) end # Ensure we're still connected: ApplicationRecord.connection_pool.connection end setup do ApplicationRecord.connection.reconnect! @workers = 10 end test 'creates multiple duplicate rows without advisory locks' do skip if %i[sqlite3 jdbcsqlite3].include?(env_db) @use_advisory_lock = false @iterations = 1 run_workers assert_operator(Tag.all.size, :>, @iterations) # <- any duplicated rows will make me happy. assert_operator(TagAudit.all.size, :>, @iterations) # <- any duplicated rows will make me happy. assert_operator(Label.all.size, :>, @iterations) # <- any duplicated rows will make me happy. end test "doesn't create multiple duplicate rows with advisory locks" do @use_advisory_lock = true @iterations = 10 run_workers assert_equal(@iterations, Tag.all.size) # <- any duplicated rows will NOT make me happy. assert_equal(@iterations, TagAudit.all.size) # <- any duplicated rows will NOT make me happy. assert_equal(@iterations, Label.all.size) # <- any duplicated rows will NOT make me happy. end end with_advisory_lock-5.3.0/test/with_advisory_lock/concern_test.rb0000644000004100000410000000171515022647710025357 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class WithAdvisoryLockConcernTest < GemTestCase test 'adds with_advisory_lock to ActiveRecord classes' do assert_respond_to(Tag, :with_advisory_lock) end test 'adds with_advisory_lock to ActiveRecord instances' do assert_respond_to(Label.new, :with_advisory_lock) end test 'adds advisory_lock_exists? to ActiveRecord classes' do assert_respond_to(Tag, :advisory_lock_exists?) end test 'adds advisory_lock_exists? to ActiveRecord instances' do assert_respond_to(Label.new, :advisory_lock_exists?) end end class ActiveRecordQueryCacheTest < GemTestCase test 'does not disable quary cache by default' do Tag.connection.expects(:uncached).never Tag.with_advisory_lock('lock') { Tag.first } end test 'can disable ActiveRecord query cache' do Tag.connection.expects(:uncached).once Tag.with_advisory_lock('a-lock', disable_query_cache: true) { Tag.first } end end with_advisory_lock-5.3.0/test/with_advisory_lock/transaction_test.rb0000644000004100000410000000371015022647710026252 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class TransactionScopingTest < GemTestCase def supported? %i[postgresql jdbcpostgresql].include?(env_db) end test 'raises an error when attempting to use transaction level locks if not supported' do skip if supported? Tag.transaction do exception = assert_raises(ArgumentError) do Tag.with_advisory_lock 'test', transaction: true do raise 'should not get here' end end assert_match(/#{Regexp.escape('not supported')}/, exception.message) end end class PostgresqlTest < TransactionScopingTest setup do skip unless env_db == :postgresql @pg_lock_count = lambda do ApplicationRecord.connection.select_value("SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';").to_i end end test 'session locks release after the block executes' do Tag.transaction do assert_equal(0, @pg_lock_count.call) Tag.with_advisory_lock 'test' do assert_equal(1, @pg_lock_count.call) end assert_equal(0, @pg_lock_count.call) end end test 'session locks release when transaction fails inside block' do Tag.transaction do assert_equal(0, @pg_lock_count.call) exception = assert_raises(ActiveRecord::StatementInvalid) do Tag.with_advisory_lock 'test' do Tag.connection.execute 'SELECT 1/0;' end end assert_match(/#{Regexp.escape('division by zero')}/, exception.message) assert_equal(0, @pg_lock_count.call) end end test 'transaction level locks hold until the transaction completes' do Tag.transaction do assert_equal(0, @pg_lock_count.call) Tag.with_advisory_lock 'test', transaction: true do assert_equal(1, @pg_lock_count.call) end assert_equal(1, @pg_lock_count.call) end assert_equal(0, @pg_lock_count.call) end end end with_advisory_lock-5.3.0/test/with_advisory_lock/base_test.rb0000644000004100000410000000032315022647710024634 0ustar www-datawww-datarequire 'test_helper' class WithAdvisoryLockBaseTest < GemTestCase test 'should support advisory_locks_enabled' do skip if is_sqlite3_adapter? assert Tag.connection.advisory_locks_enabled? end end with_advisory_lock-5.3.0/test/with_advisory_lock/nesting_test.rb0000644000004100000410000000141115022647710025370 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class LockNestingTest < GemTestCase setup do @prior_prefix = ENV['WITH_ADVISORY_LOCK_PREFIX'] ENV['WITH_ADVISORY_LOCK_PREFIX'] = nil end teardown do ENV['WITH_ADVISORY_LOCK_PREFIX'] = @prior_prefix end test "doesn't request the same lock twice" do impl = WithAdvisoryLock::Base.new(nil, nil, nil) assert_empty(impl.lock_stack) Tag.with_advisory_lock('first') do assert_equal(%w[first], impl.lock_stack.map(&:name)) # Even MySQL should be OK with this: Tag.with_advisory_lock('first') do assert_equal(%w[first], impl.lock_stack.map(&:name)) end assert_equal(%w[first], impl.lock_stack.map(&:name)) end assert_empty(impl.lock_stack) end end with_advisory_lock-5.3.0/test/with_advisory_lock/lock_test.rb0000644000004100000410000000620215022647710024654 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class LockTest < GemTestCase setup do @lock_name = 'test lock' @return_val = 1900 end test 'returns nil outside an advisory lock request' do assert_nil(Tag.current_advisory_lock) end test 'returns the name of the last lock acquired' do Tag.with_advisory_lock(@lock_name) do assert_match(/#{@lock_name}/, Tag.current_advisory_lock) end end test 'can obtain a lock with a name that attempts to disrupt a SQL comment' do dangerous_lock_name = 'test */ lock /*' Tag.with_advisory_lock(dangerous_lock_name) do assert_match(/#{Regexp.escape(dangerous_lock_name)}/, Tag.current_advisory_lock) end end test 'returns false for an unacquired lock' do refute(Tag.advisory_lock_exists?(@lock_name)) end test 'returns true for an acquired lock' do Tag.with_advisory_lock(@lock_name) do assert(Tag.advisory_lock_exists?(@lock_name)) end end test 'returns block return value if lock successful' do assert_equal(@return_val, Tag.with_advisory_lock!(@lock_name) { @return_val }) end test 'returns false on lock acquisition failure' do thread_with_lock = Thread.new do Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do @locked_elsewhere = true loop { sleep 0.01 } end end sleep 0.01 until @locked_elsewhere assert_not(Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) { @return_val }) thread_with_lock.kill end test 'raises an error on lock acquisition failure' do thread_with_lock = Thread.new do Tag.with_advisory_lock(@lock_name, timeout_seconds: 0) do @locked_elsewhere = true loop { sleep 0.01 } end end sleep 0.01 until @locked_elsewhere assert_raises(WithAdvisoryLock::FailedToAcquireLock) do Tag.with_advisory_lock!(@lock_name, timeout_seconds: 0) { @return_val } end thread_with_lock.kill end test 'attempts the lock exactly once with no timeout' do expected = SecureRandom.base64 actual = Tag.with_advisory_lock(@lock_name, 0) do expected end assert_equal(expected, actual) end test 'current_advisory_locks returns empty array outside an advisory lock request' do assert_equal([], Tag.current_advisory_locks) end test 'current_advisory_locks returns an array with names of the acquired locks' do Tag.with_advisory_lock(@lock_name) do locks = Tag.current_advisory_locks assert_equal(1, locks.size) assert_match(/#{@lock_name}/, locks.first) end end test 'current_advisory_locks returns array of all nested lock names' do first_lock = 'outer lock' second_lock = 'inner lock' Tag.with_advisory_lock(first_lock) do Tag.with_advisory_lock(second_lock) do locks = Tag.current_advisory_locks assert_equal(2, locks.size) assert_match(/#{first_lock}/, locks.first) assert_match(/#{second_lock}/, locks.last) end locks = Tag.current_advisory_locks assert_equal(1, locks.size) assert_match(/#{first_lock}/, locks.first) end assert_equal([], Tag.current_advisory_locks) end end with_advisory_lock-5.3.0/test/with_advisory_lock/options_test.rb0000644000004100000410000000325515022647710025424 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class OptionsParsingTest < GemTestCase def parse_options(options) WithAdvisoryLock::Base.new(mock, mock, options) end test 'defaults (empty hash)' do impl = parse_options({}) assert_nil(impl.timeout_seconds) assert_not(impl.shared) assert_not(impl.transaction) end test 'nil sets timeout to nil' do impl = parse_options(nil) assert_nil(impl.timeout_seconds) assert_not(impl.shared) assert_not(impl.transaction) end test 'integer sets timeout to value' do impl = parse_options(42) assert_equal(42, impl.timeout_seconds) assert_not(impl.shared) assert_not(impl.transaction) end test 'hash with invalid key errors' do assert_raises(ArgumentError) do parse_options(foo: 42) end end test 'hash with timeout_seconds sets timeout to value' do impl = parse_options(timeout_seconds: 123) assert_equal(123, impl.timeout_seconds) assert_not(impl.shared) assert_not(impl.transaction) end test 'hash with shared option sets shared to true' do impl = parse_options(shared: true) assert_nil(impl.timeout_seconds) assert(impl.shared) assert_not(impl.transaction) end test 'hash with transaction option set transaction to true' do impl = parse_options(transaction: true) assert_nil(impl.timeout_seconds) assert_not(impl.shared) assert(impl.transaction) end test 'hash with multiple keys sets options' do foo = mock bar = mock impl = parse_options(timeout_seconds: foo, shared: bar) assert_equal(foo, impl.timeout_seconds) assert_equal(bar, impl.shared) assert_not(impl.transaction) end end with_advisory_lock-5.3.0/test/with_advisory_lock/thread_test.rb0000644000004100000410000000302315022647710025171 0ustar www-datawww-data# frozen_string_literal: true require 'test_helper' class SeparateThreadTest < GemTestCase setup do @lock_name = 'testing 1,2,3' # OMG COMMAS @mutex = Mutex.new @t1_acquired_lock = false @t1_return_value = nil @t1 = Thread.new do Label.connection_pool.with_connection do @t1_return_value = Label.with_advisory_lock(@lock_name) do @mutex.synchronize { @t1_acquired_lock = true } sleep 't1 finished' end end end # Wait for the thread to acquire the lock: sleep(0.1) until @mutex.synchronize { @t1_acquired_lock } Label.connection.reconnect! end teardown do @t1.wakeup if @t1.status == 'sleep' @t1.join end test '#with_advisory_lock with a 0 timeout returns false immediately' do response = Label.with_advisory_lock(@lock_name, 0) do raise 'should not be yielded to' end assert_not(response) end test '#with_advisory_lock yields to the provided block' do assert(@t1_acquired_lock) end test '#advisory_lock_exists? returns true when another thread has the lock' do assert(Tag.advisory_lock_exists?(@lock_name)) end test 'can re-establish the lock after the other thread releases it' do @t1.wakeup @t1.join assert_equal('t1 finished', @t1_return_value) # We should now be able to acquire the lock immediately: reacquired = false lock_result = Label.with_advisory_lock(@lock_name, 0) do reacquired = true end assert(lock_result) assert(reacquired) end end with_advisory_lock-5.3.0/test/test_helper.rb0000644000004100000410000000353115022647710021302 0ustar www-datawww-data# frozen_string_literal: true require 'erb' require 'active_record' require 'with_advisory_lock' require 'tmpdir' require 'securerandom' begin require 'activerecord-trilogy-adapter' ActiveSupport.on_load(:active_record) do require "trilogy_adapter/connection" ActiveRecord::Base.public_send :extend, TrilogyAdapter::Connection end rescue LoadError # do nothing end ActiveRecord::Base.configurations = { default_env: { url: ENV.fetch('DATABASE_URL', "sqlite3://#{Dir.tmpdir}/with_advisory_lock_test#{RUBY_VERSION}-#{ActiveRecord.gem_version}.sqlite3"), pool: 20, properties: { allowPublicKeyRetrieval: true } # for JRuby madness } } ENV['WITH_ADVISORY_LOCK_PREFIX'] ||= SecureRandom.hex ActiveRecord::Base.establish_connection def env_db @env_db ||= ActiveRecord::Base.connection_db_config.adapter.to_sym end ActiveRecord::Migration.verbose = false require 'test_models' require 'minitest' require 'maxitest/autorun' require 'mocha/minitest' class GemTestCase < ActiveSupport::TestCase parallelize(workers: 1) def adapter_support @adapter_support ||= WithAdvisoryLock::DatabaseAdapterSupport.new(ActiveRecord::Base.connection) end def is_sqlite3_adapter?; adapter_support.sqlite?; end def is_mysql_adapter?; adapter_support.mysql?; end def is_postgresql_adapter?; adapter_support.postgresql?; end setup do ENV['FLOCK_DIR'] = Dir.mktmpdir if is_sqlite3_adapter? ApplicationRecord.connection.truncate_tables( Tag.table_name, TagAudit.table_name, Label.table_name ) end teardown do FileUtils.remove_entry_secure(ENV['FLOCK_DIR'], true) if is_sqlite3_adapter? end end puts "Testing with #{env_db} database, ActiveRecord #{ActiveRecord.gem_version} and #{RUBY_ENGINE} #{RUBY_ENGINE_VERSION} as #{RUBY_VERSION}" puts "Connection Pool size: #{ActiveRecord::Base.connection_pool.size}" with_advisory_lock-5.3.0/test/test_models.rb0000644000004100000410000000112215022647710021300 0ustar www-datawww-data# frozen_string_literal: true ActiveRecord::Schema.define(version: 1) do create_table 'tags', force: true do |t| t.string 'name' end create_table 'tag_audits', id: false, force: true do |t| t.string 'tag_name' end create_table 'labels', id: false, force: true do |t| t.string 'name' end end class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end class Tag < ApplicationRecord after_save do TagAudit.create(tag_name: name) Label.create(name: name) end end class TagAudit < ApplicationRecord end class Label < ApplicationRecord end with_advisory_lock-5.3.0/gemfiles/0000755000004100000410000000000015022647710017251 5ustar www-datawww-datawith_advisory_lock-5.3.0/gemfiles/activerecord_7.0.gemfile0000644000004100000410000000060715022647710023644 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 7.0.0" platforms :ruby do gem "sqlite3" gem "mysql2" gem "trilogy" gem "activerecord-trilogy-adapter" gem "pg" end platforms :jruby do gem "activerecord-jdbcmysql-adapter" gem "activerecord-jdbcpostgresql-adapter" gem "activerecord-jdbcsqlite3-adapter" end gemspec path: "../" with_advisory_lock-5.3.0/gemfiles/activerecord_6.1.gemfile0000644000004100000410000000060715022647710023644 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 6.1.0" platforms :ruby do gem "sqlite3" gem "mysql2" gem "trilogy" gem "activerecord-trilogy-adapter" gem "pg" end platforms :jruby do gem "activerecord-jdbcmysql-adapter" gem "activerecord-jdbcpostgresql-adapter" gem "activerecord-jdbcsqlite3-adapter" end gemspec path: "../" with_advisory_lock-5.3.0/gemfiles/activerecord_7.1.gemfile0000644000004100000410000000031515022647710023641 0ustar www-datawww-data# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 7.1.0" platforms :ruby do gem "sqlite3" gem "mysql2" gem "trilogy" gem "pg" end gemspec path: "../" with_advisory_lock-5.3.0/Rakefile0000644000004100000410000000045115022647710017123 0ustar www-datawww-datarequire "bundler/gem_tasks" require 'yard' YARD::Rake::YardocTask.new do |t| t.files = ['lib/**/*.rb', 'README.md'] end require 'rake/testtask' Rake::TestTask.new do |t| t.libs.push 'lib' t.libs.push 'test' t.pattern = 'test/**/*_test.rb' t.verbose = true end task :default => :test with_advisory_lock-5.3.0/with_advisory_lock.gemspec0000644000004100000410000000267615022647710022741 0ustar www-datawww-data# frozen_string_literal: true require 'English' require_relative 'lib/with_advisory_lock/version' Gem::Specification.new do |spec| spec.name = 'with_advisory_lock' spec.version = WithAdvisoryLock::VERSION spec.authors = ['Matthew McEachen', 'Abdelkader Boudih'] spec.email = %w[matthew+github@mceachen.org terminale@gmail.com] spec.homepage = 'https://github.com/ClosureTree/with_advisory_lock' spec.summary = 'Advisory locking for ActiveRecord' spec.description = 'Advisory locking for ActiveRecord' spec.license = 'MIT' spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) spec.test_files = spec.files.grep(%r{^test/}) spec.require_paths = %w[lib] spec.metadata = { 'rubygems_mfa_required' => 'true' } spec.required_ruby_version = '>= 2.7.0' spec.metadata['yard.run'] = 'yri' spec.metadata['homepage_uri'] = spec.homepage spec.metadata['source_code_uri'] = 'https://github.com/ClosureTree/with_advisory_lock' spec.metadata['changelog_uri'] = 'https://github.com/ClosureTree/with_advisory_lock/blob/master/CHANGELOG.md' spec.add_runtime_dependency 'activerecord', '>= 6.1' spec.add_runtime_dependency 'zeitwerk', '>= 2.6' spec.add_development_dependency 'appraisal' spec.add_development_dependency 'maxitest' spec.add_development_dependency 'minitest-reporters' spec.add_development_dependency 'mocha' spec.add_development_dependency 'yard' end with_advisory_lock-5.3.0/Gemfile0000644000004100000410000000004715022647710016752 0ustar www-datawww-datasource 'https://rubygems.org' gemspec with_advisory_lock-5.3.0/release-please-config.json0000644000004100000410000000021415022647710022500 0ustar www-datawww-data{ "release-type": "ruby", "packages": { ".": { "release-type": "ruby", "package-name": "with_advisory_lock" } } } with_advisory_lock-5.3.0/docker-compose.yml0000644000004100000410000000074615022647710021122 0ustar www-datawww-dataversion: "3.9" services: pg: image: postgres:16 environment: POSTGRES_USER: with_advisory POSTGRES_PASSWORD: with_advisory_pass POSTGRES_DB: with_advisory_lock_test ports: - "5432:5432" mysql: image: mysql:8 environment: MYSQL_USER: with_advisory MYSQL_PASSWORD: with_advisory_pass MYSQL_DATABASE: with_advisory_lock_test MYSQL_RANDOM_ROOT_PASSWORD: "yes" MYSQL_ROOT_HOST: '%' ports: - "3306:3306" with_advisory_lock-5.3.0/README.md0000644000004100000410000001613515022647710016743 0ustar www-datawww-data# with_advisory_lock Adds advisory locking (mutexes) to ActiveRecord 6.0+, with ruby 2.7+, jruby or truffleruby, when used with [MySQL](https://dev.mysql.com/doc/refman/8.0/en/miscellaneous-functions.html#function_get-lock) or [PostgreSQL](https://www.postgresql.org/docs/current/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS). SQLite resorts to file locking. [![Gem Version](https://badge.fury.io/rb/with_advisory_lock.svg)](https://badge.fury.io/rb/with_advisory_lock) [![CI](https://github.com/ClosureTree/with_advisory_lock/actions/workflows/ci.yml/badge.svg)](https://github.com/ClosureTree/with_advisory_lock/actions/workflows/ci.yml) ## What's an "Advisory Lock"? An advisory lock is a [mutex](https://en.wikipedia.org/wiki/Mutual_exclusion) used to ensure no two processes run some process at the same time. When the advisory lock is powered by your database server, as long as it isn't SQLite, your mutex spans hosts. ## Usage This gem automatically includes the `WithAdvisoryLock` module in all of your ActiveRecord models. Here's an example of how to use it where `User` is an ActiveRecord model, and `lock_name` is some string: ```ruby User.with_advisory_lock(lock_name) do do_something_that_needs_locking end ``` ### What happens 1. The thread will wait indefinitely until the lock is acquired. 2. While inside the block, you will exclusively own the advisory lock. 3. The lock will be released after your block ends, even if an exception is raised in the block. ### Lock wait timeouts `with_advisory_lock` takes an options hash as the second parameter. The `timeout_seconds` option defaults to `nil`, which means wait indefinitely for the lock. A value of zero will try the lock only once. If the lock is acquired, the block will be yielded to. If the lock is currently being held, the block will not be called. > **Note** > > If a non-nil value is provided for `timeout_seconds`, the block will *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. For backwards compatability, the timeout value can be specified directly as the second parameter. ### Shared locks The `shared` option defaults to `false` which means an exclusive lock will be obtained. Setting `shared` to `true` will allow locks to be obtained by multiple actors as long as they are all shared locks. Note: MySQL does not support shared locks. ### Transaction-level locks PostgreSQL supports transaction-level locks which remain held until the transaction completes. You can enable this by setting the `transaction` option to `true`. Note: transaction-level locks will not be reflected by `.current_advisory_lock` when the block has returned. ### Return values The return value of `with_advisory_lock_result` is a `WithAdvisoryLock::Result` instance, which has a `lock_was_acquired?` method and a `result` accessor method, which is the returned value of the given block. If your block may validly return false, you should use this method. The return value of `with_advisory_lock` will be the result of the yielded block, if the lock was able to be acquired and the block yielded, or `false`, if you provided a timeout_seconds value and the lock was not able to be acquired in time. `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. ### Testing for the current lock status If you needed to check if the advisory lock is currently being held, you can call `Tag.advisory_lock_exists?("foo")`, but realize the lock can be acquired between the time you test for the lock, and the time you try to acquire the lock. If you want to see if the current Thread is holding a lock, you can call `Tag.current_advisory_lock` which will return the name of the current lock. If no lock is currently held, `.current_advisory_lock` returns `nil`. ### ActiveRecord Query Cache You can optionally pass `disable_query_cache: true` to the options hash of `with_advisory_lock` in order to disable ActiveRecord's query cache. This can prevent problems when you query the database from within the lock and it returns stale results. More info on why this can be a problem can be [found here](https://github.com/ClosureTree/with_advisory_lock/issues/52) ## Installation Add this line to your application's Gemfile: ```ruby gem 'with_advisory_lock' ``` And then execute: $ bundle ## Lock Types First off, know that there are **lots** of different kinds of locks available to you. **Pick the finest-grain lock that ensures correctness.** If you choose a lock that is too coarse, you are unnecessarily blocking other processes. ### Advisory locks These are named mutexes that are inherently "application level"—it is up to the application to acquire, run a critical code section, and release the advisory lock. ### Row-level locks Whether [optimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) or [pessimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html), row-level locks prevent concurrent modification to a given model. **If you're building a [CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) application, this will be 2.4, 2.5 and your most commonly used lock.** ### Table-level locks Provided through something like the [monogamy](https://github.com/ClosureTree/monogamy) gem, these prevent concurrent access to **any instance of a model**. Their coarseness means they aren't going to be commonly applicable, and they can be a source of [deadlocks](http://en.wikipedia.org/wiki/Deadlock). ## FAQ ### Transactions and Advisory Locks Advisory locks with MySQL and PostgreSQL ignore database transaction boundaries. You will want to wrap your block within a transaction to ensure consistency. ### MySQL < 5.7.5 doesn't support nesting With MySQL < 5.7.5, if you ask for a _different_ advisory lock within a `with_advisory_lock` block, you will be releasing the parent lock (!!!). A `NestedAdvisoryLockError`will be raised in this case. If you ask for the same lock name, `with_advisory_lock` won't ask for the lock again, and the block given will be yielded to. This is not an issue in MySQL >= 5.7.5, and no error will be raised for nested lock usage. You can override this by passing `force_nested_lock_support: true` or `force_nested_lock_support: false` to the `with_advisory_lock` options. ### Is clustered MySQL supported? [No.](https://github.com/ClosureTree/with_advisory_lock/issues/16) ### There are many `lock-*` files in my project directory after test runs This is expected if you aren't using MySQL or Postgresql for your tests. See [issue 3](https://github.com/ClosureTree/with_advisory_lock/issues/3). SQLite doesn't have advisory locks, so we resort to file locking, which will only work if the `FLOCK_DIR` is set consistently for all ruby processes. In your `spec_helper.rb` or `minitest_helper.rb`, add a `before` and `after` block: ```ruby before do ENV['FLOCK_DIR'] = Dir.mktmpdir end after do FileUtils.remove_entry_secure ENV['FLOCK_DIR'] end ``` with_advisory_lock-5.3.0/.ruby-version0000644000004100000410000000000715022647710020120 0ustar www-datawww-data3.4.4 with_advisory_lock-5.3.0/CHANGELOG.md0000644000004100000410000001723415022647710017276 0ustar www-datawww-data## Changelog ## [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) ### Features * 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)) ## [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) ### Features * 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)) ### Bug Fixes * 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)) ## [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) ### Features * use zeitwerk loader instead of ActiveSupport::Autoload ([b5082fd](https://github.com/ClosureTree/with_advisory_lock/commit/b5082fddacacacff48139f5bf509601a37945a0e)) ## 5.0.1 (2024-01-21) ### Features * add release workflow ([5d32520](https://github.com/ClosureTree/with_advisory_lock/commit/5d325201c82974991381a9fbc4d1714c9739dc4f)) * 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)) * 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)) * add truffleruby support ([#62](https://github.com/ClosureTree/with_advisory_lock/issues/62)) ([ec34bd4](https://github.com/ClosureTree/with_advisory_lock/commit/ec34bd448e3505e5df631daaf47bb83f2f5316dc)) ### Bug Fixes * 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)) ### 5.0.0 - Drop support for EOL rubies and activerecord (ruby below 2.7 and activerecord below 6.1). - Allow lock name to be integer - Jruby support - Truffleruby support - Add `with_advisory_lock!`, which raises an error if the lock acquisition fails - Add `disable_query_cache` option to `with_advisory_lock` - Drop support for mysql < 5.7.5 ### 4.6.0 - Support for ActiveRecord 6 - Add Support for nested locks in MySQL ### 4.0.0 - Drop support for unsupported versions of activerecord - Drop support for unsupported versions of ruby ### 3.2.0 - [Joshua Flanagan](https://github.com/joshuaflanagan) [added a SQL comment to the lock query for PostgreSQL](https://github.com/ClosureTree/with_advisory_lock/pull/28). Thanks! - [Fernando Luizão](https://github.com/fernandoluizao) found a spurious requirement for `thread_safe`. Thanks for the [fix](https://github.com/ClosureTree/with_advisory_lock/pull/27)! ### 3.1.1 - [Joel Turkel](https://github.com/jturkel) added `require 'active_support'` (it was required, but relied on downstream gems to pull in active_support before pulling in with_advisory_lock). Thanks! ### 3.1.0 - [Jason Weathered](https://github.com/jasoncodes) Added new shared and transaction-level lock options ([Pull request 21](https://github.com/ClosureTree/with_advisory_lock/pull/21)). Thanks! - Added ActiveRecord 5.0 to build matrix. Dropped 3.2, 4.0, and 4.1 (which no longer get security updates: http://rubyonrails.org/security/) - Replaced ruby 1.9 and 2.0 (both EOL) with ruby 2.2 and 2.3 (see https://www.ruby-lang.org/en/downloads/) ### 3.0.0 - Added jruby/PostgreSQL support for Rails 4.x - Reworked threaded tests to allow jruby tests to pass #### API changes - `yield_with_lock_and_timeout` and `yield_with_lock` now return instances of `WithAdvisoryLock::Result`, so blocks that return `false` are not misinterpreted as a failure to lock. As this changes the interface (albeit internal methods), the major version number was incremented. - `with_advisory_lock_result` was introduced, which clarifies whether the lock was acquired versus the yielded block returned false. ### 2.0.0 - Lock timeouts of 0 now attempt the lock once, as per suggested by [Jon Leighton](https://github.com/jonleighton) and implemented by [Abdelkader Boudih](https://github.com/seuros). Thanks to both of you! - [Pull request 11](https://github.com/ClosureTree/with_advisory_lock/pull/11) fixed a downstream issue with jruby support! Thanks, [Aaron Todd](https://github.com/ozzyaaron)! - Added Travis tests for jruby - Dropped support for Rails 3.0, 3.1, and Ruby 1.8.7, as they are no longer receiving security patches. See http://rubyonrails.org/security/ for more information. This required the major version bump. - Refactored `advisory_lock_exists?` to use existing functionality - Fixed sqlite's implementation so parallel tests could be run against it ### 1.0.0 - Releasing 1.0.0. The interface will be stable. - Added `advisory_lock_exists?`. Thanks, [Sean Devine](https://github.com/barelyknown), for the great pull request! - Added Travis test for Rails 4.1 ### 0.0.10 - Explicitly added MIT licensing to the gemspec. ### 0.0.9 - Merged in Postgis Adapter Support to address [issue 7](https://github.com/ClosureTree/with_advisory_lock/issues/7) Thanks for the pull request, [Abdelkader Boudih](https://github.com/seuros)! - The database switching code had to be duplicated by [Closure Tree](https://github.com/ClosureTree/closure_tree), so I extracted a new `WithAdvisoryLock::DatabaseAdapterSupport` one-trick pony. - Builds were failing on Travis, so I introduced a global lock prefix that can be set with the `WITH_ADVISORY_LOCK_PREFIX` environment variable. I'm not going to advertise this feature yet. It's a secret. Only you and I know, now. _shhh_ ### 0.0.8 - Addressed [issue 5](https://github.com/ClosureTree/with_advisory_lock/issues/5) by using a deterministic hash for Postgresql + MRI >= 1.9. Thanks for the pull request, [Joel Turkel](https://github.com/jturkel)! - Addressed [issue 2](https://github.com/ClosureTree/with_advisory_lock/issues/2) by using a cache-busting query for MySQL and Postgres to deal with AR value caching bug. Thanks for the pull request, [Jaime Giraldo](https://github.com/sposmen)! - Addressed [issue 4](https://github.com/ClosureTree/with_advisory_lock/issues/4) by adding support for `em-postgresql-adapter`. Thanks, [lestercsp](https://github.com/lestercsp)! (Hey, github—your notifications are WAY too easy to ignore!) ### 0.0.7 - Added Travis tests for Rails 3.0, 3.1, 3.2, and 4.0 - Fixed MySQL bug with select_value returning a string instead of an integer when using AR 3.0.x ### 0.0.6 - Only require ActiveRecord >= 3.0.x - Fixed MySQL error reporting ### 0.0.5 - Asking for the currently acquired advisory lock doesn't re-ask for the lock now. - Introduced NestedAdvisoryLockError when asking for different, nested advisory locksMySQL ### 0.0.4 - Moved require into on_load, which should speed loading when AR doesn't have to spin up ### 0.0.3 - Fought with ActiveRecord 3.0.x and 3.1.x. You don't want them if you use threads—they fail predictably. ### 0.0.2 - Added warning log message for nested MySQL lock calls - Randomized lock wait time, which can help ameliorate lock contention ### 0.0.1 - First whack with_advisory_lock-5.3.0/Makefile0000644000004100000410000000071315022647710017117 0ustar www-datawww-data.PHONY: test-pg test-mysql test-pg: docker compose up -d pg sleep 10 # give some time for the service to start DATABASE_URL=postgres://with_advisory:with_advisory_pass@localhost/with_advisory_lock_test appraisal rake test test-mysql: docker compose up -d mysql sleep 10 # give some time for the service to start DATABASE_URL=mysql2://with_advisory:with_advisory_pass@0.0.0.0:3306/with_advisory_lock_test appraisal rake test test: test-pg test-mysql