scheddy 0.2.1 → 0.3.0

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: 86885e07595c5cc31d8a5963684a2618de1505986399fd93e0f54f6ff248bbac
4
- data.tar.gz: 409a482e3d28dbdda1f2ec48e06ed7a52ff020e961ce89b89aef976698e9e400
3
+ metadata.gz: aaf5fc0d82ffab054b047932e5b346d027f3e58ff9c79b1942acf5bf6be38c73
4
+ data.tar.gz: 6477ad6b6bacb90f76b915146daacd771ec042d70787f4bca6d09e01ffe1f22f
5
5
  SHA512:
6
- metadata.gz: fa861cb109163a3a86e8b327436441ed4f4f4dbee4b95d1ea605f67500b91110465aded2062b00934d1621211625665f735f07d5a9f8c14d0b8d0d9a9800f560
7
- data.tar.gz: 33acabd55cc1ecce55ec9cd0a30e547dad4570d19414382839a02510e6d7543b2d12f5b136aa9951550dbf96a0b7bb7b3085ed974cd5b41e1f157380f642ded1
6
+ metadata.gz: e09bad65eecbb09c235c8410bf0e3c3f4b402ffbaa2f1c09153189ff5fac187b93a8b1524dafa636b4bc70fa69ebadcfc015884cd924f7f5cc3cdfa8c5d1ffca
7
+ data.tar.gz: b1075009c017fc0b13fc5399c81f6d2cea4f3f09295de94d781fcb06623e9962e2fc9b08701c566dbe34cacaf601cc48aec444da5fddf4edb760610b5af056a7
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2023 thomas morgan
1
+ Copyright 2023-2024 thomas morgan
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -7,6 +7,7 @@ Scheddy is a batteries-included task scheduler for Rails. It is intended as a re
7
7
  * Catch up missed tasks. Designed for environments with frequent deploys. Also useful in dev where the scheduler isn't always running.
8
8
  * Job-queue agnostic. Works great with various ActiveJob adapters and non-ActiveJob queues too.
9
9
  * Minimal dependencies. Uses your existing database (or no database at all). Redis not required either.
10
+ * Built-in cluster support by running 2+ Scheddy instances. (Database required in this case.)
10
11
  * Tasks and their schedules are versioned as part of your code.
11
12
 
12
13
 
@@ -18,13 +19,13 @@ Add to your application's `Gemfile`:
18
19
  gem "scheddy"
19
20
  ```
20
21
 
21
- After running `bundle install`, add the migration to your app:
22
+ After running `bundle install`, add the migrations to your app:
22
23
  ```bash
23
24
  bin/rails scheddy:install:migrations
24
25
  bin/rails db:migrate
25
26
  ```
26
27
 
27
- FYI, if all tasks set `track_runs false`, the migration can be skipped.
28
+ FYI, if all tasks set `track_runs false` and running only a single Scheddy instance, the migrations are optional.
28
29
 
29
30
 
30
31
 
@@ -129,7 +130,16 @@ Database transactions are valid. These can increase use of database connections
129
130
 
130
131
  Each task runs in its own thread which helps ensure all tasks perform on time. However, Scheddy is not intended as a job executor and doesn't have a robust mechanism for retrying failed jobs--that belongs to your background job queue.
131
132
 
132
- A given task will only ever be executed once at a time. Mostly relevant when using tiny intervals, if a prior execution is still going when the next execution is scheduled, Scheddy will skip the next execution and log an error message to that effect.
133
+ A given task will only ever be executed once at a time. Mostly relevant when using tiny intervals, if a prior execution is still going when the next execution is scheduled to start, Scheddy will skip the next execution and log an error message to that effect.
134
+
135
+
136
+ #### Running 2+ Scheddy instances in a cluster
137
+
138
+ Running multiple Scheddy instances will automatically form a cluster with one leader and however many standbys. Tasks are only executed by the leader.
139
+
140
+ Clean and unclean handoffs are both supported, taking 1-2 and 4-5 minutes respectively. After a handoff, task executions will be caught up by the new leader, same as if a single instance had been stopped and restarted.
141
+
142
+ Note that the prior section's promise that a task will only be executed once at a time cannot be guaranteed in the case of a handoff between Scheddy instances. Since tasks are intended to run very quickly, and long tasks should run via ActiveJob instead, this should not be an issue in practice.
133
143
 
134
144
 
135
145
  #### Task context
@@ -193,24 +203,21 @@ You can also check your tasks configuration with:
193
203
  bundle exec scheddy tasks
194
204
  ```
195
205
 
206
+ When running 2+ Scheddy's, a handoff may be initiated with:
207
+ ```bash
208
+ scheddy stepdown
209
+ # OR
210
+ bundle exec scheddy stepdown
211
+ ```
212
+
196
213
 
197
214
  ### In production
198
215
 
199
- Scheddy runs as its own process. It is intended to be run only once. Because Scheddy has the ability to catch up missed tasks, redundancy should be achieved through automatic restarts via `systemd`, `dockerd`, Kubernetes, or whatever supervisory system you use.
200
-
201
- During deployment, shutdown the old instance before starting the new one. In Kubernetes this might look like:
202
- ```yaml
203
- kind: Deployment
204
- spec:
205
- replicas: 1
206
- strategy:
207
- rollingUpdate:
208
- maxSurge: 0
209
- maxUnavailable: 1
210
- template:
211
- spec:
212
- terminationGracePeriodSeconds: 60
213
- ```
216
+ Scheddy runs as its own process. During app deployments, it is recommended to shutdown the old Scheddy instance before starting the new one.
217
+
218
+ Since Scheddy has the ability to catch up missed tasks, it is often viable to run just a single Scheddy instance, especially when that instance is automatically restarted by `systemd`, `dockerd`, Kubernetes, or whatever supervisory system you use.
219
+
220
+ It is also possible to run 2+ Scheddy instances as a cluster. One instance will be selected as the leader and execute tasks. The other instance(s) will operate as standbys, ready to take over if the leader steps down or fails.
214
221
 
215
222
 
216
223
  ### In development (and `Procfile` in production)
@@ -0,0 +1,64 @@
1
+ module Scheddy
2
+ class TaskScheduler < ApplicationRecord
3
+
4
+ validates :leader_expires_at,
5
+ presence: {if: :leader_state},
6
+ absence: {unless: :leader_state}
7
+
8
+ validates :leader_state,
9
+ inclusion: [nil, 'leader']
10
+
11
+
12
+ scope :leader, ->{ where(leader_state: 'leader') }
13
+ scope :not_leader, ->{ where.not(leader_state: 'leader') }
14
+ scope :stale, ->{ where(last_seen_at: ..2.hours.ago).not_leader }
15
+
16
+
17
+ def expired?
18
+ leader_expires_at && leader_expires_at < Time.current
19
+ end
20
+
21
+ def leader?
22
+ leader_state == 'leader'
23
+ end
24
+
25
+
26
+ def clear_leader(only_if_expired: false)
27
+ reload if changed?
28
+ with_lock do
29
+ if !only_if_expired || expired?
30
+ update! leader_state: nil, leader_expires_at: nil
31
+ end
32
+ end
33
+ end
34
+
35
+ def mark_seen
36
+ if last_seen_at < (LEASE_RENEWAL_INTERVAL - 5.seconds).ago
37
+ update! last_seen_at: Time.current
38
+ end
39
+ end
40
+
41
+ def renew_leadership
42
+ if last_seen_at < (LEASE_RENEWAL_INTERVAL - 5.seconds).ago
43
+ update! leader_expires_at: LEASE_DURATION.from_now, last_seen_at: Time.current
44
+ else
45
+ true
46
+ end
47
+ rescue ActiveRecord::StaleObjectError
48
+ reload
49
+ false
50
+ end
51
+
52
+ def take_leadership
53
+ update! leader_state: 'leader', leader_expires_at: LEASE_DURATION.from_now, last_seen_at: Time.current
54
+ rescue ActiveRecord::RecordNotUnique
55
+ false
56
+ end
57
+
58
+ def request_stepdown
59
+ # intentionally leaves self.lock_version behind
60
+ self.class.increment_counter :lock_version, id, touch: true if leader?
61
+ end
62
+
63
+ end
64
+ end
@@ -1,6 +1,5 @@
1
1
  class CreateScheddyTaskHistories < ActiveRecord::Migration[6.0]
2
2
  def change
3
- # feel free to modify to id: :uuid or another :id format if you prefer
4
3
  create_table :scheddy_task_histories do |t|
5
4
  t.string :name, null: false, index: {unique: true}
