cron-table 0.2 → 0.3

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: ae408479bf5161e8a60f21fd10758b7bc9790f2e47eb13517127f78ec09d9174
4
- data.tar.gz: 70787c17ee8e536bb73c8226162765e5c7bc6a3b888cc6c3686df83f676004cc
3
+ metadata.gz: 5d3679f2b53e9c33edb30d8eaf43c20fbacb038113f1b5e86c1d495f3a312429
4
+ data.tar.gz: fffbfde8b3008a9c29de5de2466098f3e5e6116c0b54d9527d1f30784a977da9
5
5
  SHA512:
6
- metadata.gz: 82b82aa4118fefa72c3896bb3850e281b8987205ecee857a5266c6674c81613ada788f3090b696e15a15761641cc2f471e7c96bafe4d9547fe5d722d8d58e497
7
- data.tar.gz: fe33d54efd7eb2036125202dada55394624a81c17d461d512cda2693cedd4c9f943228dbae0b783ac959b474fec9bfc0298d449df9d7e2d66d99581ac0c86e86
6
+ metadata.gz: ba77e1aadaf57e1035407aba41c9e3ed1d88358d047a04fe2fe9536b907cae1a37dc105560cfe41aa0c6cbb5b32a11e672f34633a23d741428a4d24b52143114
7
+ data.tar.gz: a36b8ec47e3a1fd8b3fed8b20282ffa522b5b0feb3f287456bac9796a507822b19ee9e43f68d79ccf56573e05ecbe9adc730bfd87a3571954b574532f9f5e60d
data/README.md CHANGED
@@ -9,7 +9,31 @@ Basic cron-like system to schedule jobs
9
9
  ## Usage
10
10
  1. Add `include CronTable::Schedule` to the job
11
11
  2. Define cron schedule using `crontable(every: <interval>)`
12
- 3. Use block if cron requires params, eg `crontable(every: 1.day) { perform_later(Time.now) }`
12
+ 3. Use block if cron requires params, eg `crontable(every: 1.day) { perform_later(Time.current) }`
13
+
14
+ ## Interval specification
15
+ Use `every` to specify when cron should run. Allowed values include:
16
+ 1. Positive `ActiveSupport::Duration` like `1.hour`, `2.days`
17
+ 2. Predefined intervals
18
+ - `midnight`
19
+ - `noon`
20
+ - `beginning_of_hour`
21
+ 3. Custom interval registered in `CronTable.every`, eg
22
+ ```
23
+ # config/initializers/cron_table.rb
24
+ CronTable.every[:custom] = ->(context) { rand(1.hour..1.day).from_now }
25
+ ```
26
+
27
+ ## Middlewares
28
+ Cron execution can be instrumented using middleware, eg
29
+ ```
30
+ module CronTableInstrumentation
31
+ def process(context)
32
+ ElasticAPM.with_transaction(context.cron.key, "cron") { super }
33
+ end
34
+ end
35
+ CronTable.register(CronTableInstrumentation)
36
+ ```
13
37
 
14
38
  ## License
15
39
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,67 +1,84 @@
1
- class CronTable::Server
2
- SLEEP = 1.second..1.day
3
- SLEEP_IDLE = 1.hour
1
+ module CronTable
2
+ class Server
3
+ SLEEP = 1.second..1.day
4
+ SLEEP_IDLE = 1.hour
4
5
 
5
- def initialize
6
- end
6
+ def initialize
7
+ @middlewares = Middlewares.new
8
+ end
7
9
 
8
- def sync!
9
- crons = CronTable.all.clone
10
- deleted = []
11
- CronTable::Item.transaction do
12
- CronTable::Item.lock.all.each do |cron|
13
- if definition = crons.delete(cron.key)
14
- cron.update(next_run_at: definition.next_run_at(Time.now)) if cron.next_run_at.nil?
15
- elsif cron.next_run_at.present?
16
- deleted << cron.key
10
+ def sync!
11
+ crons = CronTable.all.clone
12
+ deleted = []
13
+ Item.transaction do
14
+ Item.lock.all.each do |cron|
15
+ if definition = crons.delete(cron.key)
16
+ context = Context.new(last_run_at: nil)
17
+ cron.update(next_run_at: definition.next_run_at(context)) if cron.next_run_at.nil?
18
+ elsif cron.next_run_at.present?
19
+ deleted << cron.key
20
+ end
21
+ end
22
+ Item.where(key: deleted).update(next_run_at: nil)
23
+ crons.each do |key, definition|
24
+ context = Context.new(last_run_at: nil)
25
+ Item.create(key: key, next_run_at: definition.next_run_at(context))
17
26
  end
18
- end
19
- CronTable::Item.where(key: deleted).update(next_run_at: nil)
20
- crons.each do |key, definition|
21
- CronTable::Item.create(key: key, next_run_at: definition.next_run_at(Time.now))
22
27
  end
23
28
  end
24
- end
25
29
 
26
- def run!
27
- @exit = false
28
- next_run_at = CronTable::Item.minimum(:next_run_at) || SLEEP_IDLE.from_now
29
- interruptible_sleep(next_run_at.to_f - Time.now.to_f)
30
+ def run!
31
+ Item.connection_pool.with_connection do
32
+ sync!
33
+ end
30
34
 
31
- CronTable::Item.lock.where(next_run_at: ..Time.now).each do |cron|
32
- process(cron)
35
+ @exit = false
36
+
37
+ run_once! until exit?
33
38
  end
34
- end
35
39
 
36
- def exit!
37
- @exit = true
38
- interrupt!
39
- end
40
+ def exit!
41
+ @exit = true
42
+ interrupt!
43
+ end
40
44
 
41
- def exit?
42
- @exit.present?
43
- end
45
+ def exit?
46
+ @exit.present?
47
+ end
44
48
 
45
- private
49
+ private
46
50
 
47
- def process(cron)
48
- Rails.application.reloader.wrap do
49
- definition = CronTable.all.fetch(cron.key)
50
- definition.call
51
+ def run_once!
52
+ next_run_at = Item.connection_pool.with_connection do
53
+ Item.lock.where(next_run_at: ..Time.now).each do |cron|
54
+ process(cron)
55
+ end
56
+ Item.minimum(:next_run_at) || SLEEP_IDLE.from_now
57
+ end
58
+ interruptible_sleep(next_run_at.to_f - Time.now.to_f)
59
+ end
51
60
 
52
- cron.update(last_run_at: Time.now, next_run_at: definition.next_run_at(Time.now))
61
+ def process(cron)
62
+ Rails.application.reloader.wrap do
63
+ definition = CronTable.all.fetch(cron.key)
64
+ context = Context.new(last_run_at: cron.next_run_at, cron: definition)
65
+ @middlewares.process(context) do
66
+ definition.call
67
+ end
68
+ cron.update(last_run_at: Time.now, next_run_at: definition.next_run_at(context))
69
+ end
70
+ rescue => e
71
+ cron.update(next_run_at: nil)
72
+ Rails.error.report(e, handled: true, severity: :error, context: { cron: cron.id })
53
73
  end
54
- rescue => e
55
- cron.update(next_run_at: nil)
56
- Rails.error.report(e, handled: true, severity: :error, context: { cron: cron.id })
57
- end
58
74
 
59
- def interruptible_sleep(seconds)
60
- @sleep, @interrupt = IO.pipe
61
- IO.select([@sleep], nil, nil, seconds.to_i.clamp(SLEEP))
62
- end
75
+ def interruptible_sleep(seconds)
76
+ @sleep, @interrupt = IO.pipe
77
+ IO.select([@sleep], nil, nil, seconds.to_i.clamp(SLEEP))
78
+ end
63
79
 
64
- def interrupt!
65
- @interrupt&.close
80
+ def interrupt!
81
+ @interrupt&.close
82
+ end
66
83
  end
67
84
  end
@@ -1,7 +1,7 @@
1
1
  class CreateCronTable < ActiveRecord::Migration[7.0]
2
2
  def change
3
3
  create_table :cron_table do |t|
4
- t.string :key, null: false, unique: true
4
+ t.string :key, null: false, index: { unique: true }
5
5
 
6
6
  t.datetime :next_run_at, index: true
7
7
  t.datetime :last_run_at
@@ -0,0 +1,4 @@
1
+ module CronTable
2
+ class Context < Struct.new(:last_run_at, :cron, keyword_init: true)
3
+ end
4
+ end
@@ -2,8 +2,13 @@ module CronTable
2
2
  class Definition < Struct.new(:key, :every, :block, keyword_init: true)
3
3
  delegate :call, to: :block
4
4
 
5
- def next_run_at(now)
6
- now + every
5
+ def next_run_at(context)
6
+ case every
7
+ when ActiveSupport::Duration
8
+ (context.last_run_at || Time.current) + every
9
+ when Symbol
10
+ CronTable.every.fetch(every).call(context)
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -2,11 +2,21 @@ module CronTable
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace CronTable
4
4
 
