cron-table 0.2 → 0.4

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: ae408479bf5161e8a60f21fd10758b7bc9790f2e47eb13517127f78ec09d9174
4
- data.tar.gz: 70787c17ee8e536bb73c8226162765e5c7bc6a3b888cc6c3686df83f676004cc
3
+ metadata.gz: 571ea2789836b6430cec796c0317d39ffc0e9f7f00b8042b1ddb25dd6933cb7a
4
+ data.tar.gz: 5063f7095bae49b7d4d456cf5b9d7befae1a45cbab6541f0354acd663c8538fa
5
5
  SHA512:
6
- metadata.gz: 82b82aa4118fefa72c3896bb3850e281b8987205ecee857a5266c6674c81613ada788f3090b696e15a15761641cc2f471e7c96bafe4d9547fe5d722d8d58e497
7
- data.tar.gz: fe33d54efd7eb2036125202dada55394624a81c17d461d512cda2693cedd4c9f943228dbae0b783ac959b474fec9bfc0298d449df9d7e2d66d99581ac0c86e86
6
+ metadata.gz: 84fa54045c393a270df6c000b06a86f326290eea6a1d7366c7270f8b73ddade644ed3a4d9ec2c66192f29834df15c6618cb06644a3317299f9a4b3098b500cdc
7
+ data.tar.gz: 0ae48405338a0b0e8ee83c9be85ecd5c51114dd1affb8b51d0ec18fb8e3097ac3c72741d6449c7765e8f3b9c6145d4e1ee7b43e63b5af69f0f999abc96befa57
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.4"
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.4'
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: 2025-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '7.0'
19
+ version: '8.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '7.0'
26
+ version: '8.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: mocha
29
29
  requirement: !ruby/object:Gem::Requirement
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
@@ -97,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
99
  - !ruby/object:Gem::Version
98
100
  version: '0'
99
101
  requirements: []
100
- rubygems_version: 3.3.26
102
+ rubygems_version: 3.5.22
101
103
  signing_key:
102
104
  specification_version: 4
103
105
  summary: Basic cron-like scheduling system for Rails