pg_lock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c24490141a8dd6d9d3903c84120a9635c561a029
4
+ data.tar.gz: 6e5408ba72c4eed608a141dc34e3778402d7cda9
5
+ SHA512:
6
+ metadata.gz: 78c5bcf7287fed34d351ab5bb053d28bd0d9da26275ad09f52ef011f36cf188a09ba6239eec991e52d51d3b2c5e63d56232cc9b505486f2049392d9cbb0ad6bd
7
+ data.tar.gz: d88848d3287d74314e4a8240d7404a3d7e3515da93da5e526f7fc7716070cf3e1188addd4969e5272e6197b60cf04a87d45c23738b9b6c6f5ed2df074f52263b
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1
4
+ - 2.2
5
+ - ruby-head
6
+
7
+ script:
8
+ - bundle exec rspec spec
9
+
10
+ before_script:
11
+ - psql -c 'create database pg_lock_test;' -U postgres
12
+
13
+ addons:
14
+ postgresql: "9.3"
15
+
16
+ matrix:
17
+ allow_failures:
18
+ - rvm: ruby-head
@@ -0,0 +1,5 @@
1
+ # A Log of Changes!
2
+
3
+ ## [0.1.0] - 2015-10-06
4
+
5
+ - First version.
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pg_lock.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 schneems
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,197 @@
1
+ # PgLock
2
+
3
+ Uses [Postgres advisory locks](http://www.postgresql.org/docs/9.2/static/view-pg-locks.html) to enable you to syncronize actions across processes and machines.
4
+
5
+ [![Build Status](https://travis-ci.org/heroku/pg_lock.svg?branch=master)](https://travis-ci.org/heroku/pg_lock)
6
+
7
+ ## Installation
8
+
9
+ This gem requires Ruby 2.1+
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'pg_lock'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install pg_lock
24
+
25
+ ## Usage
26
+
27
+ Create a `PgLock.new` instance and call the `lock` method to ensure exclusive execution of a block of code.
28
+
29
+ ```ruby
30
+ PgLock.new(name: "all_your_base").lock do
31
+ # stuff
32
+ end
33
+ ```
34
+
35
+ Now no matter how many times this code is executed across any number of machines, one block of code will be allowed to execute at a time.
36
+
37
+ ## Session based locking
38
+
39
+ The postgres lock is unique across different database sessions, if the same session tries to aquire the same lock it will succeed. So while `PgLock` will guarantee unique execution across machines and processes, it will not block the same process (sharing the same connection session) from running. For example while you would think the middle block would not run in this example:
40
+
41
+ ```ruby
42
+ key = "all_your_base"
43
+ PgLock.new(name: key).lock do
44
+ puts "First block called"
45
+ PgLock.new(name: key).lock do
46
+ puts "Second block called because it's sharing the same session"
47
+ end
48
+ end
49
+ ```
50
+
51
+ The result will be:
52
+
53
+ ```
54
+ First block called
55
+ Second block called because it's sharing the same session
56
+ ```
57
+
58
+ If you need to syncronize code execution inside of the same process you should [use a mutex](http://ruby-doc.org/core-2.2.2/Mutex.html).
59
+
60
+ ## Timeout
61
+
62
+ By default, locked blocks will timeout after 60 seconds of execution, the lock will be released and any code executing will be terminated by a `Timeout::Error` will be raised. You can lower or raise this value by passing in a `ttl` (time to live) argument:
63
+
64
+ ```ruby
65
+ begin
66
+ PgLock.new(name: "all_your_base", ttl: 30).lock do
67
+ # stuff
68
+ end
69
+ rescue Timeout::Error
70
+ puts "Took longer than 30 seconds to execute"
71
+ end
72
+ ```
73
+
74
+ To disable the timeout pass in a falsey value:
75
+
76
+ ```ruby
77
+ PgLock.new(name: "all_your_base", ttl: false).lock do
78
+ # stuff
79
+ end
80
+ ```
81
+
82
+ ## Retry Attempts
83
+
84
+ By default if a lock cannot be aquired, `PgLock` will try 3 times with a 1 second delay between tries. You can configure this behavior using `attempts` and `attempt_interval` arguments:
85
+
86
+ ```ruby
87
+ PgLock.new(name: "all_your_base", attempts: 10, attempt_interval: 5).lock do
88
+ # stuff
89
+ end
90
+ ```
91
+
92
+ To run once use `attempts: 1`.
93
+
94
+ ## Raise Error on Failed Lock
95
+
96
+ You can optionally raise an error if a block cannot be executed in the given number of attempts by using the `lock!` method:
97
+
98
+ ```ruby
99
+ begin
100
+ PgLock.new(name: "all_your_base").lock! do
101
+ # stuff
102
+ end
103
+ rescue PgLock::UnableToLockError
104
+ # do stuff
105
+ end
106
+ ```
107
+
108
+ ## Manual Lock
109
+
110
+ The `create` method will return the `PgLock` instance if a lock object was created, or `false` if no lock was aquired. You should manually `delete` a successfully created lock object:
111
+
112
+ ```ruby
113
+ begin
114
+ lock = PgLock.new(name: "all_your_base")
115
+ lock.create
116
+ # do stuff
117
+ ensure
118
+ lock.delete
119
+ end
120
+ ```
121
+
122
+ You can check on the status of a lock with the `aquired?` method:
123
+
124
+ ```ruby
125
+ begin
126
+ lock = PgLock.new(name: "all_your_base")
127
+ lock.create
128
+ if lock.aquired?
129
+ # do stuff
130
+ end
131
+ ensure
132
+ lock.delete
133
+ end
134
+ ```
135
+
136
+ ## Logging
137
+
138
+ By default there is no logging, if you want you can provide a logging block:
139
+
140
+ ```ruby
141
+ PgLock.new(name: "all_your_base", log: ->(data) { puts data.inspect }).lock do
142
+ # stuff
143
+ end
144
+ ```
145
+
146
+ One argument will be passed to the block, a hash. You can optionally define a default log for all instances:
147
+
148
+ ```ruby
149
+ PgLock::DEFAULT_LOG = ->(data) { puts data.inspect }
150
+ ```
151
+
152
+ Note: When you enable logging exceptions raised when deleting a lock will be swallowed. To re-raise you can use the exception in `data[:exception]`.
153
+
154
+ ## Database Connection
155
+
156
+ This library defaults to use Active Record. If you want to use another library, or spin up a dedicated connection you can use the `connection` argument:
157
+
158
+ ```ruby
159
+ my_connection = MyCustomConnectionObject.new
160
+ PgLock.new(name: "all_your_base", connection: my_connection).lock do
161
+ # stuff
162
+ end
163
+ ```
164
+
165
+ The object needs to respond to the `exec` method where the first argument is a query string, and the second is an array of bind arguments. For example to use with [sequel](https://github.com/jeremyevans/sequel) you could do something like this:
166
+
167
+ ```ruby
168
+ connection = Module do
169
+ def self.exec(sql, bind)
170
+ DB.fetch(sql, bind)
171
+ end
172
+ end
173
+
174
+ PgLock.new(name: "all_your_base", connection: my_connection).lock do
175
+ # stuff
176
+ end
177
+ ```
178
+
179
+ Where `DB` is to be your database connection.
180
+
181
+ ## Development
182
+
183
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
184
+
185
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
186
+
187
+ ## Contributing
188
+
189
+ 1. Fork it ( https://github.com/[my-github-username]/pg_lock/fork )
190
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
191
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
192
+ 4. Push to the branch (`git push origin my-new-feature`)
193
+ 5. Create a new Pull Request
194
+
195
+ ## Acknowledgements
196
+
197
+ Originally written by [@mikehale](https://github.com/mikehale)
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+ task :test => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pg_lock"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,106 @@
1
+ require 'zlib'
2
+ require 'timeout'
3
+
4
+ require "pg_lock/version"
5
+
6
+ class PgLock
7
+ PG_LOCK_SPACE = -2147483648
8
+ DEFAULT_CONNECTION = Proc.new do
9
+ defined?(ActiveRecord::Base) ? ActiveRecord::Base.connection.raw_connection : false
10
+ end
11
+
12
+ DEFAULT_LOGGER = Proc.new do
13
+ defined?(DEFAULT_LOG) ? DEFAULT_LOG : false
14
+ end
15
+
16
+ class UnableToLockError < RuntimeError
17
+ def initialize(name:, attempts: )
18
+ msg = "Was unable to aquire a lock #{ name.inspect } after #{ attempts } attempts"
19
+ super msg
20
+ end
21
+ end
22
+ UnableToLock = UnableToLockError
23
+
24
+ def initialize(name:, attempts: 3, attempt_interval: 1, ttl: 60, connection: DEFAULT_CONNECTION.call, log: DEFAULT_LOGGER.call )
25
+ self.name = name
26
+ self.max_attempts = [attempts, 1].max
27
+ self.attempt_interval = attempt_interval
28
+ self.ttl = ttl || 0 # set this to 0 to disable the timeout
29
+ self.log = log
30
+
31
+ connection or raise "Must provide a valid connection object"
32
+ self.locket = Locket.new(connection, [PG_LOCK_SPACE, key(name)])
33
+ end
34
+
35
+ # Runs the given block if an advisory lock is able to be acquired.
36
+ def lock(&block)
37
+ if create
38
+ begin
39
+ Timeout::timeout(ttl, &block) if block_given?
40
+ ensure
41
+ delete
42
+ end
43
+ return true
44
+ else
45
+ return false
46
+ end
47
+ end
48
+
49
+ # A PgLock::UnableToLock is raised if the lock is not acquired.
50
+ def lock!(exception_klass = PgLock::UnableToLockError)
51
+ if lock { yield self if block_given? }
52
+ # lock successful, do nothing
53
+ else
54
+ raise exception_klass.new(name: name, attempts: max_attempts)
55
+ end
56
+ end
57
+
58
+ def create
59
+ max_attempts.times.each do |attempt|
60
+ if locket.lock
61
+ log.call(at: :create, attempt: attempt, args: locket.args, pg_lock: true) if log
62
+ return self
63
+ else
64
+ return false if attempt.next == max_attempts
65
+ sleep attempt_interval
66
+ end
67
+ end
68
+ end
69
+
70
+ def delete
71
+ locket.unlock
72
+ log.call(at: :delete, args: locket.args, pg_lock: true ) if log
73
+ rescue => e
74
+ if log
75
+ log.call(at: :exception, exception: e, pg_lock: true )
76
+ else
77
+ raise e
78
+ end
79
+ end
80
+
81
+ def aquired?
82
+ locket.active?
83
+ end
84
+ alias :has_lock? :aquired?
85
+
86
+ private
87
+ attr_accessor :max_attempts
88
+ attr_accessor :attempt_interval
89
+ attr_accessor :connection
90
+ attr_accessor :locket
91
+ attr_accessor :ttl
92
+ attr_accessor :name
93
+ attr_accessor :log
94
+
95
+ def key(name)
96
+ i = Zlib.crc32(name.to_s)
97
+ # We need to wrap the value for postgres
98
+ if i > 2147483647
99
+ -(-(i) & 0xffffffff)
100
+ else
101
+ i
102
+ end
103
+ end
104
+ end
105
+
106
+ require 'pg_lock/locket'
@@ -0,0 +1,38 @@
1
+ class PgLock
2
+ # Holds the logic to aquire a lock and parse if locking was successful
3
+ class Locket
4
+ attr_accessor :args, :connection
5
+ def initialize(connection, lock_args)
6
+ self.connection = connection
7
+ self.args = lock_args
8
+ end
9
+
10
+ def lock
11
+ @lock = connection.exec("select pg_try_advisory_lock($1,$2)", args)
12
+ return aquired?
13
+ end
14
+
15
+ def unlock
16
+ connection.exec("select pg_advisory_unlock($1,$2)", args)
17
+ @lock = false
18
+ end
19
+
20
+ def aquired?
21
+ @lock[0]["pg_try_advisory_lock"] == "t"
22
+ rescue
23
+ false
24
+ end
25
+
26
+ def active?
27
+ connection.exec(<<-eos, args).getvalue(0,0) == "t"
28
+ SELECT granted
29
+ FROM pg_locks
30
+ WHERE locktype = 'advisory' AND
31
+ pid = pg_backend_pid() AND
32
+ mode = 'ExclusiveLock' AND
33
+ classid = $1 AND
34
+ objid = $2
35
+ eos
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ class PgLock
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pg_lock/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pg_lock"
8
+ spec.version = PgLock::VERSION
9
+ spec.authors = ["mikehale", "schneems"]
10
+ spec.email = ["mike@hales.ws", "richard.schneeman@gmail.com"]
11
+
12
+ spec.summary = %q{ Use Postgres advisory lock to isolate code execution across machines }
13
+ spec.description = %q{ A Postgres advisory lock client }
14
+ spec.homepage = "http://github.com/heroku/pg_lock"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "pg", ">= 0.15"
23
+ spec.add_development_dependency "activerecord", ">= 2.3"
24
+ spec.add_development_dependency "bundler", "~> 1"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+
27
+ spec.add_development_dependency "rspec", "~> 3.1"
28
+ end
29
+
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_lock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - mikehale
8
+ - schneems
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2015-10-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pg
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0.15'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0.15'
28
+ - !ruby/object:Gem::Dependency
29
+ name: activerecord
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '2.3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '2.3'
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '10.0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '10.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rspec
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '3.1'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '3.1'
84
+ description: " A Postgres advisory lock client "
85
+ email:
86
+ - mike@hales.ws
87
+ - richard.schneeman@gmail.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".travis.yml"
95
+ - CHANGELOG.md
96
+ - CODE_OF_CONDUCT.md
97
+ - Gemfile
98
+ - LICENSE.txt
99
+ - README.md
100
+ - Rakefile
101
+ - bin/console
102
+ - bin/setup
103
+ - lib/pg_lock.rb
104
+ - lib/pg_lock/locket.rb
105
+ - lib/pg_lock/version.rb
106
+ - pg_lock.gemspec
107
+ homepage: http://github.com/heroku/pg_lock
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.4.5.1
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Use Postgres advisory lock to isolate code execution across machines
131
+ test_files: []