6
5
  t.datetime :last_run_at
@@ -0,0 +1,15 @@
1
+ class CreateScheddyTaskSchedulers < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :scheddy_task_schedulers, id: :string do |t|
4
+ t.string :hostname, null: false
5
+ t.datetime :last_seen_at, null: false
6
+ t.datetime :leader_expires_at
7
+ t.string :leader_state
8
+ t.integer :lock_version, null: false, default: 0
9
+ t.integer :pid, null: false
10
+ t.timestamps
11
+
12
+ t.index :leader_state, unique: true
13
+ end
14
+ end
15
+ end
data/lib/scheddy/cli.rb CHANGED
@@ -17,6 +17,14 @@ module Scheddy
17
17
  end
18
18
 
19
19
 
20
+ desc :stepdown, 'Ask current Scheddy leader to step down'
21
+ def stepdown
22
+ load_app!
23
+ puts 'Requesting step down...'
24
+ Scheddy::TaskScheduler.leader.first&.request_stepdown
25
+ end
26
+
27
+
20
28
  desc :tasks, 'Show configured tasks'
21
29
  def tasks
22
30
  load_app!
@@ -2,13 +2,6 @@ module Scheddy
2
2
  # default task list for when running standalone
3
3
  mattr_accessor :tasks, default: []
4
4
 
5
- # called from within task's execution thread; must be multi-thread safe
6
- # task is allowed to be nil
7
- mattr_accessor :error_handler, default: lambda {|e, task|
8
- logger.error "Exception in Scheddy task '#{task&.name}': #{e.inspect}\n #{e.backtrace.join("\n ")}"
9
- Rails.error.report(e, handled: true, severity: :error)
10
- }
11
-
12
5
  def self.config(&block)
13
6
  Config.new(tasks, &block)
14
7
  end
@@ -37,8 +30,8 @@ module Scheddy
37
30
  def run_at(cron, name:, tag: :auto, track: :auto, &task)
38
31
  task(name) do
39
32
  run_at cron
40
- logger_tag tag if tag!=:auto
41
- track_runs track if track!=:auto
33
+ logger_tag tag if tag != :auto
34
+ track_runs track if track != :auto
42
35
  perform(&task)
43
36
  end
44
37
  end
@@ -48,8 +41,8 @@ module Scheddy
48
41
  task(name) do
49
42
  run_every interval
50
43
  initial_delay delay if delay
51
- logger_tag tag if tag!=:auto
52
- track_runs track if track!=:auto
44
+ logger_tag tag if tag != :auto
45
+ track_runs track if track != :auto
53
46
  perform(&task)
54
47
  end
55
48
  end
@@ -0,0 +1,16 @@
1
+ module Scheddy
2
+ # called from within task's execution thread; must be multi-thread safe
3
+ # task is allowed to be nil
4
+ mattr_accessor :error_handler, default: lambda {|e, task|
5
+ task &&= "task '#{task&.name}' "
6
+ logger.error "Exception in Scheddy #{task}: #{e.inspect}\n #{e.backtrace.join("\n ")}"
7
+ Rails.error.report(e, handled: true, severity: :error)
8
+ }
9
+
10
+ def self.handle_error(e, task=nil)
11
+ if h = Scheddy.error_handler
12
+ h.call(*[e, task].take(h.arity.abs))
13
+ end
14
+ end
15
+
16
+ end
@@ -1,4 +1,7 @@
1
1
  module Scheddy
2
+ LEASE_RENEWAL_INTERVAL = 1.minute
3
+ LEASE_DURATION = 4.minutes
4
+ # must be > 2x the renewal interval
2
5
 
3
6
  def self.run
4
7
  Scheduler.new(tasks).run
@@ -7,69 +10,149 @@ module Scheddy
7
10
  class Scheduler
8
11
 
9
12
  def run
10
- puts "[Scheddy] Starting scheduler with #{tasks.size} #{'task'.pluralize tasks.size}"
13
+ puts "[scheddy] Hello. This is Scheddy v#{VERSION}."
14
+ puts "[scheddy] hostname=#{hostname}, pid=#{pid}, id=#{scheduler_id}"
11
15
  trap_signals!
