scheddy 0.2.2 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dd5a13a85dcb1c2a8e22e675c54f635b4599715d0fb0c987bd94ec81be6e9e1
4
- data.tar.gz: 429d55cd2b0a788839606a348cbd64e71089c56d8c7749daea13030d0481be88
3
+ metadata.gz: 5ce0ea805132830bdcffb3fb889a6d84b303d3830b0aa88d55640fabc2d4d814
4
+ data.tar.gz: 133df548d63e8a6e0f4ffb5909ba0cf9e7855ab8777a81b8d402286483067302
5
5
  SHA512:
6
- metadata.gz: 712b103f0708dfb037945aff505a692d158b9847be9a4d1ddea745380ae8eb0105ad15d2787a74dff1f85cb93e6c9bb6d1bfba3dbf62b175f7ee66db709548b8
7
- data.tar.gz: 71d629e47d1a354fd775951cb001da61be779b7698307f2178a9644310989dad762984906ef6703745ce71b948c1c3a2e40e1403d8623c283a4e0921e19b52ea
6
+ metadata.gz: 2c4551a478498dfa568ee319c382bd346bc5a5450410df7fed1917c0dd2893febab9f58d331999d1f96c1c09a0a8df559faee14e520a7fd2d6f8f979a9d19f24
7
+ data.tar.gz: 76960cba79cd32b4525773f1a21a23e90204727404f280afbc13b05ed0859ac9d01d8e5b81faff4235e7c6320b88b418ca6a3454351c9e7f66e4611bb07a7966
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2023 thomas morgan
1
+ Copyright 2023-2025 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)
@@ -254,10 +261,6 @@ end
254
261
 
255
262
 
256
263
 
257
- ## Compatibility
258
- Used in production on Rails 7.0+. Gemspec is set to Rails 6.0+, but such is not well tested.
259
-
260
-
261
264
  ## Contributing
262
265
  Pull requests are welcomed.
263
266
 
@@ -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,75 +1,156 @@
1
1
  module Scheddy
2
-
3
- def self.run
4
- Scheduler.new(tasks).run
5
- end
2
+ LEASE_RENEWAL_INTERVAL = 1.minute
3
+ LEASE_DURATION = 4.minutes
4
+ # must be > 2x the renewal interval
6
5
 
7
6
  class Scheduler
8
7
 
9
8
  def run
10
- puts "[Scheddy] Starting scheduler with #{tasks.size} #{'task'.pluralize tasks.size}"
9
+ puts "[scheddy] Hello. This is Scheddy v#{VERSION}."
10
+ puts "[scheddy] hostname=#{hostname}, pid=#{pid}, id=#{scheduler_id}"
11
11
  trap_signals!
12
- cleanup_task_history
12
+ puts "[scheddy] Starting scheduler with #{tasks.size} #{'task'.pluralize tasks.size}"
13
+ unless register_process
14
+ puts '[scheddy] No scheddy_task_schedulers table found; disabling cluster support'
15
+ end
13
16
 
14
17
  until stop?
15
- next_cycle = run_once
16
- wait_until next_cycle unless stop?
18
+ with_leader do |new_leader|
19
+ reset_tasks if new_leader
20
+ cleanup_task_history
21
+ cleanup_task_scheduler
22
+
23
+ next_cycle = run_once
24
+ if tasks.any? && scheduler_record
25
+ next_cycle = [next_cycle, LEASE_RENEWAL_INTERVAL.from_now].compact.min
26
+ end
27
+ wait_until next_cycle unless stop?
28
+ end
17
29
  end
18
30
 
31
+ stepdown_as_leader
32
+
19
33
  running = tasks.select(&:running?).count
20
34
  if running > 0
21
- puts "[Scheddy] Waiting for #{running} tasks to complete"
22
- wait_until(45.seconds.from_now, skip_stop: true) do
35
+ puts "[scheddy] Waiting for #{running} tasks to complete"
36
+ wait_for(45.seconds, skip_stop: true) do
23
37
  tasks.none?(&:running?)
24
38
  end
25
39
  tasks.select(&:running?).each do |task|
26
- $stderr.puts "[Scheddy] Killing task #{task.name}"
40
+ $stderr.puts "[scheddy] Killing task #{task.name}"
27
41
  task.kill
28
42
  end
29
43
  end
30
44
 
31
- puts '[Scheddy] Done'
45
+ ensure
46
+ unregister_process
47
+ puts '[scheddy] Goodbye'
32
48
  end
33
49
 
34
50
  # return : Time of next cycle
35
51
  def run_once
36
- tasks.flat_map do |task|
52
+ if tasks.empty?
53
+ logger.warn 'No tasks found; doing nothing'
54
+ return 1.hour.from_now
55
+ end
56
+ tasks.filter_map do |task|
37
57
  task.perform(self) unless stop?
