travis-lock 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4c983c384c38f7bb07cddd92b3ea775570ff2aec
4
+ data.tar.gz: 21b952b4ce5c7eeed44d21d4db04e6b4e920f428
5
+ SHA512:
6
+ metadata.gz: 490a9cb44c09b52426876d131d30c118e3e279a82dd75d7c9cc30d0d6553d5278ba4e9b8d686d34683a18e370727e09a6e2e1ea124365f359a4aaf23a2c2f3c0
7
+ data.tar.gz: afccd06931aaa156e0e6fbafa590d3929e0a0fc903f3386554499d15277e862f6ab6d1ee09822ae9cf0042e91c134e1a07959def5a58f3522b8439aaa48d590d
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'redlock'
6
+
7
+ platform :ruby do
8
+ gem 'pg'
9
+ end
10
+
11
+ group :test do
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'mocha', '~> 1.1'
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,61 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ travis-lock (0.1.4)
5
+ activerecord (~> 4.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (4.2.4)
11
+ activesupport (= 4.2.4)
12
+ builder (~> 3.1)
13
+ activerecord (4.2.4)
14
+ activemodel (= 4.2.4)
15
+ activesupport (= 4.2.4)
16
+ arel (~> 6.0)
17
+ activesupport (4.2.4)
18
+ i18n (~> 0.7)
19
+ json (~> 1.7, >= 1.7.7)
20
+ minitest (~> 5.1)
21
+ thread_safe (~> 0.3, >= 0.3.4)
22
+ tzinfo (~> 1.1)
23
+ arel (6.0.3)
24
+ builder (3.2.2)
25
+ diff-lcs (1.2.5)
26
+ i18n (0.7.0)
27
+ json (1.8.3)
28
+ metaclass (0.0.4)
29
+ minitest (5.8.0)
30
+ mocha (1.1.0)
31
+ metaclass (~> 0.0.1)
32
+ pg (0.18.3)
33
+ redis (3.2.1)
34
+ redlock (0.1.1)
35
+ redis (~> 3, >= 3.0.5)
36
+ rspec (3.3.0)
37
+ rspec-core (~> 3.3.0)
38
+ rspec-expectations (~> 3.3.0)
39
+ rspec-mocks (~> 3.3.0)
40
+ rspec-core (3.3.2)
41
+ rspec-support (~> 3.3.0)
42
+ rspec-expectations (3.3.1)
43
+ diff-lcs (>= 1.2.0, < 2.0)
44
+ rspec-support (~> 3.3.0)
45
+ rspec-mocks (3.3.2)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.3.0)
48
+ rspec-support (3.3.0)
49
+ thread_safe (0.3.5)
50
+ tzinfo (1.2.2)
51
+ thread_safe (~> 0.1)
52
+
53
+ PLATFORMS
54
+ ruby
55
+
56
+ DEPENDENCIES
57
+ mocha (~> 1.1)
58
+ pg
59
+ redlock
60
+ rspec (~> 3.0)
61
+ travis-lock!
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Travis Lock
2
+
3
+ Application-level locks for use in, e.g. travis-hub.
4
+
5
+ At the moment it seems the Redlock strategy works fine as well as the
6
+ Postgresql advisory locks strategy when used with the options in the
7
+ example below.
8
+
9
+ Usage:
10
+
11
+ ```
12
+ options = {
13
+ strategy: :postgresql,
14
+ try: true,
15
+ transactional: false
16
+ }
17
+ Travis::Lock.exclusive('build-1', options) do
18
+ # update build
19
+ end
20
+ ```
21
+
22
+ ### Doing a Rubygem release
23
+
24
+ Any tool works. The current releases were done with
25
+ [`gem-release`](https://github.com/svenfuchs/gem-release) which allows creating
26
+ a Git tag, pushing it to GitHub, building the gem and pushing it to Rubygems in
27
+ one go:
28
+
29
+ ```bash
30
+ $ gem install gem-release
31
+ $ gem bump --push --tag --release
32
+ ```
@@ -0,0 +1,9 @@
1
+ module Travis
2
+ module Lock
3
+ class None < Struct.new(:name, :options)
4
+ def exclusive
5
+ yield
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,76 @@
1
+ # http://hashrocket.com/blog/posts/advisory-locks-in-postgres
2
+ # https://github.com/mceachen/with_advisory_lock
3
+ # 13.3.4. Advisory Locks : http://www.postgresql.org/docs/9.3/static/explicit-locking.html
4
+ # http://www.postgresql.org/docs/9.3/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
5
+
6
+ require 'zlib'
7
+ require 'active_record'
8
+ require 'travis/lock/support/retry'
9
+
10
+ module Travis
11
+ module Lock
12
+ class Postgresql < Struct.new(:name, :options)
13
+ def initialize(*)
14
+ super
15
+ fail 'lock name cannot be blank' if name.nil? || name.empty?
16
+ end
17
+
18
+ def exclusive(&block)
19
+ with_timeout { obtain_lock }
20
+ transactional? ? connection.transaction(&block) : with_release(&block)
21
+ end
22
+
23
+ private
24
+
25
+ def with_timeout(&block)
26
+ try? ? Retry.new(name, options).run(&block) : with_statement_timeout(&block)
27
+ end
28
+
29
+ def obtain_lock
30
+ result = connection.select_value("select #{pg_function}(#{key});")
31
+ try? ? result == 't' : true
32
+ end
33
+
34
+ def with_release
35
+ yield
36
+ ensure
37
+ connection.execute("select pg_advisory_unlock(#{key});")
38
+ end
39
+
40
+ def try?
41
+ !!options[:try]
42
+ end
43
+
44
+ def timeout
45
+ options[:timeout] || 30
46
+ end
47
+
48
+ def transactional?
49
+ !!options[:transactional]
50
+ end
51
+
52
+ def with_statement_timeout
53
+ connection.execute("set statement_timeout to #{Integer(timeout * 1000)};")
54
+ yield
55
+ rescue ActiveRecord::StatementInvalid => e
56
+ retry if defined?(PG) && e.original_exception.is_a?(PG::QueryCanceled)
57
+ timeout!
58
+ end
59
+
60
+ def pg_function
61
+ func = ['pg', 'advisory', 'lock']
62
+ func.insert(2, 'xact') if transactional?
63
+ func.insert(1, 'try') if try?
64
+ func.join('_')
65
+ end
66
+
67
+ def connection
68
+ ActiveRecord::Base.connection
69
+ end
70
+
71
+ def key
72
+ Zlib.crc32(name).to_i & 0x7fffffff
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,60 @@
1
+ require 'monitor'
2
+ begin
3
+ require 'redlock'
4
+ rescue LoadError
5
+ end
6
+
7
+ module Travis
8
+ module Lock
9
+ class Redis
10
+ class LockError < StandardError
11
+ def initialize(key)
12
+ super("Could not obtain lock for #{key.inspect} on Redis.")
13
+ end
14
+ end
15
+
16
+ extend MonitorMixin
17
+
18
+ DEFAULTS = {
19
+ ttl: 5 * 60,
20
+ retries: 5,
21
+ interval: 0.1
22
+ }
23
+
24
+ attr_reader :name, :config, :retried
25
+
26
+ def initialize(name, config)
27
+ @name = name
28
+ @config = DEFAULTS.merge(config)
29
+ @retried = 0
30
+ end
31
+
32
+ def exclusive
33
+ retrying do
34
+ client.lock(name, config[:ttl]) do |lock|
35
+ lock ? yield : raise(LockError.new(name))
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def client
43
+ Redlock::Client.new([url])
44
+ end
45
+
46
+ def url
47
+ config[:url] || fail("No Redis URL specified")
48
+ end
49
+
50
+ def retrying
51
+ yield
52
+ rescue LockError
53
+ raise if retried.to_i >= config[:retries]
54
+ sleep config[:interval]
55
+ @retries = retried + 1
56
+ retry
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ module Travis
2
+ module Lock
3
+ class Retry < Struct.new(:name, :options)
4
+ WAIT = 0.0001..0.0009
5
+
6
+ def run
7
+ wait until result = yield
8
+ result
9
+ end
10
+
11
+ def wait
12
+ sleep(rand(options[:wait] || WAIT))
13
+ timeout! if timeout?
14
+ end
15
+
16
+ def started
17
+ @started ||= Time.now
18
+ end
19
+
20
+ def timeout?
21
+ started + timeout < Time.now
22
+ end
23
+
24
+ def timeout
25
+ options[:timeout] || 30
26
+ end
27
+
28
+ def timeout!
29
+ fail Timeout.new(name, options)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Travis
2
+ module Lock
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,28 @@
1
+ require 'travis/lock/none'
2
+ require 'travis/lock/postgresql'
3
+ require 'travis/lock/redis'
4
+
5
+ module Travis
6
+ module Lock
7
+ class Timeout < StandardError
8
+ def initialize(name, options)
9
+ super("Could not obtain lock for #{name}: #{options.map { |*pair| pair.join('=') }.join(' ')}")
10
+ end
11
+ end
12
+
13
+ extend self
14
+
15
+ attr_reader :default_strategy
16
+
17
+ def exclusive(name, options = {}, &block)
18
+ options[:strategy] ||= Lock.default_strategy || :none
19
+ const_get(camelize(options[:strategy])).new(name, options).exclusive(&block)
20
+ end
21
+
22
+ private
23
+
24
+ def camelize(object)
25
+ object.to_s.split('_').collect(&:capitalize).join
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_record'
2
+ require 'travis/lock'
3
+
4
+ ActiveRecord::Base.establish_connection(
5
+ adapter: 'postgresql',
6
+ database: 'travis_development',
7
+ pool: 30
8
+ )
9
+
10
+ class Lock
11
+ class Redis
12
+ def exclusive(&block)
13
+ options = {
14
+ strategy: :redis,
15
+ url: 'redis://localhost:6379'
16
+ }
17
+ Travis::Lock.exclusive('test', options, &block)
18
+ end
19
+ end
20
+
21
+ class Postgresql
22
+ def exclusive(&block)
23
+ options = {
24
+ strategy: :postgresql,
25
+ # try: true,
26
+ try: false,
27
+ transactional: false
28
+ }
29
+ Travis::Lock.exclusive('test', options, &block)
30
+ end
31
+ end
32
+ end
33
+
34
+ number_of_runs = Integer(ARGV[0] || 1)
35
+ concurrency = Integer(ARGV[1] || 20)
36
+ lock_types = ARGV[2] ? [ARGV[2].to_sym] : Lock.constants
37
+
38
+ 1.upto(number_of_runs) do |ix|
39
+ lock_types.each do |strategy|
40
+ puts "#{ix} Using strategy #{strategy}"
41
+ lock = Lock.const_get(strategy).new
42
+ count = 0
43
+
44
+ threads = (1..concurrency).to_a.map do
45
+ Thread.new do
46
+ lock.exclusive do
47
+ count = count.tap { sleep(rand(0.001..0.009)) } + 1
48
+ end
49
+ end
50
+ end
51
+ threads.map(&:join)
52
+
53
+ puts " #{count == concurrency ? "\033[32;1m" : "\033[31;1m" }Expected count to be #{concurrency}. Actually is #{count}.\033[0m\n\n"
54
+ end
55
+ end
@@ -0,0 +1,9 @@
1
+ ENV['ENV'] = 'test'
2
+
3
+ require 'mocha'
4
+ require 'travis/lock'
5
+ require 'support/database'
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :mocha
9
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection(
4
+ adapter: 'postgresql',
5
+ database: 'travis_test',
6
+ pool: 50
7
+ )
@@ -0,0 +1,8 @@
1
+ describe Travis::Lock::None do
2
+ let(:lock) { described_class.new }
3
+
4
+ it 'yields' do
5
+ lock.exclusive { @called = true }
6
+ expect(@called).to eq(true)
7
+ end
8
+ end
@@ -0,0 +1,138 @@
1
+ describe Travis::Lock::Postgresql do
2
+ let(:lock) { described_class.new(name, config) }
3
+ let(:name) { 'name' }
4
+ let(:key) { 1579384326 }
5
+
6
+ def rescueing
7
+ yield
8
+ rescue Travis::Lock::Timeout
9
+ end
10
+
11
+ shared_examples_for 'yields' do
12
+ it 'yields' do
13
+ lock.exclusive { @called = true }
14
+ expect(@called).to eq(true)
15
+ end
16
+ end
17
+
18
+ context do
19
+ let(:conn) { stub('connection', select_value: 't', execute: nil) }
20
+ before { def conn.transaction; yield end }
21
+ before { ActiveRecord::Base.stubs(:connection).returns(conn) }
22
+
23
+ shared_examples_for 'locks_with' do |method|
24
+ it "locks_with #{method}" do
25
+ conn.expects(:select_value).with("select #{method}(#{key});").returns('t')
26
+ lock.exclusive { }
27
+ end
28
+ end
29
+
30
+ shared_examples_for 'retries until timeout' do
31
+ it 'retries until timeout' do
32
+ conn.expects(:select_value).returns('f').at_least(50)
33
+ rescueing { lock.exclusive { } }
34
+ end
35
+ end
36
+
37
+ shared_examples_for 'sets a statement level timeout' do
38
+ it 'sets a statement level timeout' do
39
+ conn.expects(:execute).with('set statement_timeout to 100;')
40
+ lock.exclusive { }
41
+ end
42
+ end
43
+
44
+ shared_examples_for 'raises Travis::Lock::Timeout when timed out' do
45
+ it 'raises Travis::Lock::Timeout when timed out' do
46
+ conn.stubs(:select_value).returns('f')
47
+ expect { lock.exclusive { } }.to raise_error(Travis::Lock::Timeout)
48
+ end
49
+ end
50
+
51
+ describe 'using try_*' do
52
+ describe 'not using transactions' do
53
+ let(:config) { { try: true, transactional: false, timeout: 0.1 } }
54
+
55
+ include_examples 'yields'
56
+ include_examples 'locks_with', 'pg_try_advisory_lock'
57
+ include_examples 'retries until timeout'
58
+ include_examples 'raises Travis::Lock::Timeout when timed out'
59
+ end
60
+
61
+ describe 'using transactions' do
62
+ let(:config) { { try: true, transactional: true, timeout: 0.1 } }
63
+
64
+ include_examples 'yields'
65
+ include_examples 'locks_with', 'pg_try_advisory_xact_lock'
66
+ include_examples 'retries until timeout'
67
+ include_examples 'raises Travis::Lock::Timeout when timed out'
68
+ end
69
+ end
70
+
71
+ describe 'not using try_*' do
72
+ describe 'not using transactions' do
73
+ let(:config) { { try: false, transactional: false, timeout: 0.1 } }
74
+
75
+ include_examples 'yields'
76
+ include_examples 'locks_with', 'pg_advisory_lock'
77
+ include_examples 'sets a statement level timeout'
78
+ # include_examples 'raises Travis::Lock::Timeout when timed out'
79
+ end
80
+
81
+ describe 'using transactions' do
82
+ let(:config) { { try: false, transactional: true, timeout: 0.1 } }
83
+
84
+ include_examples 'yields'
85
+ include_examples 'locks_with', 'pg_advisory_xact_lock'
86
+ include_examples 'sets a statement level timeout'
87
+ # include_examples 'raises Travis::Lock::Timeout when timed out'
88
+ end
89
+ end
90
+ end
91
+
92
+ # describe 'integration' do
93
+ # shared_examples_for 'no race condition' do
94
+ # runs = ENV['RUNS'] || 1
95
+ # threads = ENV['THREADS'] || 10
96
+
97
+ # 1.upto(runs) do |ix|
98
+ # it "does not see a race condition on #{threads} threads (run #{ix})" do
99
+ # counter = 0
100
+
101
+ # Array(1..threads).map do
102
+ # Thread.new do
103
+ # lock.exclusive do
104
+ # counter = counter.tap { sleep(rand(0.001)) } + 1
105
+ # end
106
+ # end
107
+ # end.map(&:join)
108
+
109
+ # expect(counter).to eq(threads)
110
+ # end
111
+ # end
112
+ # end
113
+
114
+ # describe 'using try_*' do
115
+ # describe 'not using transactions' do
116
+ # let(:config) { { try: true, transactional: false, timeout: 0.1 } }
117
+ # include_examples 'no race condition'
118
+ # end
119
+
120
+ # describe 'using transactions' do
121
+ # let(:config) { { try: true, transactional: true, timeout: 0.1 } }
122
+ # include_examples 'no race condition'
123
+ # end
124
+ # end
125
+
126
+ # describe 'not using try_*' do
127
+ # describe 'not using transactions' do
128
+ # let(:config) { { try: false, transactional: false, timeout: 0.1 } }
129
+ # include_examples 'no race condition'
130
+ # end
131
+
132
+ # describe 'using transactions' do
133
+ # let(:config) { { try: false, transactional: true, timeout: 0.1 } }
134
+ # include_examples 'no race condition'
135
+ # end
136
+ # end
137
+ # end
138
+ end
@@ -0,0 +1,17 @@
1
+ describe Travis::Lock::Redis do
2
+ let(:config) { { url: 'redis://localhost' } }
3
+ let(:client) { stub('redlock', lock: nil) }
4
+ let(:lock) { described_class.new(name, config) }
5
+ let(:name) { 'name' }
6
+
7
+ it 'yields' do
8
+ lock.exclusive { @called = true }
9
+ expect(@called).to eq(true)
10
+ end
11
+
12
+ it 'delegates to a Redlock instance' do
13
+ Redlock::Client.stubs(:new).returns(client)
14
+ client.expects(:lock).with(name, 300)
15
+ lock.exclusive {}
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'travis/lock/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "travis-lock"
8
+ s.version = Travis::Lock::VERSION
9
+ s.authors = ["Travis CI"]
10
+ s.email = "contact@travis-ci.org"
11
+ s.homepage = "https://github.com/travis-ci/travis-lock"
12
+ s.summary = "Travis CI config"
13
+ s.description = "#{s.summary}."
14
+ s.license = "MIT"
15
+
16
+ s.files = Dir['{lib/**/*,spec/**/*,[A-Z]*}']
17
+ s.platform = Gem::Platform::RUBY
18
+ s.require_path = 'lib'
19
+ s.rubyforge_project = '[none]'
20
+
21
+ s.add_dependency 'activerecord' , '~> 4.0'
22
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: travis-lock
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Travis CI
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ description: Travis CI config.
28
+ email: contact@travis-ci.org
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - Gemfile
34
+ - Gemfile.lock
35
+ - README.md
36
+ - lib/travis/lock.rb
37
+ - lib/travis/lock/none.rb
38
+ - lib/travis/lock/postgresql.rb
39
+ - lib/travis/lock/redis.rb
40
+ - lib/travis/lock/support/retry.rb
41
+ - lib/travis/lock/version.rb
42
+ - spec/integration.rb
43
+ - spec/spec_helper.rb
44
+ - spec/support/database.rb
45
+ - spec/travis/lock/none_spec.rb
46
+ - spec/travis/lock/postgresql_spec.rb
47
+ - spec/travis/lock/redis_spec.rb
48
+ - travis-lock.gemspec
49
+ homepage: https://github.com/travis-ci/travis-lock
50
+ licenses:
51
+ - MIT
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project: "[none]"
69
+ rubygems_version: 2.4.5
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Travis CI config
73
+ test_files: []
74
+ has_rdoc: