periodically 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|