38
58
  end.min
39
59
  end
40
60
 
61
+ def stepdown? ; @stepdown ; end
41
62
  def stop? ; @stop ; end
42
63
 
64
+ def hostname
65
+ @hostname ||= Socket.gethostname.force_encoding(Encoding::UTF_8)
66
+ end
67
+
68
+ def pid
69
+ @pid ||= Process.pid
70
+ end
71
+
72
+ def scheduler_id
73
+ @scheduler_id ||= SecureRandom.alphanumeric 12
74
+ end
75
+
76
+ def logger
77
+ @logger ||= Scheddy.logger.tagged "scheddy-#{scheduler_id}"
78
+ end
79
+
43
80
 
44
81
  private
45
82
 
46
- attr_reader :tasks
47
- attr_writer :stop
83
+ attr_reader :scheduler_record, :tasks
84
+ attr_writer :stepdown, :stop
85
+ attr_accessor :leader_state
48
86
 
49
87
  def initialize(tasks)
50
88
  @tasks = tasks
89
+ self.leader_state = :standby
51
90
  end
52
91
 
53
92
  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
93
+ return if @cleaned_tasks
94
+ @cleaned_tasks = true
95
+ return unless Scheddy::TaskHistory.table_exists?
96
+ trackable_tasks = tasks.select(&:track_runs).map(&:name)
97
+ return if trackable_tasks.empty?
98
+ Scheddy::TaskHistory.where(updated_at: ..1.day.ago).where.not(name: trackable_tasks).find_each do |r|
99
+ r.destroy
100
+ end
101
+ end
102
+
103
+ def cleanup_task_scheduler
104
+ return if @cleaned_schedulers
105
+ @cleaned_schedulers = true
106
+ return unless Scheddy::TaskScheduler.table_exists?
107
+ Scheddy::TaskScheduler.stale.find_each do |r|
108
+ logger.debug "Removing stale scheduler record for id=#{r.id}"
109
+ r.destroy
110
+ end
111
+ end
112
+
113
+ def register_process
114
+ return false unless Scheddy::TaskScheduler.table_exists?
115
+ @scheduler_record ||= Scheddy::TaskScheduler.create!(
116
+ id: scheduler_id,
117
+ hostname: hostname,
118
+ last_seen_at: Time.current,
119
+ pid: pid
120
+ )
121
+ end
122
+
123
+ def unregister_process
124
+ Scheddy::TaskScheduler.delete scheduler_record.id if scheduler_record
125
+ end
126
+
127
+ def reset_tasks
128
+ tasks.each(&:reset)
129
+ end
130
+
131
+ def stepdown!(sig=nil)
132
+ if scheduler_record&.leader?
133
+ puts '[scheddy] Requesting step down'
134
+ # trap handlers cannot use Logger
135
+ self.stepdown = true
58
136
  end
59
- rescue ActiveRecord::StatementInvalid => e
60
- return if e.message =~ /relation "scheddy_task_histories" does not exist/
61
- raise
62
137
  end
63
138
 
64
139
  def stop!(sig=nil)
65
- puts '[Scheddy] Stopping'
140
+ puts '[scheddy] Stopping'
141
+ # trap handlers cannot use Logger
66
142
  self.stop = true
67
143
  end
68
144
 
69
145
  def trap_signals!
70
- trap 'INT', &method(:stop!)
146
+ trap 'INT', &method(:stop!)
71
147
  trap 'QUIT', &method(:stop!)
72
148
  trap 'TERM', &method(:stop!)
149
+ trap 'USR1', &method(:stepdown!)
150
+ end
151
+
152
+ def wait_for(duration, skip_stop: false, &block)
153
+ wait_until duration.from_now, skip_stop:, &block
73
154
  end
74
155
 
75
156
  # &block - optional block - return truthy to end prematurely
@@ -81,5 +162,65 @@ module Scheddy
81
162
  end
82
163
  end
83
164
 
