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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6b138f5577f4135c5cc3b9114624be617202245cbb66419cea0d2fe9a65e32c
4
- data.tar.gz: bc2d6f5942c6188aa6054975613794481605c49bc58409e2437a474c8e727a3b
3
+ metadata.gz: 1f35124533240ee8deb6be9e68e68886950898319c94b6dbdccd9be8f515bc0d
4
+ data.tar.gz: 1ff62610c643f3e9e226d5ed7747c5d5297b88d486516f40d57a0381dc777cc1
5
5
  SHA512:
6
- metadata.gz: 8ea4cd75750d13a4e9efa42f28ad8c2a680d5ec43ce2b1b03451d1e2ecf0a7a020c81dd8ca74725d1792db74ef7269456072db9ea253675fc64602aa770f1769
7
- data.tar.gz: e7ccc2c21b2be69296a826bb6106564d8412e528d533ba53483fba17b23a53e260fea865c7abd6226ceddf04c2390cad364563b8d498edbf8639b99b91a3e52b
6
+ metadata.gz: 23fef90a350aa3ba3e7786236c1fcd1b89576c79f6757538e0424328c90daa1ffb3d583e1fed8f0d0fec0e09bf1c8b2115168c565ba2ebe6312813d507402f6f
7
+ data.tar.gz: 9eff6deaafd19e8211bfa7142bf0ae7a99906ff8c30e4155dd6ff0aebda52516c87ebdf2009631f8e0a5d291498a5207d6d3bd787ef167d2e316ff35b37d0194
data/.editorconfig CHANGED
@@ -4,6 +4,6 @@ root = true
4
4
  end_of_line = lf
5
5
  insert_final_newline = true
6
6
 
7
- [*.{rb,gemspec}}]
7
+ [*.{rb,gemspec}]
8
8
  indent_style = space
9
9
  indent_size = 2
data/Gemfile CHANGED
@@ -3,7 +3,6 @@ source 'https://rubygems.org'
3
3
  gem 'rake'
4
4
  gem 'activerecord'
5
5
  gem 'redis-namespace'
6
- gem 'rufus-scheduler'
7
6
 
8
7
  group :test do
9
8
  gem 'minitest'
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 same model instance, even if it still passes the condition: `return Periodically::Defer(60.minutes)`
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
 
@@ -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, locks: locks }
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
@@ -7,11 +7,14 @@ module Periodically
7
7
  def initialize(klass, method, opts)
8
8
  @klass = klass
9
9
  @method = method
10
- @job_key = "#{klass.name}/#{method.to_s}"
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 error: #{e}")
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 full_instance_key(instance)
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:#{full_instance_key(instance)}"
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:#{full_instance_key(instance)}"
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:#{full_instance_key(instance)}")}
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 = "locks:#{full_instance_key(instance)}"
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(lock_key, "1")
64
- multi.expire(lock_key, seconds)
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
- job.execute_instance(instance) if job && instance
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
- scheduler.interval "10s" do
54
- Periodically.execute_next
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.4"
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
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-06 00:00:00.000000000 Z
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