scheddy 0.2.1 → 0.3.0
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 +4 -4
- data/LICENSE.txt +1 -1
- data/README.md +25 -18
- data/app/models/scheddy/task_scheduler.rb +64 -0
- data/db/migrate/20230607201527_create_scheddy_task_histories.rb +0 -1
- data/db/migrate/20240822165904_create_scheddy_task_schedulers.rb +15 -0
- data/lib/scheddy/cli.rb +8 -0
- data/lib/scheddy/config.rb +4 -11
- data/lib/scheddy/error_handler.rb +16 -0
- data/lib/scheddy/scheduler.rb +163 -20
- data/lib/scheddy/task.rb +13 -6
- data/lib/scheddy/version.rb +1 -1
- data/lib/scheddy.rb +1 -0
- data/lib/tasks/scheddy_tasks.rake +6 -0
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aaf5fc0d82ffab054b047932e5b346d027f3e58ff9c79b1942acf5bf6be38c73
|
4
|
+
data.tar.gz: 6477ad6b6bacb90f76b915146daacd771ec042d70787f4bca6d09e01ffe1f22f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e09bad65eecbb09c235c8410bf0e3c3f4b402ffbaa2f1c09153189ff5fac187b93a8b1524dafa636b4bc70fa69ebadcfc015884cd924f7f5cc3cdfa8c5d1ffca
|
7
|
+
data.tar.gz: b1075009c017fc0b13fc5399c81f6d2cea4f3f09295de94d781fcb06623e9962e2fc9b08701c566dbe34cacaf601cc48aec444da5fddf4edb760610b5af056a7
|
data/LICENSE.txt
CHANGED
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
|
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
|
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.
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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!
|
data/lib/scheddy/config.rb
CHANGED
@@ -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
|
41
|
-
track_runs track if track
|
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
|
52
|
-
track_runs track if track
|
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
|
data/lib/scheddy/scheduler.rb
CHANGED
@@ -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 "[
|
13
|
+
puts "[scheddy] Hello. This is Scheddy v#{VERSION}."
|
14
|
+
puts "[scheddy] hostname=#{hostname}, pid=#{pid}, id=#{scheduler_id}"
|
11
15
|
trap_signals!
|
12
|
-
|
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
|
-
|
16
|
-
|
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 "[
|
22
|
-
|
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 "[
|
44
|
+
$stderr.puts "[scheddy] Killing task #{task.name}"
|
27
45
|
task.kill
|
28
46
|
end
|
29
47
|
end
|
30
48
|
|
31
|
-
|
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.
|
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
|
-
|
55
|
-
|
56
|
-
Scheddy::TaskHistory.
|
57
|
-
|
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 '[
|
143
|
+
puts '[scheddy] Stopping'
|
66
144
|
self.stop = true
|
67
145
|
end
|
68
146
|
|
69
147
|
def trap_signals!
|
70
|
-
trap 'INT',
|
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
|
-
|
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
|
-
|
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
|
data/lib/scheddy/version.rb
CHANGED
data/lib/scheddy.rb
CHANGED
@@ -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.
|
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:
|
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
|
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.
|
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: []
|