travis-lock 0.1.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 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: