periodically 0.0.5 → 0.0.6

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: 1f35124533240ee8deb6be9e68e68886950898319c94b6dbdccd9be8f515bc0d
4
- data.tar.gz: 1ff62610c643f3e9e226d5ed7747c5d5297b88d486516f40d57a0381dc777cc1
3
+ metadata.gz: db828c2104d540e27fda8d4896b3ea91620c6b40034ef7be09678938bd57eba3
4
+ data.tar.gz: 4dad188a4afe02239af267042e3fea818c7544889dcfa17962a83f7fa00735c9
5
5
  SHA512:
6
- metadata.gz: 23fef90a350aa3ba3e7786236c1fcd1b89576c79f6757538e0424328c90daa1ffb3d583e1fed8f0d0fec0e09bf1c8b2115168c565ba2ebe6312813d507402f6f
7
- data.tar.gz: 9eff6deaafd19e8211bfa7142bf0ae7a99906ff8c30e4155dd6ff0aebda52516c87ebdf2009631f8e0a5d291498a5207d6d3bd787ef167d2e316ff35b37d0194
6
+ metadata.gz: 30e5029889fc90ff67fd12cc9e2be6eedf438474130d03ba41474998415747230f57a7e0aea424afd5c32274c3d2b89fa1b0eb61a4f2a5ee640e17cb5b30853c
7
+ data.tar.gz: 010cbdfd28fe74a99a9d4ed7ce3ea1062f14726fd9a92239a7c8f389c7a393bcebdea5d26af83fa289d6bac7a4391506c21ec85c732d0e3ffbcd805d8e163ea7
@@ -0,0 +1,18 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v1
10
+ - name: Set up Ruby 2.6
11
+ uses: actions/setup-ruby@v1
12
+ with:
13
+ ruby-version: 2.6.x
14
+ - name: Build and test with Rake
15
+ run: |
16
+ gem install bundler
17
+ bundle install --jobs 4 --retry 3
18
+ bundle exec rake test
data/Gemfile CHANGED
@@ -3,9 +3,11 @@ source 'https://rubygems.org'
3
3
  gem 'rake'
4
4
  gem 'activerecord'
5
5
  gem 'redis-namespace'
6
+ gem 'connection_pool'
6
7
 
7
8
  group :test do
8
9
  gem 'minitest'
10
+ gem 'mock_redis'
9
11
  end
10
12
 
11
13
  group :development, :test do
data/Gemfile.lock CHANGED
@@ -14,10 +14,12 @@ GEM
14
14
  zeitwerk (~> 2.2)
15
15
  ast (2.4.0)
16
16
  concurrent-ruby (1.1.5)
17
+ connection_pool (2.2.2)
17
18
  i18n (1.7.0)
18
19
  concurrent-ruby (~> 1.0)
19
20
  jaro_winkler (1.5.4)
20
21
  minitest (5.13.0)
22
+ mock_redis (0.22.0)
21
23
  parallel (1.19.1)
22
24
  parser (2.7.0.1)
23
25
  ast (~> 2.4.0)
@@ -50,7 +52,9 @@ PLATFORMS
50
52
 
51
53
  DEPENDENCIES
52
54
  activerecord
55
+ connection_pool
53
56
  minitest
57
+ mock_redis
54
58
  rake
55
59
  redis-namespace
56
60
  standard
data/README.md CHANGED
@@ -1,15 +1,19 @@
1
1
  # periodically
2
2
 
3
- Redis-backed Ruby library for periodically running tasks, whose execution depends can depend on a custom lambda block (e.g. weekly syncs).
4
- The job execution won't be guaranteed to happen exactly as the condition is fulfilled, hence periodically is best for
5
- nonfrequent and noncritical jobs.
3
+ Redis-backed Ruby library for tasks that need to run once in a while. "Once in a while" is defined more accurately by a custom lambda
4
+ block by the library user.
5
+
6
+ Since task execution is done in a single threa using non-accurate and non-time-based conditions, periodically is best for
7
+ infrequent and noncritical jobs, such as weekly syncs.
6
8
 
7
9
  Example usecases:
8
10
 
9
- - Sync a Rails model's data once per week
10
- - Achievable by e.g. a `last_synced` value in the database
11
- - Launch a non-important sync operation depending on a specific condition (e.g. NULL value in some database column)
12
- - Achievable by checking the column against NULL inside the `on` condition
11
+ - Sync a Rails model's data once per week from an external source. External source can be unstable, so you'd like to repeat failing jobs but some amount of marginally outdated data won't be a problem
12
+ - Great fit for periodically! Can be achieved for example by a `last_synced` value in the database and a condition based on it
13
+ - Additionally, periodically supports deferring jobs (meaning one line rate limiting!)
14
+ - Apply asynchronous corrections to data added to the database. For example automatically fetch title for user-submitted links.
15
+ - Achievable by adding a condition against the correctable column (`where(title: nil)`)
16
+ - However, a background job processor like Sidekiq would likely be more efficient
13
17
 
14
18
  ## Getting started with Rails
15
19
 
@@ -21,6 +25,8 @@ Example usecases:
21
25
 
22
26
  ```rb
23
27
  require "periodically"
28
+
29
+ # Launches a background thread. For production usage you may want to do this in another process
24
30
  Periodically.start
25
31
  ```
26
32
 
@@ -37,12 +43,13 @@ class Item < ApplicationRecord
37
43
  include Periodically::Model
38
44
 
39
45
  periodically :refresh_price,
40
- on: -> { Item.where("last_synced < ?", 7.days.ago) }
46
+ on: -> { Item.where("last_synced < ?", 7.days.ago) }
41
47
 
42
48
  private
43
49
 
44
50
  def refresh_price
45
51
  self.price = PriceFetcher.fetch(item_id)
52
+ self.last_synced = Time.now! # Remember to update the condition by yourself!
46
53
  save!
47
54
  end
48
55
  end
@@ -74,7 +81,7 @@ include Periodically::Model
74
81
  periodically :update_method, # call instance method "update_method" for found instances
75
82
  on: -> { where("last_synced < ?", 7.days.ago) }, # Condition that must be true for update method to be called
76
83
  min_class_interval: 5.minutes, # (Optional) The minimum interval between calls to this specific class (TODO not implemented)
77
- max_retries: 25, # (Optional) Maximum number of retries. Periodically uses exponential backoff
84
+ max_retries: 25, # (Optional) Maximum number of retries. Periodically uses exponential backoff (TODO not implemented)
78
85
  instance_id: -> { cache_key_with_version }, # (Optional) Returns this instance's unique identifying key. Used for e.g. deferring jobs and marking them as erroring (TODO not implemented)
79
86
 
80
87
 
@@ -126,29 +133,28 @@ Just call them by yourself in your tests.
126
133
 
127
134
  **The periodically :on condition**
128
135
 
129
- (TODO this does not exist yet)
130
-
131
- You can call `Periodically.would_execute?(MyModel, :object)` to statically check the condition.
136
+ You can call `Periodically.would_execute?(MyModel, :object)` to statically check the condition. (TODO not implemented)
132
137
 
133
138
  ## Debugging
134
139
 
135
- As part of your application you might want to be able to get a quick snapshot of how Periodically is doing.
140
+ As part of your application you might want to be able to get a quick snapshot of how periodically is doing.
136
141
  You can do that by calling `Periodically::Debug.total_debug_dump`, which returns a hash containing bunch of debug information.
137
142
 
138
143
  ## Dashboard
139
144
 
140
- (When implemented :D) Dashboard contains recently succeeded executions, failed executions (with stacktrace) and deferred executions.
145
+ Dashboard contains recently succeeded executions, failed executions (with stacktrace) and deferred executions. (TODO not implemented)
141
146
 
142
147
  ## Why not Sidekiq?
143
148
 