165
+ def with_leader
166
+ return yield(false) if !scheduler_record || tasks.empty?
167
+
168
+ wait_t = LEASE_RENEWAL_INTERVAL.from_now
169
+ if leader_state == :standby
170
+ if current_leader = Scheddy::TaskScheduler.leader.first
171
+ if current_leader.expired?
172
+ logger.error "Forcefully clearing expired leader status for id=#{current_leader.id}"
173
+ current_leader.clear_leader(only_if_expired: true)
174
+ wait_t = 5.seconds.from_now
175
+ end
176
+ else
177
+ if scheduler_record.take_leadership
178
+ self.leader_state = :new_leader
179
+ end
180
+ end
181
+ end
182
+
183
+ case leader_state
184
+ when :new_leader
185
+ logger.info 'We are now cluster leader'
186
+ self.leader_state = :existing_leader
187
+ return yield true
188
+ when :existing_leader
189
+ if !stepdown? && scheduler_record.renew_leadership
190
+ return yield false
191
+ else
192
+ if stepdown_as_leader
193
+ scheduler_record.mark_seen
194
+ end
195
+ wait_t += 5.seconds # improve odds of another daemon taking over
196
+ end
197
+ when :standby
198
+ scheduler_record.mark_seen
199
+ end
200
+
201
+ wait_until wait_t
202
+ rescue Exception => e
203
+ logger.error 'Error in scheduler; retrying in 1 minute'
204
+ Scheddy.handle_error(e)
205
+ if leader_state != :standby
206
+ logger.warn 'Due to prior error, stepping down as leader'
207
+ stepdown_as_leader skip_msg: true
208
+ end
209
+ wait_for 1.minute
210
+ end
211
+
212
+ def stepdown_as_leader(skip_msg: false)
213
+ return true if leader_state == :standby
214
+ logger.info 'Stepping down as leader' unless skip_msg
215
+ scheduler_record.clear_leader
216
+ self.stepdown = false
217
+ self.leader_state = :standby
218
+ true
219
+ rescue Exception => e
220
+ logger.error 'Failed to step down as leader'
221
+ Scheddy.handle_error(e)
222
+ false
223
+ end
224
+
84
225
  end
85
226
  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 = {
@@ -1,3 +1,3 @@
1
1
  module Scheddy
2
- VERSION = "0.2.2"
2
+ VERSION = '0.4.0'
3
3
  end
data/lib/scheddy.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  require 'fugit'
2
2
 
3
- module Scheddy
4
- end
5
-
6
3
  %w(
7
4
  config
8
5
  context
6
+ error_handler
9
7
  logger
10
8
  scheduler
11
9
  task
@@ -14,3 +12,11 @@ end
14
12
  ).each do |f|
15
13
  require_relative "scheddy/#{f}"
16
14
  end
15
+
16
+ module Scheddy
17
+
18
+ def self.run
19
+ Scheduler.new(tasks).run
20
+ end
21
+
22
+ end
@@ -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,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scheddy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thomas morgan
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-07-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: fugit
@@ -30,14 +29,14 @@ dependencies:
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '6'
32
+ version: '7'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
- version: '6'
39
+ version: '7'
41
40
  - !ruby/object:Gem::Dependency
42
41
  name: thor
43
42
  requirement: !ruby/object:Gem::Requirement
@@ -54,7 +53,8 @@ dependencies:
54
53
  version: '1.0'
55
54
  description: Scheddy is a batteries-included task scheduler for Rails. It is intended
56
55
  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.
56
+ schedulers). It is job-queue agnostic, can catch up missed tasks, and has native
57
+ clustering.
58
58
  email:
59
59
  - tm@iprog.com
60
60
  executables:
@@ -67,13 +67,16 @@ files:
67
67
  - Rakefile
68
68
  - app/models/scheddy/application_record.rb
69
69
  - app/models/scheddy/task_history.rb
70
+ - app/models/scheddy/task_scheduler.rb
70
71
  - db/migrate/20230607201527_create_scheddy_task_histories.rb
72
+ - db/migrate/20240822165904_create_scheddy_task_schedulers.rb
71
73
  - exe/scheddy
72
74
  - lib/scheddy.rb
73
75
  - lib/scheddy/cli.rb
74
76
  - lib/scheddy/config.rb
75
77
  - lib/scheddy/context.rb
76
78
  - lib/scheddy/engine.rb
79
+ - lib/scheddy/error_handler.rb
77
80
  - lib/scheddy/logger.rb
78
81
  - lib/scheddy/scheduler.rb
79
82
  - lib/scheddy/task.rb
@@ -86,7 +89,6 @@ metadata:
86
89
  homepage_uri: https://github.com/zarqman/scheddy
87
90
  source_code_uri: https://github.com/zarqman/scheddy
88
91
  changelog_uri: https://github.com/zarqman/scheddy/blob/master/CHANGELOG.md
89
- post_install_message:
90
92
  rdoc_options: []
91
93
  require_paths:
92
94
  - lib
@@ -101,9 +103,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
103
  - !ruby/object:Gem::Version
102
104
  version: '0'
103
105
  requirements: []
104
- rubygems_version: 3.4.10
105
- signing_key:
106
+ rubygems_version: 3.6.9
106
107
  specification_version: 4
107
108
  summary: Job-queue agnostic, cron-like task scheduler for Rails apps, with missed
108
- task catch-ups and other features.
109
+ task catch-ups, clustering, and other features.
109
110
  test_files: []