mongo_mutex 0.0.1

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: 8a899ddda53434cc9bcb3160e4cd2d4ab0cf6a8d
4
+ data.tar.gz: 60fc670259bba255522221e4ee73db1b498773ae
5
+ SHA512:
6
+ metadata.gz: af3c6349d873aa41ff186190f14d5e8c7f688171daa4a97614e62fb438772980b1bc578bc0335d1920b5741761fe4d264d8f239d0bb5faf90e72d8c27b8a56c8
7
+ data.tar.gz: 59f6faa7f7d3a9b2ec12e35104c55bcb793e2a56f0bb561ad9ff907f7417d18f46a0036b092f588082f470afb806dfd82b9b31e801dc03aa2a8962eae11fde57
data/bin/mongo_mutex ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $: << File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'mongo_mutex'
7
+ require 'socket'
8
+ require 'optionparser'
9
+ require 'open3'
10
+ Mongo::Logger.logger.level = ::Logger::FATAL
11
+
12
+ hosts = ['127.0.0.1']
13
+ database_name = 'mongo_mutex'
14
+ collection_name = 'mutex'
15
+ lock_name = nil
16
+ locker_name = Socket.gethostname
17
+ block_on_complete = nil
18
+ options = {
19
+ # logger: Logger.new(STDOUT)
20
+ }
21
+
22
+ OptionParser.new do |opts|
23
+ opts.on('-c CONFIG_FILE', 'Path to config file containing mongodb connection uri') do |filename|
24
+ hosts = File.readlines(filename).map(&:strip).reject(&:empty?)
25
+ end
26
+
27
+ opts.on('-h host1,host2', Array, 'MongoDB hosts (defaults to localhost)') do |h|
28
+ hosts = h.map(&:strip)
29
+ end
30
+
31
+ opts.on('-d DATABASE', '--database DATABASE', 'Database to use (default mongo_mutex)') do |db|
32
+ database_name = db
33
+ end
34
+
35
+ opts.on('-l LOCK_NAME', '--lock LOCK_NAME', 'Name of the distributed lock') do |lock|
36
+ lock_name = lock
37
+ end
38
+
39
+ opts.on('-n LOCKER_NAME', '--locker LOCKER_NAME', 'Name of the locking node (defaults to hostname)') do |name|
40
+ locker_name = name
41
+ end
42
+
43
+ opts.on('--retention N', Integer, 'Lock retention period in seconds (default 600)') do |retention|
44
+ options[:lock_retention_timeout] = retention
45
+ end
46
+
47
+ opts.on('--block N', Integer, 'On success, block other nodes from executing for N seconds') do |block_time|
48
+ block_on_complete = block_time
49
+ end
50
+ end.parse!
51
+
52
+ raise OptionParser::MissingArgument.new('Lock name is mandatory') unless lock_name
53
+
54
+ module Enumerable
55
+ alias :all_are? :all?
56
+ end
57
+
58
+ command = ARGV.join(' ')
59
+
60
+ collection = Mongo::Client.new(hosts, :database => database_name)[collection_name]
61
+ mutex = MongoMutex::Mutex.new(collection, lock_name, locker_name, options)
62
+ complete_mutex = MongoMutex::Mutex.new(collection, "#{lock_name}_complete", locker_name, options.merge(:lock_retention_timeout => block_on_complete)) if block_on_complete
63
+
64
+ mutex.synchronize do
65
+ if complete_mutex && complete_mutex.locked?
66
+ puts "Job already completed, skipping"
67
+ exit
68
+ end
69
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
70
+ maxlen = 1024
71
+ pipes = {
72
+ stdout => $stdout,
73
+ stderr => $stderr
74
+ }
75
+ until pipes.empty?
76
+ IO.select(pipes.keys).first.each do |io|
77
+ begin
78
+ pipes[io].write io.read_nonblock(maxlen)
79
+ rescue EOFError
80
+ pipes.delete(io)
81
+ end
82
+ end
83
+ end
84
+ complete_mutex && complete_mutex.try_lock
85
+ end
86
+ end
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+
3
+ require 'mongo'
4
+
5
+ module MongoMutex
6
+ class Mutex
7
+ def initialize(collection, lock_id, locker_id, options = {})
8
+ raise ArgumentError, "String lock id required" unless lock_id.is_a?(String)
9
+ raise ArgumentError, "String locker id required" unless locker_id.is_a?(String)
10
+ @collection = collection
11
+ @lock_id = lock_id
12
+ @locker_id = locker_id
13
+ options = options.dup
14
+ @lock_check_period = options.delete(:lock_check_period) || 5
15
+ @lock_retention_timeout = options.delete(:lock_retention_timeout) || 600
16
+ @clock = options.delete(:clock) || Time
17
+ @logger = options.delete(:logger)
18
+ @lock_operations = options.delete(:lock_operations) || MongoOperations.new
19
+ raise ArgumentError, "Unsupported options #{options.keys}" unless options.empty?
20
+ end
21
+
22
+ def try_lock
23
+ previous = @lock_operations.try_lock(@collection, @lock_id, @locker_id, @clock.now, @lock_retention_timeout)
24
+ if previous && (locked_by = previous[LOCKED_BY])
25
+ if expired?(previous[LOCKED_AT])
26
+ @logger.warn "Ignoring old #{@lock_id} lock by #{locked_by} since #{previous[LOCKED_AT]} was too long ago" if @logger
27
+ else
28
+ raise ThreadError, "mutex already locked by #{locked_by} at #{previous[LOCKED_AT]}"
29
+ end
30
+ end
31
+ true
32
+ rescue Mongo::Error::OperationFailure => error
33
+ if error.message.include?(DUPLICATE_KEY_ERROR)
34
+ return false
35
+ else
36
+ raise
37
+ end
38
+ end
39
+
40
+ def locked?
41
+ lock_info = @lock_operations.lock_info(@collection, @lock_id)
42
+ lock_info && lock_info[LOCKED_BY] && !expired?(lock_info[LOCKED_AT])
43
+ end
44
+
45
+ def lock
46
+ until try_lock
47
+ sleep @lock_check_period
48
+ end
49
+ self
50
+ end
51
+
52
+ def unlock
53
+ unless @lock_operations.unlock(@collection, @lock_id, @locker_id, @clock.now, @lock_retention_timeout)
54
+ raise ThreadError, 'lock is either not locked or locked by someone else'
55
+ end
56
+ self
57
+ end
58
+
59
+ def synchronize(&block)
60
+ lock
61
+ begin
62
+ yield
63
+ ensure
64
+ unlock
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ LOCKED_BY = 'locked_by'.freeze
71
+ LOCKED_AT = 'locked_at'.freeze
72
+
73
+ def expired?(time)
74
+ !time || time < @clock.now - @lock_retention_timeout
75
+ end
76
+
77
+ DUPLICATE_KEY_ERROR = 'E11000'
78
+
79
+ class MongoOperations
80
+ def lock_info(collection, lock_id)
81
+ collection.find({_id: lock_id}, limit: 1).first
82
+ end
83
+
84
+ def try_lock(collection, lock_id, locker_id, now, lock_retention_timeout)
85
+ collection.find_one_and_update(
86
+ {:_id => lock_id, :$or => [
87
+ {locked_at: {:$lt => now - lock_retention_timeout}},
88
+ {locked_by: locker_id},
89
+ {locked_by: {:$exists => 0}},
90
+ ]},
91
+ {_id: lock_id, locked_by: locker_id, locked_at: now},
92
+ upsert: true,
93
+ )
94
+ end
95
+
96
+ def unlock(collection, lock_id, locker_id, now, lock_retention_support)
97
+ collection.find_one_and_update(
98
+ {_id: lock_id, locked_by: locker_id, locked_at: {:$gte => now - lock_retention_support}},
99
+ {:$unset => {locked_by: 1, locked_at: 1}},
100
+ )
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1 @@
1
+ require 'mongo_mutex/mutex'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongo_mutex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - David Dahl
8
+ - Gustav Munkby
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-11-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongo
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.1'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.1'
28
+ description: A distributed lock using MongoDB as backend
29
+ email: david@burtcorp.com
30
+ executables:
31
+ - mongo_mutex
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - bin/mongo_mutex
36
+ - lib/mongo_mutex.rb
37
+ - lib/mongo_mutex/mutex.rb
38
+ homepage: https://github.com/effata/mongo_mutex
39
+ licenses:
40
+ - BSD-3-Clause
41
+ metadata: {}
42
+ post_install_message:
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 2.4.8
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Mongo Mutex
62
+ test_files: []