pg_advisory_lock 0.2.0 → 0.4.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: 466169c01607df8b0c40594fa296e1fed78308d06a5512bab01e12cbafe59c2d
4
- data.tar.gz: 55fb38aafaaad466d4ef287dcb5ae9b2ad17bdd2e22cfc8ddeb6ca75e7d036e4
3
+ metadata.gz: 8a63a1943d6c02f12078939e1c72cad482972a9493ec408a0984308b329e269b
4
+ data.tar.gz: fcf0e101ff148fa73651759f0dbe4fe32cbaac1be1b5e9a13f26a7816c2d3a15
5
5
  SHA512:
6
- metadata.gz: cb2a78d382278811c09863faee99aca67c7fffdfb9f579b9ea96f76c3a895888b598c1bbd16c2ea5e57cbd409f6d36d60dc8b185b00c7861a83c3ac98dfe1d75
7
- data.tar.gz: 8fa4803b480af658d61c4c86441e7f0e0886f573a373ab182ed2c3b5bf41f195a9f1e4d81bdecadd1881655feb28eed39b1dd84f0bb8386dfcf2c15033f72fae
6
+ metadata.gz: 12b30ae854f94924bbced7bd831f4bbe89e682a50a0d01c299b7c3071c79e04e1d8cadadc5017574138d328104dc118f35f0fcee4e69889ea3ebb01bb0b8c8a8
7
+ data.tar.gz: 33ce47ca69b19de4985d8bf9d7517fecb5d9074bf3fe5ce53518b2b880ae6a7e1110f607a2160a6e8b8ee3928938603c75d53e957ee3f8e7dca1f5961b38682b
@@ -0,0 +1,69 @@
1
+ name: Tests
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches:
6
+ - master
7
+
8
+ jobs:
9
+ rubocop:
10
+ runs-on: ubuntu-latest
11
+ name: Rubocop lint
12
+ env:
13
+ RAILS_VERSION: '~> 6.0'
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: 2.7
19
+ bundler-cache: true
20
+ - name: Run rubocop
21
+ run: |
22
+ gem install bundler
23
+ bundle install
24
+ bundle exec rake rubocop
25
+ test:
26
+ runs-on: ubuntu-latest
27
+ strategy:
28
+ matrix:
29
+ ruby: [ '2.7', '3.0', '3.1' ]
30
+ rails: [ '~> 6.0', '~> 7.0' ]
31
+ name: Tests with Ruby ${{ matrix.ruby }} Activerecord ${{ matrix.rails }}
32
+ services:
33
+ # Label used to access the service container
34
+ postgres:
35
+ # Docker Hub image
36
+ image: postgres
37
+ # Provide the password for postgres
38
+ env:
39
+ POSTGRES_DB: postgres_db
40
+ POSTGRES_PORT: 5432
41
+ POSTGRES_USER: postgres_user
42
+ POSTGRES_PASSWORD: postgres_password
43
+ ports:
44
+ - 5432:5432
45
+ # Set health checks to wait until postgres has started
46
+ options: >-
47
+ --health-cmd pg_isready
48
+ --health-interval 10s
49
+ --health-timeout 5s
50
+ --health-retries 5
51
+ env:
52
+ RAILS_VERSION: ${{ matrix.rails }}
53
+ POSTGRES_HOST: localhost
54
+ POSTGRES_DB: postgres_db
55
+ POSTGRES_PORT: 5432
56
+ POSTGRES_USER: postgres_user
57
+ POSTGRES_PASSWORD: postgres_password
58
+ steps:
59
+ - uses: actions/checkout@v2
60
+ - uses: ruby/setup-ruby@v1
61
+ with:
62
+ ruby-version: ${{ matrix.ruby }}
63
+ bundler-cache: true
64
+ - name: Run tests
65
+ run: |
66
+ gem install bundler
67
+ bundle install
68
+ cp -v spec/config/database.ci.yml spec/config/database.yml
69
+ bundle exec rake spec
data/.rubocop.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  AllCops:
2
2
  DisplayCopNames: true
