unique_thread 0.1.0

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