12
- cleanup_task_history
16
+ puts "[scheddy] Starting scheduler with #{tasks.size} #{'task'.pluralize tasks.size}"
17
+ unless register_process
18
+ puts '[scheddy] No scheddy_task_schedulers table found; disabling cluster support'
19
+ end
13
20
 
14
21
  until stop?
15
- next_cycle = run_once
16
- wait_until next_cycle unless stop?
22
+ with_leader do |new_leader|
23
+ reset_tasks if new_leader
24
+ cleanup_task_history
25
+ cleanup_task_scheduler
26
+
27
+ next_cycle = run_once
28
+ if tasks.any? && scheduler_record
29
+ next_cycle = [next_cycle, LEASE_RENEWAL_INTERVAL.from_now].compact.min
30
+ end
31
+ wait_until next_cycle unless stop?
32
+ end
17
33
  end
18
34
 
35
+ stepdown_as_leader
36
+
19
37
  running = tasks.select(&:running?).count
20
38
  if running > 0
21
- puts "[Scheddy] Waiting for #{running} tasks to complete"
22
- wait_until(45.seconds.from_now, skip_stop: true) do
39
+ puts "[scheddy] Waiting for #{running} tasks to complete"
40
+ wait_for(45.seconds, skip_stop: true) do
23
41
  tasks.none?(&:running?)
24
42
  end
25
43
  tasks.select(&:running?).each do |task|
26
- $stderr.puts "[Scheddy] Killing task #{task.name}"
44
+ $stderr.puts "[scheddy] Killing task #{task.name}"
27
45
  task.kill
28
46
  end
29
47
  end
30
48
 
31
- puts '[Scheddy] Done'
49
+ ensure
50
+ unregister_process
51
+ puts '[scheddy] Goodbye'
32
52
  end
33
53
 
34
54
  # return : Time of next cycle
35
55
  def run_once
36
- tasks.flat_map do |task|
56
+ if tasks.empty?
57
+ logger.warn 'No tasks found; doing nothing'
58
+ return 1.hour.from_now
59
+ end
60
+ tasks.filter_map do |task|
37
61
  task.perform(self) unless stop?
38
62
  end.min
39
63
  end
40
64
 
65
+ def stepdown? ; @stepdown ; end
41
66
  def stop? ; @stop ; end
42
67
 
68
+ def hostname
69
+ @hostname ||= Socket.gethostname.force_encoding(Encoding::UTF_8)
70
+ end
71
+
72
+ def pid
73
+ @pid ||= Process.pid
74
+ end
75
+
76
+ def scheduler_id
77
+ @scheduler_id ||= SecureRandom.alphanumeric 12
78
+ end
79
+
80
+ def logger
81
+ @logger ||= Scheddy.logger.tagged "scheddy-#{scheduler_id}"
82
+ end
83
+
43
84
 
44
85
  private
45
86
 
46
- attr_reader :tasks
47
- attr_writer :stop
87
+ attr_reader :scheduler_record, :tasks
88
+ attr_writer :stepdown, :stop
89
+ attr_accessor :leader_state
48
90
 
49
91
  def initialize(tasks)
50
92
  @tasks = tasks
93
+ self.leader_state = :standby
51
94
  end
52
95
 
53
96
  def cleanup_task_history
54
- known_tasks = tasks.select(&:track_runs).map(&:name)
55
- return if known_tasks.empty? # table doesn't have to exist if track_runs always disabled
56
- Scheddy::TaskHistory.find_each do |r|
57
- r.destroy if known_tasks.exclude? r.name
97
+ return if @cleaned_tasks
98
+ @cleaned_tasks = true
99
+ return unless Scheddy::TaskHistory.table_exists?
100
+ trackable_tasks = tasks.select(&:track_runs).map(&:name)
101
+ return if trackable_tasks.empty?
102
+ Scheddy::TaskHistory.where(updated_at: ..1.day.ago).where.not(name: trackable_tasks).find_each do |r|
103
+ r.destroy
104
+ end
105
+ end
106
+
107
+ def cleanup_task_scheduler
108
+ return if @cleaned_schedulers
109
+ @cleaned_schedulers = true
110
+ return unless Scheddy::TaskScheduler.table_exists?
111
+ Scheddy::TaskScheduler.stale.find_each do |r|
112
+ logger.debug "Removing stale scheduler record for id=#{r.id}"
113
+ r.destroy
114
+ end
115
+ end
116
+
117
+ def register_process
118
+ return false unless Scheddy::TaskScheduler.table_exists?
119
+ @scheduler_record ||= Scheddy::TaskScheduler.create!(
120
+ id: scheduler_id,
121
+ hostname: hostname,
122
+ last_seen_at: Time.current,
123
+ pid: pid
124
+ )
125
+ end
126
+
127
+ def unregister_process
128
+ Scheddy::TaskScheduler.delete scheduler_record.id if scheduler_record
129
+ end
130
+
131
+ def reset_tasks
132
+ tasks.each(&:reset)
133
+ end
134
+
135
+ def stepdown!(sig=nil)
136
+ if scheduler_record&.leader?
137
+ puts '[scheddy] Requesting step down'
138
+ self.stepdown = true
58
139
  end
