periodically 0.0.4 → 0.0.5
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 +4 -4
- data/.editorconfig +1 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +0 -9
- data/README.md +30 -2
- data/lib/periodically/debug.rb +19 -2
- data/lib/periodically/defer.rb +34 -0
- data/lib/periodically/job.rb +29 -10
- data/lib/periodically.rb +16 -6
- data/periodically.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f35124533240ee8deb6be9e68e68886950898319c94b6dbdccd9be8f515bc0d
|
4
|
+
data.tar.gz: 1ff62610c643f3e9e226d5ed7747c5d5297b88d486516f40d57a0381dc777cc1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23fef90a350aa3ba3e7786236c1fcd1b89576c79f6757538e0424328c90daa1ffb3d583e1fed8f0d0fec0e09bf1c8b2115168c565ba2ebe6312813d507402f6f
|
7
|
+
data.tar.gz: 9eff6deaafd19e8211bfa7142bf0ae7a99906ff8c30e4155dd6ff0aebda52516c87ebdf2009631f8e0a5d291498a5207d6d3bd787ef167d2e316ff35b37d0194
|
data/.editorconfig
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -14,11 +14,6 @@ GEM
|
|
14
14
|
zeitwerk (~> 2.2)
|
15
15
|
ast (2.4.0)
|
16
16
|
concurrent-ruby (1.1.5)
|
17
|
-
et-orbi (1.2.2)
|
18
|
-
tzinfo
|
19
|
-
fugit (1.3.3)
|
20
|
-
et-orbi (~> 1.1, >= 1.1.8)
|
21
|
-
raabro (~> 1.1)
|
22
17
|
i18n (1.7.0)
|
23
18
|
concurrent-ruby (~> 1.0)
|
24
19
|
jaro_winkler (1.5.4)
|
@@ -26,7 +21,6 @@ GEM
|
|
26
21
|
parallel (1.19.1)
|
27
22
|
parser (2.7.0.1)
|
28
23
|
ast (~> 2.4.0)
|
29
|
-
raabro (1.1.6)
|
30
24
|
rainbow (3.0.0)
|
31
25
|
rake (13.0.1)
|
32
26
|
redis (4.1.3)
|
@@ -42,8 +36,6 @@ GEM
|
|
42
36
|
rubocop-performance (1.5.2)
|
43
37
|
rubocop (>= 0.71.0)
|
44
38
|
ruby-progressbar (1.10.1)
|
45
|
-
rufus-scheduler (3.6.0)
|
46
|
-
fugit (~> 1.1, >= 1.1.6)
|
47
39
|
standard (0.1.7)
|
48
40
|
rubocop (~> 0.77.0)
|
49
41
|
rubocop-performance (~> 1.5.1)
|
@@ -61,7 +53,6 @@ DEPENDENCIES
|
|
61
53
|
minitest
|
62
54
|
rake
|
63
55
|
redis-namespace
|
64
|
-
rufus-scheduler
|
65
56
|
standard
|
66
57
|
|
67
58
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -95,9 +95,14 @@ def update_method
|
|
95
95
|
return if status == "pending"
|
96
96
|
|
97
97
|
# Log error and defer execution
|
98
|
-
# This unique instance will be deferred for later execution (using exponential backoff) and the error logged
|
98
|
+
# This unique instance will be deferred for later execution (using exponential backoff) and the error is logged
|
99
99
|
raise "something went wrong" if status == "error"
|
100
100
|
|
101
|
+
# Defer any further calls to :update_method (on any instance)
|
102
|
+
# This is perfect for short-time rate limiting, but highly discouraged as a method of timing updates!
|
103
|
+
# You should use database columns (e.g. last_synced like this method) instead, which causes less stress on Redis
|
104
|
+
return Periodically::Defer.job_by(60.minutes) if status == "rate_limited"
|
105
|
+
|
101
106
|
# Update checked delay
|
102
107
|
# Updates the property we check against, thus making this instance not pass the Periodically condition
|
103
108
|
# Note that this line is normal Rails code: Periodically conditions are database/anything-agnostic
|
@@ -105,7 +110,30 @@ def update_method
|
|
105
110
|
end
|
106
111
|
```
|
107
112
|
|
108
|
-
The job method's return value can be used to defer further execution of the
|
113
|
+
The job method's return value can be used to defer further execution of either the model instance, the specific job within the model or any job within the model:
|
114
|
+
|
115
|
+
- `Periodically::Defer.instance_by(60.minutes) # calling :update_method on this instance will be delayed`
|
116
|
+
- `Periodically::Defer.job_by(60.minutes) # calling :update_method on any instance of this class will be delayed`
|
117
|
+
- `Periodically::Defer.class_by(60.minutes) # calling anything on any instance of this class will be delayed`
|
118
|
+
|
119
|
+
## Testing
|
120
|
+
|
121
|
+
What do you want to test?
|
122
|
+
|
123
|
+
**Behavior of my update callbacks**
|
124
|
+
|
125
|
+
Just call them by yourself in your tests.
|
126
|
+
|
127
|
+
**The periodically :on condition**
|
128
|
+
|
129
|
+
(TODO this does not exist yet)
|
130
|
+
|
131
|
+
You can call `Periodically.would_execute?(MyModel, :object)` to statically check the condition.
|
132
|
+
|
133
|
+
## Debugging
|
134
|
+
|
135
|
+
As part of your application you might want to be able to get a quick snapshot of how Periodically is doing.
|
136
|
+
You can do that by calling `Periodically::Debug.total_debug_dump`, which returns a hash containing bunch of debug information.
|
109
137
|
|
110
138
|
## Dashboard
|
111
139
|
|
data/lib/periodically/debug.rb
CHANGED
@@ -9,9 +9,26 @@ module Periodically
|
|
9
9
|
Hash[keys.zip(values)]
|
10
10
|
end
|
11
11
|
locks = Periodically.redis do |conn|
|
12
|
-
conn.scan_each(:match => "locks:*").to_a
|
12
|
+
keys = conn.scan_each(:match => "locks:*").to_a
|
13
|
+
ttls = conn.eval(%{
|
14
|
+
local matcher = KEYS[1] .. ":*"
|
15
|
+
local ttls = {}
|
16
|
+
|
17
|
+
local cursor = "0"
|
18
|
+
repeat
|
19
|
+
local result = redis.call("SCAN", cursor, "MATCH", matcher)
|
20
|
+
cursor = result[1]
|
21
|
+
|
22
|
+
local keys = result[2]
|
23
|
+
for i, key in ipairs(keys) do
|
24
|
+
ttls[#ttls + 1] = redis.call('ttl', key)
|
25
|
+
end
|
26
|
+
until cursor == "0"
|
27
|
+
return ttls
|
28
|
+
}, :keys => ['locks'])
|
29
|
+
Hash[keys.zip(ttls)]
|
13
30
|
end
|
14
|
-
{ job_count: Periodically._registered_jobs.size, error_counts: error_counts,
|
31
|
+
{ job_count: Periodically._registered_jobs.size, error_counts: error_counts, lock_ttls: locks }
|
15
32
|
end
|
16
33
|
end
|
17
34
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Periodically
|
4
|
+
module Defer
|
5
|
+
def self.instance_by(duration)
|
6
|
+
DeferInstance.new(duration)
|
7
|
+
end
|
8
|
+
def self.job_by(duration)
|
9
|
+
DeferJob.new(duration)
|
10
|
+
end
|
11
|
+
def self.class_by(duration)
|
12
|
+
DeferClass.new(duration)
|
13
|
+
end
|
14
|
+
|
15
|
+
class DeferInstance
|
16
|
+
attr_reader :duration
|
17
|
+
def initialize(duration)
|
18
|
+
@duration = duration
|
19
|
+
end
|
20
|
+
end
|
21
|
+
class DeferJob
|
22
|
+
attr_reader :duration
|
23
|
+
def initialize(duration)
|
24
|
+
@duration = duration
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class DeferClass
|
28
|
+
attr_reader :duration
|
29
|
+
def initialize(duration)
|
30
|
+
@duration = duration
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/periodically/job.rb
CHANGED
@@ -7,11 +7,14 @@ module Periodically
|
|
7
7
|
def initialize(klass, method, opts)
|
8
8
|
@klass = klass
|
9
9
|
@method = method
|
10
|
-
@
|
10
|
+
@class_key = klass.name
|
11
|
+
@job_key = "#{@class_key}/#{method.to_s}"
|
11
12
|
@opts = opts
|
12
13
|
end
|
13
14
|
|
14
15
|
def poll_next_instance
|
16
|
+
return if job_or_class_locked?
|
17
|
+
|
15
18
|
ActiveRecord::Base.uncached do
|
16
19
|
where = @opts[:on].call()
|
17
20
|
where.to_a.find do |obj|
|
@@ -25,43 +28,59 @@ module Periodically
|
|
25
28
|
begin
|
26
29
|
instance.send(@method)
|
27
30
|
rescue => e
|
28
|
-
Periodically.logger.error("Job instance[#{instance}] execution raised an
|
29
|
-
|
31
|
+
Periodically.logger.error("Job instance[#{instance}] execution raised an exception\n#{e.message}\n#{e.backtrace.join("\n")}")
|
30
32
|
new_error_count = increase_instance_error_count(instance)
|
31
33
|
lock_instance(instance, DEFAULT_ERROR_DELAY.call(new_error_count))
|
32
34
|
return
|
33
35
|
end
|
34
36
|
|
37
|
+
if return_value.is_a?(Periodically::Defer::DeferInstance)
|
38
|
+
lock_instance(instance, return_value.duration)
|
39
|
+
elsif return_value.is_a?(Periodically::Defer::DeferJob)
|
40
|
+
Job.lock_key("locks:#{@job_key}", return_value.duration)
|
41
|
+
elsif return_value.is_a?(Periodically::Defer::DeferClass)
|
42
|
+
Job.lock_key("locks:#{@class_key}", return_value.duration)
|
43
|
+
end
|
44
|
+
|
35
45
|
clear_instance_error_count(instance)
|
36
46
|
end
|
37
47
|
|
38
48
|
private
|
39
49
|
|
40
|
-
def
|
50
|
+
def instance_key(instance)
|
41
51
|
"#{@job_key}/#{instance.id}"
|
42
52
|
end
|
43
53
|
|
44
54
|
def increase_instance_error_count(instance)
|
45
|
-
error_count_key = "errors:#{
|
55
|
+
error_count_key = "errors:#{instance_key(instance)}"
|
46
56
|
Periodically.redis {|conn| conn.incr(error_count_key)}
|
47
57
|
end
|
48
58
|
|
49
59
|
def clear_instance_error_count(instance)
|
50
|
-
error_count_key = "errors:#{
|
60
|
+
error_count_key = "errors:#{instance_key(instance)}"
|
51
61
|
Periodically.redis {|conn| conn.del(error_count_key)}
|
52
62
|
end
|
53
63
|
|
64
|
+
def job_or_class_locked?
|
65
|
+
Periodically.redis do |conn|
|
66
|
+
# TODO can this be optimized with exists?(Array)
|
67
|
+
conn.exists("locks:#{@job_key}") || conn.exists("locks:#{@class_key}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
54
71
|
def instance_locked?(instance)
|
55
|
-
Periodically.redis {|conn| conn.exists("locks:#{
|
72
|
+
Periodically.redis {|conn| conn.exists("locks:#{instance_key(instance)}")}
|
56
73
|
end
|
57
74
|
|
58
75
|
def lock_instance(instance, seconds)
|
59
|
-
lock_key
|
76
|
+
Job.lock_key("locks:#{instance_key(instance)}", seconds)
|
77
|
+
end
|
60
78
|
|
79
|
+
def self.lock_key(key, seconds)
|
61
80
|
Periodically.redis do |conn|
|
62
81
|
conn.multi do |multi|
|
63
|
-
multi.set(
|
64
|
-
multi.expire(
|
82
|
+
multi.set(key, "1")
|
83
|
+
multi.expire(key, seconds)
|
65
84
|
end
|
66
85
|
end
|
67
86
|
end
|
data/lib/periodically.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "periodically/job"
|
4
4
|
require "periodically/debug"
|
5
|
+
require "periodically/defer"
|
5
6
|
require "periodically/redis"
|
6
7
|
require "periodically/model"
|
7
8
|
|
@@ -22,7 +23,12 @@ module Periodically
|
|
22
23
|
[job, instance] if instance
|
23
24
|
end.first
|
24
25
|
|
25
|
-
|
26
|
+
if job && instance
|
27
|
+
job.execute_instance(instance)
|
28
|
+
true
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
26
32
|
end
|
27
33
|
|
28
34
|
def self.register(klass, method, opts)
|
@@ -45,13 +51,17 @@ module Periodically
|
|
45
51
|
end
|
46
52
|
|
47
53
|
def self.start
|
48
|
-
require "rufus-scheduler"
|
49
|
-
scheduler = Rufus::Scheduler.new
|
50
|
-
|
51
54
|
Periodically.redis { |conn| conn.ping }
|
52
55
|
|
53
|
-
|
54
|
-
|
56
|
+
Thread.new do
|
57
|
+
while true
|
58
|
+
executed = Periodically.execute_next
|
59
|
+
if executed
|
60
|
+
sleep 1.seconds
|
61
|
+
else
|
62
|
+
sleep 10.seconds
|
63
|
+
end
|
64
|
+
end
|
55
65
|
end
|
56
66
|
end
|
57
67
|
end
|
data/periodically.gemspec
CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |gem|
|
|
4
4
|
|
5
5
|
gem.files = `git ls-files | grep -Ev '^(test|myapp|examples)'`.split("\n")
|
6
6
|
gem.name = "periodically"
|
7
|
-
gem.version = "0.0.
|
7
|
+
gem.version = "0.0.5"
|
8
8
|
gem.required_ruby_version = ">= 2.5.0"
|
9
9
|
|
10
10
|
gem.add_dependency "redis", ">= 4.1.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: periodically
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- wyozi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-01-
|
11
|
+
date: 2020-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -66,6 +66,7 @@ files:
|
|
66
66
|
- lib/periodically.rb
|
67
67
|
- lib/periodically/cli.rb
|
68
68
|
- lib/periodically/debug.rb
|
69
|
+
- lib/periodically/defer.rb
|
69
70
|
- lib/periodically/job.rb
|
70
71
|
- lib/periodically/model.rb
|
71
72
|
- lib/periodically/redis.rb
|