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 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