pg_advisory_lock 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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