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