59
- rescue ActiveRecord::StatementInvalid => e
60
- return if e.message =~ /relation "scheddy_task_histories" does not exist/
61
- raise
62
140
  end
63
141
 
64
142
  def stop!(sig=nil)
65
- puts '[Scheddy] Stopping'
143
+ puts '[scheddy] Stopping'
66
144
  self.stop = true
67
145
  end
68
146
 
69
147
  def trap_signals!
70
- trap 'INT', &method(:stop!)
148
+ trap 'INT', &method(:stop!)
71
149
  trap 'QUIT', &method(:stop!)
72
150
  trap 'TERM', &method(:stop!)
151
+ trap 'USR1', &method(:stepdown!)
152
+ end
153
+
154
+ def wait_for(duration, skip_stop: false, &block)
155
+ wait_until duration.from_now, skip_stop:, &block
73
156
  end
74
157
 
75
158
  # &block - optional block - return truthy to end prematurely
@@ -81,5 +164,65 @@ module Scheddy
81
164
  end
82
165
  end
83
166
 
167
+ def with_leader
168
+ return yield(false) if !scheduler_record || tasks.empty?
169
+
170
+ wait_t = LEASE_RENEWAL_INTERVAL.from_now
171
+ if leader_state == :standby
172
+ if current_leader = Scheddy::TaskScheduler.leader.first
173
+ if current_leader.expired?
174
+ logger.error "Forcefully clearing expired leader status for id=#{current_leader.id}"
175
+ current_leader.clear_leader(only_if_expired: true)
176
+ wait_t = 5.seconds.from_now
177
+ end
178
+ else
179
+ if scheduler_record.take_leadership
180
+ self.leader_state = :new_leader
181
+ end
182
+ end
183
+ end
184
+
185
+ case leader_state
186
+ when :new_leader
187
+ logger.info 'We are now cluster leader'
188
+ self.leader_state = :existing_leader
189
+ return yield true
190
+ when :existing_leader
191
+ if !stepdown? && scheduler_record.renew_leadership
192
+ return yield false
193
+ else
194
+ if stepdown_as_leader
195
+ scheduler_record.mark_seen
196
+ end
197
+ wait_t += 5.seconds # improve odds of another daemon taking over
198
+ end
199
+ when :standby
200
+ scheduler_record.mark_seen
201
+ end
202
+
203
+ wait_until wait_t
204
+ rescue Exception => e
205
+ logger.error 'Error in scheduler; retrying in 1 minute'
206
+ Scheddy.handle_error(e)
207
+ if leader_state != :standby
208
+ logger.warn 'Due to prior error, stepping down as leader'
209
+ stepdown_as_leader skip_msg: true
210
+ end
211
+ wait_for 1.minute
212
+ end
213
+
214
+ def stepdown_as_leader(skip_msg: false)
215
+ return true if leader_state == :standby
216
+ logger.info 'Stepping down as leader' unless skip_msg
217
+ scheduler_record.clear_leader
218
+ self.stepdown = false
219
+ self.leader_state = :standby
220
+ true
221
+ rescue Exception => e
222
+ logger.error 'Failed to step down as leader'
223
+ Scheddy.handle_error(e)
224
+ false
225
+ end
226
+
84
227
  end
85
228
  end
data/lib/scheddy/task.rb CHANGED
@@ -23,9 +23,7 @@ module Scheddy
23
23
  task.call(*[context].take(task.arity.abs))
24
24
  end
25
25
  rescue Exception => e
26
- if h = Scheddy.error_handler
27
- h.call(*[e, self].take(h.arity.abs))
28
- end
26
+ Scheddy.handle_error(e, self)
29
27
  end