144
- With Sidekiq you can achieve something almost similar by combining a scheduled job that enqueues further unique jobs based on the condition.
149
+ With Sidekiq you can achieve something almost similar by combining a scheduled job that enqueues further unique jobs based on the condition. (see https://github.com/mperham/sidekiq/wiki/Ent-Periodic-Jobs#dynamic-jobs)
145
150
 
146
- However, there are few advantages Periodically has for the specific usecase of per-instance non-critical jobs:
151
+ However, there are few advantages periodically has for the specific usecase of per-instance non-critical jobs:
147
152
 
148
- - **Improved backpressure handling.** Due to knowing the conditions, we are able to track the number of pending jobs at all times. This enables early warnings for the developers in case of job buildup. (TODO)
149
- - **Better observability.** We know exactly how many items fulfill the condition and how many don't; therefore, we can visualize success rates and current status as a percentage of the total. (TODO)
150
- - **Cleaner per-instance retrying.** If we start executing a job, but suddenly want to defer execution by some time in Sidekiq, it is definitely doable with scheduled jobs. However, this may entrap you in a "scheduled unique job" hell: if some job keeps getting mistakenly deferred, it might be hard to find out about this behavior without some complex job tracking logic. In contrast, Periodically delivers this functionality for free due to more explicit control over job scheduling and rescheduling. (TODO)
151
- - **More clever polling.** Since we know the exact condition for new periodic jobs, we can deduce the next execution time and sleep accordingly. (TODO)
152
- - **Easier priority escalation.** Periodically selects jobs in order of the given condition and maintains no queue of its own; therefore it is trivial to prioritize certain jobs by adding a new query condition.
153
+ - **Improved backpressure handling.** Since we know the conditions for executing jobs, we are in better control of the job producer and able to balance between different jobs. This enables for instance early warnings for the developers in case of job buildup. (TODO not implemented)
154
+ - **Closer to the source.** Periodic callbacks are defined inside the models, so it is always easy to find which jobs are affecting which models.
155
+ - **Cleaner per-instance retrying.** If we start executing a job, but suddenly want to defer execution by some time in Sidekiq, it is definitely doable with scheduled jobs. However, this may entrap you in a "scheduled unique job" hell: if some job keeps getting mistakenly deferred, it might be hard to find out about the repeated erroneous behavior without some complex job tracking logic. In contrast, periodically delivers this functionality for free due to more explicit control over job scheduling and rescheduling. (TODO not implemented)
156
+ - **More flexible delaying.** In periodically, you can delay job executions per-instance, per-job or per-class.
157
+ - **More clever polling.** Since we know the exact condition for new periodic jobs, we can deduce the next execution time and sleep accordingly. (TODO not implemented)
158
+ - **Easier priority escalation.** Periodically selects jobs in order of the given condition and maintains no queue of its own; therefore prioritizing certain jobs by adding a new query condition comes by design.
153
159
 
154
- Importantly, Sidekiq and Periodically aim to solve different problems. Nothing prevents one from using both at the same time.
160
+ Importantly, Sidekiq and periodically aim to solve different problems. Nothing prevents one from using both at the same time.
data/lib/periodically.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
4
+
3
5
  require "periodically/job"
4
6
  require "periodically/debug"
5
7
  require "periodically/defer"
@@ -9,8 +11,11 @@ require "periodically/model"
9
11
  module Periodically
10
12
  @@registered = []
11
13
 
14
+ def self.logger=(new_logger)
15
+ @logger ||= new_logger
16
+ end
12
17
  def self.logger
13
- Rails.logger # TODO
18
+ @logger ||= Logger.new(STDOUT)
14
19
  end
15
20
 
16
21
  REDIS_DEFAULTS = {
@@ -18,13 +23,13 @@ module Periodically
18
23
  }
19
24
 
20
25
  def self.execute_next
21
- job, instance = @@registered.lazy.filter_map do |job|
26
+ found_job, instance = @@registered.lazy.filter_map do |job|
22
27
  instance = job.poll_next_instance
23
28
  [job, instance] if instance
24
29
  end.first
25
30
 
26
- if job && instance
27
- job.execute_instance(instance)
31
+ if found_job && instance
32
+ found_job.execute_instance(instance)
28
33
  true
29
34
  else
30
35
  false
@@ -8,17 +8,32 @@ module Periodically
8
8
  values = conn.mget(keys)
9
9
  Hash[keys.zip(values)]
10
10
  end
11
- locks = Periodically.redis do |conn|
11
+ error_messages = Periodically.redis do |conn|
12
+ keys = conn.scan_each(:match => "errormessages:*").to_a
13
+ values = conn.mget(keys)
14
+ Hash[keys.zip(values)]
15
+ end
16
+ locks = lock_ttls
17
+ {
18
+ job_count: Periodically._registered_jobs.size,
19
+ error_counts: error_counts,
20
+ error_messages: error_messages,
21
+ lock_ttls: locks
22
+ }
23
+ end
24
+
25
+ def self.lock_ttls
26
+ Periodically.redis do |conn|
12
27
  keys = conn.scan_each(:match => "locks:*").to_a
13
28
  ttls = conn.eval(%{
14
29
  local matcher = KEYS[1] .. ":*"
15
30
  local ttls = {}
16
-
31
+
17
32
  local cursor = "0"
18
33
  repeat
19
34
  local result = redis.call("SCAN", cursor, "MATCH", matcher)
20
35
  cursor = result[1]
21
-
36
+
22
37
  local keys = result[2]
23
38
  for i, key in ipairs(keys) do
24
39
  ttls[#ttls + 1] = redis.call('ttl', key)
@@ -28,7 +43,6 @@ module Periodically
28
43
  }, :keys => ['locks'])
29
44
  Hash[keys.zip(ttls)]
30
45
  end
31
- { job_count: Periodically._registered_jobs.size, error_counts: error_counts, lock_ttls: locks }
32
46
  end
33
47
  end
34
48
  end
@@ -15,11 +15,9 @@ module Periodically
15
15
  def poll_next_instance
16
16
  return if job_or_class_locked?
17
17
 
18
- ActiveRecord::Base.uncached do
19
- where = @opts[:on].call()
20
- where.to_a.find do |obj|
21
- !instance_locked?(obj)
22
- end
18
+ where = @opts[:on].call()
19
+ where.to_a.find do |obj|
20
+ !instance_locked?(obj)
23
21
  end
24
22
  end
25
23
 
@@ -28,9 +26,11 @@ module Periodically
28
26
  begin
29
27
  instance.send(@method)
30
28
  rescue => e
31
- Periodically.logger.error("Job instance[#{instance}] execution raised an exception\n#{e.message}\n#{e.backtrace.join("\n")}")
29
+ stored_error = "#{e.message}\n#{e.backtrace.join("\n")}"
30
+ Periodically.logger.error("Job instance[#{instance}] execution raised an exception\n#{stored_error}")
32
31
  new_error_count = increase_instance_error_count(instance)
33
32
  lock_instance(instance, DEFAULT_ERROR_DELAY.call(new_error_count))
33
+ store_instance_error(instance, stored_error)
34
34
  return
35
35
  end
36
36
 
@@ -61,6 +61,11 @@ module Periodically
61
61
  Periodically.redis {|conn| conn.del(error_count_key)}
62
62
  end
63
63
 
64
+ def store_instance_error(instance, error)
65
+ error_message_key = "errormessages:#{instance_key(instance)}"
66
+ Periodically.redis {|conn| conn.set(error_message_key, error)}
67
+ end
68
+
64
69
  def job_or_class_locked?
65
70
  Periodically.redis do |conn|
66
71
  # TODO can this be optimized with exists?(Array)
@@ -35,7 +35,8 @@ module Periodically
35
35
  def build_client(options)
36
36
  namespace = options[:namespace]
37
37
 
38
- client = Redis.new client_opts(options)
38
+ client = $PERIODICALLY_MOCK_REDIS ? MockRedis.new : Redis.new(client_opts(options))
39
+
39
40
  if namespace
40
41
  begin
41
42
  require "redis-namespace"
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.5"
7
+ gem.version = "0.0.6"
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.5
4
+ version: 0.0.6
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-07 00:00:00.000000000 Z
11
+ date: 2020-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -59,6 +59,7 @@ extensions: []
59
59
  extra_rdoc_files: []
60
60
  files:
61
61
  - ".editorconfig"
62
+ - ".github/workflows/ci.yml"
62
63
  - Gemfile
63
64
  - Gemfile.lock
64
65
  - README.md