unique_thread 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 +7 -0
- data/lib/unique_thread/locksmith.rb +99 -0
- data/lib/unique_thread/stopwatch.rb +30 -0
- data/lib/unique_thread.rb +42 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2861f1fe74d69bad9f3cd3b472d8f8442b676eeb
|
4
|
+
data.tar.gz: a9c158c7b3e95b56739b386f4ded1ad6028b8889
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f5db23b797560f957aa04de3f8590353f13a922f56f367901860c0f9e68ab946084c1cd45b5e08b97c197a68770fab70e2382666b0a68d776688b5a103218cff
|
7
|
+
data.tar.gz: 9912187f3f39a9f29e1742d591b1f99b90ce5e68b945fd1fb388743461633a1ee582461f83ca03fb1902e5e38ea276cbbd03b149638e9cba7b0425ef08b196d4
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class UniqueThread
|
4
|
+
class Locksmith
|
5
|
+
attr_reader :name, :stopwatch, :redis, :logger
|
6
|
+
|
7
|
+
def initialize(name:, stopwatch:, redis:, logger:)
|
8
|
+
@name = name
|
9
|
+
@stopwatch = stopwatch
|
10
|
+
@redis = redis
|
11
|
+
@logger = logger
|
12
|
+
|
13
|
+
@lua_scripts = Hash[Dir[File.join(__dir__, 'redis_lua', '*.lua')].map do |lua_file|
|
14
|
+
[File.basename(lua_file, '.lua').to_sym, redis.script(:load, File.read(lua_file))]
|
15
|
+
end]
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_lock
|
19
|
+
lock_from_redis_command(:get_lock, name, stopwatch.now, stopwatch.next_renewal)
|
20
|
+
end
|
21
|
+
|
22
|
+
def renew_lock(lock)
|
23
|
+
lock_from_redis_command(:extend_lock, name, lock.locked_until, stopwatch.next_renewal)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
RedisResult = Struct.new(:lock_acquired, :locked_until) do
|
29
|
+
def lock_acquired?
|
30
|
+
lock_acquired == '1'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :lua_scripts
|
35
|
+
|
36
|
+
def lock_from_redis_command(script, *args)
|
37
|
+
redis_result = RedisResult.new(*redis.evalsha(lua_scripts[script], args))
|
38
|
+
|
39
|
+
klass = if redis_result.lock_acquired?
|
40
|
+
HeldLock
|
41
|
+
else
|
42
|
+
Lock
|
43
|
+
end
|
44
|
+
|
45
|
+
klass.new(redis_result.locked_until, stopwatch, self, logger)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
class Lock
|
52
|
+
attr_reader :locked_until, :stopwatch, :locksmith, :logger
|
53
|
+
|
54
|
+
def initialize(locked_until, stopwatch, locksmith, logger)
|
55
|
+
@locked_until = locked_until
|
56
|
+
@stopwatch = stopwatch
|
57
|
+
@locksmith = locksmith
|
58
|
+
@logger = logger
|
59
|
+
end
|
60
|
+
|
61
|
+
def acquired?
|
62
|
+
false
|
63
|
+
end
|
64
|
+
|
65
|
+
def while_held
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class HeldLock < Lock
|
71
|
+
def acquired?
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def while_held
|
76
|
+
worker = Thread.new do
|
77
|
+
yield
|
78
|
+
logger.error('The blocked passed is not an infinite loop.')
|
79
|
+
end
|
80
|
+
|
81
|
+
renew_indefinitely
|
82
|
+
|
83
|
+
logger.info('Lock lost! Killing the unique thread.')
|
84
|
+
worker.terminate
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def renew_indefinitely
|
90
|
+
active_lock = self
|
91
|
+
|
92
|
+
while active_lock.acquired?
|
93
|
+
logger.debug('Lock renewed! Sleeping until next renewal attempt.')
|
94
|
+
stopwatch.sleep_until_renewal_attempt
|
95
|
+
active_lock = locksmith.renew_lock(active_lock)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class UniqueThread
|
4
|
+
class Stopwatch
|
5
|
+
attr_reader :downtime
|
6
|
+
|
7
|
+
def initialize(downtime:)
|
8
|
+
@downtime = downtime.to_f
|
9
|
+
end
|
10
|
+
|
11
|
+
def now
|
12
|
+
Time.now.to_f
|
13
|
+
end
|
14
|
+
|
15
|
+
# FIXME: Bad name. Lap maybe? Milestone?
|
16
|
+
def next_renewal
|
17
|
+
now + (downtime * 2 / 3)
|
18
|
+
end
|
19
|
+
|
20
|
+
def sleep_until_next_attempt(locked_until)
|
21
|
+
seconds_until_next_attempt = [locked_until - now + Random.new.rand(downtime / 3), 0].max
|
22
|
+
|
23
|
+
Kernel.sleep(seconds_until_next_attempt)
|
24
|
+
end
|
25
|
+
|
26
|
+
def sleep_until_renewal_attempt
|
27
|
+
Kernel.sleep(downtime / 3)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'redis'
|
5
|
+
require_relative 'unique_thread/stopwatch'
|
6
|
+
require_relative 'unique_thread/locksmith'
|
7
|
+
|
8
|
+
class UniqueThread
|
9
|
+
attr_reader :logger, :stopwatch, :locksmith
|
10
|
+
|
11
|
+
def initialize(name, downtime: 30, logger: Logger.new(STDOUT), redis: Redis.new)
|
12
|
+
@logger = logger
|
13
|
+
@stopwatch = Stopwatch.new(downtime: downtime)
|
14
|
+
@locksmith = Locksmith.new(name: name, stopwatch: stopwatch, redis: redis, logger: logger)
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(&block)
|
18
|
+
safe_infinite_loop do
|
19
|
+
lock = locksmith.new_lock
|
20
|
+
|
21
|
+
if lock.acquired?
|
22
|
+
logger.info('Lock acquired! Running the unique thread.')
|
23
|
+
lock.while_held(&block)
|
24
|
+
else
|
25
|
+
logger.debug('Could not acquire the lock. Sleeping until next attempt.')
|
26
|
+
stopwatch.sleep_until_next_attempt(lock.locked_until.to_f)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def safe_infinite_loop
|
34
|
+
Thread.new do
|
35
|
+
begin
|
36
|
+
loop { yield }
|
37
|
+
rescue StandardError => error
|
38
|
+
logger.error(error)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unique_thread
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fernando Seror
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-05-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5'
|
33
|
+
description:
|
34
|
+
email: ferdy89@gmail.com
|
35
|
+
executables: []
|
36
|
+
extensions: []
|
37
|
+
extra_rdoc_files: []
|
38
|
+
files:
|
39
|
+
- lib/unique_thread.rb
|
40
|
+
- lib/unique_thread/locksmith.rb
|
41
|
+
- lib/unique_thread/stopwatch.rb
|
42
|
+
homepage: https://github.com/Ferdy89/unique_thread
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.6.14
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Allows a block of code to be run once across many processes
|
66
|
+
test_files: []
|