30
28
  end
31
29
  ensure
@@ -34,9 +32,7 @@ module Scheddy
34
32
  next_cycle!
35
33
  rescue Exception => e
36
34
  logger.error "Scheddy: error scheduling task '#{name}'; retrying in 5 seconds"
37
- if h = Scheddy.error_handler
38
- h.call(*[e, self].take(h.arity.abs))
39
- end
35
+ Scheddy.handle_error(e, self)
40
36
  return self.next_cycle = 5.seconds.from_now
41
37
  end
42
38
 
@@ -53,6 +49,11 @@ module Scheddy
53
49
  @next_cycle
54
50
  end
55
51
 
52
+ def reset
53
+ self.next_cycle = :initial
54
+ @task_history = nil
55
+ end
56
+
56
57
 
57
58
  def to_h
58
59
  attrs = {
@@ -169,6 +170,12 @@ module Scheddy
169
170
 
170
171
  def record_this_run
171
172
  return unless track_runs
173
+ begin
174
+ Scheddy::TaskHistory.connection.verify!
175
+ rescue => e
176
+ logger.error "Database offline while updating task history for Scheddy task '#{name}': #{e.inspect}"
177
+ return
178
+ end
172
179
  Scheddy::TaskHistory.logger.silence(Logger::INFO) do
173
180
  task_history.update last_run_at: Time.current
174
181
  end
@@ -1,3 +1,3 @@
1
1
  module Scheddy
2
- VERSION = "0.2.1"
2
+ VERSION = '0.3.0'
3
3
  end
data/lib/scheddy.rb CHANGED
@@ -6,6 +6,7 @@ end
6
6
  %w(
7
7
  config
8
8
  context
9
+ error_handler
9
10
  logger
10
11
  scheduler
11
12
  task
@@ -5,6 +5,12 @@ namespace :scheddy do
5
5
  Scheddy.run
6
6
  end
7
7
 
8
+ desc 'Ask current Scheddy leader to step down'
9
+ task stepdown: :environment do
10
+ puts 'Requesting step down...'
11
+ Scheddy::TaskScheduler.leader.first&.request_stepdown
12
+ end
13
+
8
14
  task :migrate do
9
15
  `bin/rails db:migrate SCOPE=scheddy`
10
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scheddy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thomas morgan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-09 00:00:00.000000000 Z
11
+ date: 2024-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fugit
@@ -54,7 +54,8 @@ dependencies:
54
54
  version: '1.0'
55
55
  description: Scheddy is a batteries-included task scheduler for Rails. It is intended
56
56
  as a replacement for cron and cron-like functionality (including job queue specific
57
- schedulers). It is job-queue agnostic and can catch up missed tasks.
57
+ schedulers). It is job-queue agnostic, can catch up missed tasks, and has native
58
+ clustering.
58
59
  email:
59
60
  - tm@iprog.com
60
61
  executables:
@@ -67,13 +68,16 @@ files:
67
68
  - Rakefile
68
69
  - app/models/scheddy/application_record.rb
69
70
  - app/models/scheddy/task_history.rb
71
+ - app/models/scheddy/task_scheduler.rb
70
72
  - db/migrate/20230607201527_create_scheddy_task_histories.rb
73
+ - db/migrate/20240822165904_create_scheddy_task_schedulers.rb
71
74
  - exe/scheddy
72
75
  - lib/scheddy.rb
73
76
  - lib/scheddy/cli.rb
74
77
  - lib/scheddy/config.rb
75
78
  - lib/scheddy/context.rb
76
79
  - lib/scheddy/engine.rb
80
+ - lib/scheddy/error_handler.rb
77
81
  - lib/scheddy/logger.rb
78
82
  - lib/scheddy/scheduler.rb
79
83
  - lib/scheddy/task.rb
@@ -101,9 +105,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
105
  - !ruby/object:Gem::Version
102
106
  version: '0'
103
107
  requirements: []
104
- rubygems_version: 3.4.10
108
+ rubygems_version: 3.5.11
105
109
  signing_key:
106
110
  specification_version: 4
107
111
  summary: Job-queue agnostic, cron-like task scheduler for Rails apps, with missed
108
- task catch-ups and other features.
112
+ task catch-ups, clustering, and other features.
109
113
  test_files: []