mongo_mutex 0.0.1

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: 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: []