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 +7 -0
- data/bin/mongo_mutex +86 -0
- data/lib/mongo_mutex/mutex.rb +104 -0
- data/lib/mongo_mutex.rb +1 -0
- metadata +62 -0
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
|
data/lib/mongo_mutex.rb
ADDED
@@ -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: []
|