5
+ config.before_initialize do
6
+ CronTable.every[:midnight] = ->(context) {
7
+ (context.last_run_at || Time.current).since(1.day).midnight
8
+ }
9
+ CronTable.every[:noon] = ->(context) {
10
+ (context.last_run_at&.since(1.day) || 12.hours.from_now).noon
11
+ }
12
+ CronTable.every[:beginning_of_hour] = ->(context) {
13
+ (context.last_run_at || Time.current).since(1.hour).beginning_of_hour
14
+ }
15
+ end
16
+
5
17
  server do
6
18
  Thread.new do
7
- cron = CronTable::Server.new
8
- cron.sync!
9
- cron.run until true
19
+ CronTable::Server.new.run!
10
20
  end if CronTable.attach_to_server
11
21
  end
12
22
  end
@@ -0,0 +1,10 @@
1
+ module CronTable
2
+ class BaseMiddleware
3
+ def process(context)
4
+ yield
5
+ end
6
+ end
7
+
8
+ class Middlewares < BaseMiddleware
9
+ end
10
+ end
@@ -14,6 +14,14 @@ module CronTable
14
14
  def message = "Provide a block to `crontable(..) { }` with code to execute"
15
15
  end
16
16
 
17
+ class InvalidEvery < ArgumentError
18
+ def initialize(every)
19
+ @every = every
20
+ end
21
+
22
+ def message = "Invalid every: `#{@every.inspect}`"
23
+ end
24
+
17
25
  extend ActiveSupport::Concern
18
26
 
19
27
  included do |_base|
@@ -27,6 +35,13 @@ module CronTable
27
35
  block ||= -> { self.perform_later } if self.respond_to?(:perform_later)
28
36
  raise MissingBlockError if block.nil?
29
37
 
38
+ case every
39
+ when ActiveSupport::Duration
40
+ when *CronTable.every.keys
41
+ else
42
+ raise InvalidEvery.new(every)
43
+ end
44
+
30
45
  CronTable.all[key] = Definition.new(key:, every:, block:)
31
46
 
32
47
  self
@@ -1,3 +1,3 @@
1
1
  module CronTable
2
- VERSION = "0.2"
2
+ VERSION = "0.3"
3
3
  end
data/lib/cron-table.rb CHANGED
@@ -2,12 +2,16 @@ require "cron-table/version"
2
2
  require "cron-table/engine"
3
3
  require "cron-table/definition"
4
4
  require "cron-table/schedule"
5
+ require "cron-table/context"
6
+ require "cron-table/middlewares"
5
7
 
6
8
  module CronTable
7
9
  mattr_accessor :attach_to_server, default: Rails.env.production?
8
10
 
9
11
  mattr_accessor :preload_dirs, default: ["app/jobs"]
10
12
 
13
+ mattr_accessor :every, default: {}
14
+
11
15
  @@all = nil
12
16
  def self.all
13
17
  if @@all.nil?
@@ -20,4 +24,8 @@ module CronTable
20
24
 
21
25
  @@all
22
26
  end
27
+
28
+ def self.register(middleware)
29
+ Middlewares.include(middleware)
30
+ end
23
31
  end
@@ -2,11 +2,10 @@ namespace :cron_table do
2
2
  desc "Run cron_table scheduler"
3
3
  task run: :environment do
4
4
  cron = CronTable::Server.new
5
- cron.sync!
6
5
 
7
6
  Signal.trap("INT") { cron.exit! }
8
7
  Signal.trap("TERM") { cron.exit! }
9
8
 
10
- cron.run! until cron.exit?
9
+ cron.run!
11
10
  end
12
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cron-table
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - twratajczak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-25 00:00:00.000000000 Z
11
+ date: 2023-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.0'
33
+ version: '2.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.0'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rufo
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -70,8 +70,10 @@ files:
70
70
  - config/routes.rb
71
71
  - db/migrate/20230723000000_create_cron_table.rb
72
72
  - lib/cron-table.rb
73
+ - lib/cron-table/context.rb
73
74
  - lib/cron-table/definition.rb
74
75
  - lib/cron-table/engine.rb
76
+ - lib/cron-table/middlewares.rb
75
77
  - lib/cron-table/schedule.rb
76
78
  - lib/cron-table/version.rb
77
79
  - lib/tasks/cron_table_tasks.rake