3
- TargetRubyVersion: 2.5
3
+ TargetRubyVersion: 2.7
4
4
  Exclude:
5
5
  - vendor/**/*
6
6
  - tmp/**/*
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # PgAdvisoryLock
2
2
 
3
+ ![tests](https://github.com/didww/pg_advisory_lock/actions/workflows/tests.yml/badge.svg)
4
+ [![Gem Version](https://badge.fury.io/rb/pg_advisory_lock.svg)](https://badge.fury.io/rb/pg_advisory_lock)
5
+
6
+
3
7
  Postgresql Advisory Lock for ActiveRecord.
4
8
  Allows to use mutex in applications that uses same database.
5
9
 
@@ -24,8 +28,6 @@ Or install it yourself as:
24
28
  create subclass from `PgAdvisoryLock::Base` and define `model_class` for it
25
29
 
26
30
  ```ruby
27
- require 'pg_sql_caller'
28
-
29
31
  class MySqlCaller < PgAdvisoryLock::Base
30
32
  model_class 'ApplicationRecord'
31
33
  end
@@ -44,7 +46,7 @@ PgAdvisoryLock::Base.select_values 'SELECT id from users WHERE parent_name = ?',
44
46
  ## Development
45
47
 
46
48
  After checking out the repo, run `bin/setup` to install dependencies.
47
- Create `spec/config/database.yml` (look at `spec/config/database.travis.yml` for example).
49
+ Create `spec/config/database.yml` (look at `spec/config/database.example.yml` for example).
48
50
  You need to create test database, so run `psql -c 'CREATE DATABASE pg_advisory_lock_test;'`.
49
51
  Then, run `rake spec` to run the tests.
50
52
  You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -23,6 +23,8 @@ module PgAdvisoryLock
23
23
  # @param name [Symbol, String]
24
24
  # @param value [Integer, Array<(Integer, Integer)>]
25
25
  def register_lock(name, value)
26
+ raise ArgumentError, 'value must be integer or array of integers' unless int_or_array_of_ints?(value)
27
+
26
28
  _lock_names[name.to_sym] = value
27
29
  end
28
30
 
@@ -31,6 +33,8 @@ module PgAdvisoryLock
31
33
  self._sql_caller_class = klass
32
34
  end
33
35
 
36
+ # Locks specific advisory lock.
37
+ # If it's already locked just waits.
34
38
  # @param name [Symbol, String] - lock name (will be transformed to number).
35
39
  # @param transaction [Boolean] - if true lock will be released at the end of current transaction
36
40
  # otherwise it will be released at the end of block.
@@ -41,7 +45,33 @@ module PgAdvisoryLock
41
45
  # @raise [ArgumentError] when lock name is invalid.
42
46
  # @return yield
43
47
  def with_lock(name, transaction: true, shared: false, id: nil, &block)
44
- new(name, transaction: transaction, shared: shared, id: id).lock(&block)
48
+ new(name, transaction: transaction, shared: shared, id: id, wait: true).lock(&block)
49
+ end
50
+
51
+ # Tries to lock specific advisory lock.
52
+ # If it's already locked raises exception.
53
+ # @param name [Symbol, String] - lock name (will be transformed to number).
54
+ # @param transaction [Boolean] - if true lock will be released at the end of current transaction
55
+ # otherwise it will be released at the end of block.
56
+ # @param shared [Boolean] - is lock shared or not.
57
+ # @param id [Integer] - number that will be used in pair with lock number to perform advisory lock.
58
+ # @yield - call block when lock is acquired
59
+ # block must be passed if transaction argument is false.
60
+ # @raise [ArgumentError] when lock name is invalid.
61
+ # @raise [PgAdvisoryLock::LockNotObtained] when wait: false passed and lock already locked.
62
+ # @return yield
63
+ def try_lock(name, transaction: true, shared: false, id: nil, &block)
64
+ new(name, transaction: transaction, shared: shared, id: id, wait: false).lock(&block)
65
+ end
66
+
67
+ private
68
+
69
+ def int_or_array_of_ints?(value)
70
+ return true if value.is_a?(Integer)
71
+
72
+ return true if value.is_a?(Array) && value.all? { |i| i.is_a?(Integer) }
73
+
74
+ false
45
75
  end
46
76
  end
47
77
 
@@ -50,27 +80,30 @@ module PgAdvisoryLock
50
80
  # otherwise it will be released at the end of block.
51
81
  # @param shared [Boolean] - is lock shared or not.
52
82
  # @param id [Integer] - number that will be used in pair with lock number to perform advisory lock.
53
- def initialize(name, transaction: false, shared: false, id: nil)
83
+ # @param wait [Boolean] - when locked by someone else: true - wait for lock, raise PgAdvisoryLock::LockNotObtained.
84
+ def initialize(name, transaction:, shared:, id:, wait:)
54
85
  @name = name.to_sym
55
86
  @transaction = transaction
56
87
  @shared = shared
57
88
  @id = id
89
+ @wait = wait
58
90
  end
59
91
 
60
92
  # @yield - call block when lock is acquired
61
93
  # block must be passed if transaction argument is false.
62
94
  # @raise [ArgumentError] when lock name is invalid.
95
+ # @raise [PgAdvisoryLock::LockNotObtained] when wait: false passed and lock already locked.
63
96
  # @return yield
64
97
  def lock(&block)
65
98
  with_logger do
66
- lock_number = name_to_number
67
- advisory_lock(lock_number, &block)
99
+ lock_args = build_lock_args
100
+ advisory_lock(lock_args, &block)
68
101
  end
69
102
  end
70
103
 
71
104
  private
72
105
 
73
- attr_reader :transaction, :shared, :name, :id
106
+ attr_reader :transaction, :shared, :name, :id, :wait
74
107
 
75
108
  def with_logger
76
109
  return yield if logger.nil? || !logger.respond_to?(:tagged)
@@ -78,57 +111,68 @@ module PgAdvisoryLock
78
111
  logger.tagged(self.class.to_s, name.inspect) { yield }
79
112
  end
80
113
 
81
- def advisory_lock(lock_number, &block)
114
+ def advisory_lock(lock_args, &block)
82
115
  if transaction
83
- transaction_lock(lock_number, &block)
116
+ transaction_lock(lock_args, &block)
84
117
  else
85
- non_transaction_lock(lock_number, &block)
118
+ non_transaction_lock(lock_args, &block)
86
119
  end
87
120
  end
88
121
 
89
- def transaction_lock(lock_number)
122
+ def transaction_lock(lock_args)
90
123
  raise ArgumentError, 'block required when not within transaction' if !block_given? && !sql_caller_class.transaction_open?
91
124
 
92
- return perform_lock(lock_number) unless block_given?
125
+ return perform_lock(lock_args) unless block_given?
93
126
 
94
127
  sql_caller_class.transaction do
95
- perform_lock(lock_number)
128
+ perform_lock(lock_args)
96
129
  yield
97
130
  end
98
131
  end
99
132
 
100
- def non_transaction_lock(lock_number)
133
+ def non_transaction_lock(lock_args)
101
134
  raise ArgumentError, 'block required on transaction: false' unless block_given?
102
135
 
103
136
  begin
104
- perform_lock(lock_number)
137
+ perform_lock(lock_args)
105
138
  yield
106
139
  ensure
107
- perform_unlock(lock_number)
140
+ perform_unlock(lock_args)
108
141
  end
109
142
  end
110
143
 
111
- def perform_lock(lock_number)
112
- function_name = "pg_advisory#{'_xact' if transaction}_lock#{'_shared' if shared}"
144
+ def perform_lock(lock_args)
145
+ function_name = "pg#{'_try' unless wait}_advisory#{'_xact' if transaction}_lock#{'_shared' if shared}"
113
146
 
114
- sql_caller_class.execute("SELECT #{function_name}(#{lock_number})")
147
+ if wait
148
+ sql_caller_class.execute("SELECT #{function_name}(#{lock_args})")
149
+ else
150
+ result = sql_caller_class.select_value("SELECT #{function_name}(#{lock_args})")
151
+ raise LockNotObtained, "#{self.class} can't obtain lock (#{name}, #{id.inspect})" unless result
152
+ end
115
153
  end
116
154
 
117
- def perform_unlock(lock_number)
118
- sql_caller_class.select_value("SELECT pg_advisory_unlock#{'_shared' if shared}(#{lock_number})")
155
+ def perform_unlock(lock_args)
156
+ sql_caller_class.select_value("SELECT pg_advisory_unlock#{'_shared' if shared}(#{lock_args})")
119
157
  end
120
158
 
121
159
  # Converts lock name to number, because pg advisory lock functions accept only bigint numbers.
122
160
  # @return [String] lock number or two numbers delimited by comma.
123
- def name_to_number
124
- lock_number = _lock_names.fetch(name) do
161
+ def build_lock_args
162
+ lock_args = _lock_names.fetch(name) do
125
163
  raise ArgumentError, "lock name #{name.inspect} is invalid, see #{self.class}::NAMES"
126
164
  end
127
- lock_number = Array.wrap(lock_number)
128
- lock_number.push(id) if id.present?
129
- raise ArgumentError, "can't use lock name #{name.inspect} with id" if lock_number.size > 2
165
+ lock_args = Array.wrap(lock_args)
166
+ if id.present?
167
+ if id.is_a?(Integer)
168
+ lock_args.push(id)
169
+ else
170
+ lock_args.push("hashtext(#{sql_caller_class.connection.quote(id.to_s)})")
171
+ end
172
+ end
173
+ raise ArgumentError, "can't use lock name #{name.inspect} with id" if lock_args.size > 2
130
174
 
131
- lock_number.join(', ')
175
+ lock_args.join(', ')
132
176
  end
133
177
 
134
178
  def sql_caller_class
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgAdvisoryLock
4
+ class LockNotObtained < StandardError
5
+ end
6
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgAdvisoryLock
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pg_advisory_lock/version'
4
+ require 'pg_advisory_lock/lock_not_obtained'
4
5
  require 'pg_advisory_lock/base'
5
6
 
6
7
  module PgAdvisoryLock
@@ -29,5 +29,5 @@ Gem::Specification.new do |spec|
29
29
 
30
30
  spec.add_dependency 'activerecord'
31
31
  spec.add_dependency 'activesupport'
32
- spec.add_dependency 'pg_sql_caller'
32
+ spec.add_dependency 'pg_sql_caller', '>= 0.2.2'
33
33
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_advisory_lock
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Denis Talakevich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-25 00:00:00.000000000 Z
11
+ date: 2023-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: 0.2.2
48
48
  type: :runtime
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: 0.2.2
55
55
  description: Postgresql Advisory Lock for ActiveRecord.
56
56
  email:
57
57
  - senid231@gmail.com
@@ -59,10 +59,10 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
+ - ".github/workflows/tests.yml"
62
63
  - ".gitignore"
63
64
  - ".rspec"
64
65
  - ".rubocop.yml"
65
- - ".travis.yml"
66
66
  - CODE_OF_CONDUCT.md
67
67
  - Gemfile
68
68
  - LICENSE.txt
@@ -72,6 +72,7 @@ files:
72
72
  - bin/setup
73
73
  - lib/pg_advisory_lock.rb
74
74
  - lib/pg_advisory_lock/base.rb
75
+ - lib/pg_advisory_lock/lock_not_obtained.rb
75
76
  - lib/pg_advisory_lock/version.rb
76
77
  - pg_advisory_lock.gemspec
77
78
  homepage: https://github.com/didww/pg_advisory_lock
@@ -96,7 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
97
  - !ruby/object:Gem::Version
97
98
  version: '0'
98
99
  requirements: []
99
- rubygems_version: 3.0.4
100
+ rubygems_version: 3.1.6
100
101
  signing_key:
101
102
  specification_version: 4
102
103
  summary: Postgresql Advisory Lock for ActiveRecord
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.5.5
6
- before_install: gem install bundler -v 2.1.4