crono_trigger 0.8.3 → 1.0.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/.github/workflows/rspec.yml +92 -9
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/README.md +47 -1
- data/crono_trigger.gemspec +5 -4
- data/gemfiles/activerecord-71.gemfile +1 -0
- data/gemfiles/{activerecord-61.gemfile → activerecord-80.gemfile} +1 -1
- data/lib/crono_trigger/cli.rb +2 -2
- data/lib/crono_trigger/exception_handler.rb +8 -5
- data/lib/crono_trigger/execution_tracker.rb +1 -1
- data/lib/crono_trigger/global_exception_handler.rb +8 -5
- data/lib/crono_trigger/models/execution.rb +7 -17
- data/lib/crono_trigger/models/signal.rb +1 -5
- data/lib/crono_trigger/models/worker.rb +2 -10
- data/lib/crono_trigger/polling_thread.rb +1 -7
- data/lib/crono_trigger/schedulable.rb +65 -29
- data/lib/crono_trigger/version.rb +1 -1
- data/lib/crono_trigger/web.rb +16 -14
- data/lib/crono_trigger/worker.rb +3 -26
- data/lib/crono_trigger.rb +1 -1
- data/lib/generators/crono_trigger/install/templates/install.rb +1 -1
- data/lib/generators/crono_trigger/migration/templates/create_table_migration.rb +2 -2
- data/lib/generators/crono_trigger/migration/templates/migration.rb +1 -1
- metadata +29 -19
- data/gemfiles/activerecord-70.gemfile +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 855216016010a5117316cfcdf1554d8873e91605d09bbdb226d69ccfaafec918
|
|
4
|
+
data.tar.gz: ffd42586870d68941253bad9adcf3cee6da83853bba0d2584ba79941104e0775
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bfecebefd3fc5c422ddc19db8e61014ee47d87e05ff2c0944504ea9fb0196b5352d1d91a72462778a26f5c8a0ada80dd7a6391a79fc3bfaf5c598488ebc4df11
|
|
7
|
+
data.tar.gz: 01a7b05d5150b3749520b4d29a50377aada67532c14a34eee64c9067b7b571589750169c0045b50a9f9202a52aff1bfb27ab6892366f97c801334e9544cd0698
|
data/.github/workflows/rspec.yml
CHANGED
|
@@ -4,7 +4,6 @@ on:
|
|
|
4
4
|
schedule:
|
|
5
5
|
- cron: "0 9 * * 1"
|
|
6
6
|
push:
|
|
7
|
-
branches: [ master ]
|
|
8
7
|
pull_request:
|
|
9
8
|
|
|
10
9
|
jobs:
|
|
@@ -13,18 +12,16 @@ jobs:
|
|
|
13
12
|
runs-on: ubuntu-latest
|
|
14
13
|
strategy:
|
|
15
14
|
matrix:
|
|
16
|
-
ruby-version: ['3.
|
|
17
|
-
gemfile:
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
- ruby-version: '3.3'
|
|
22
|
-
gemfile: "gemfiles/activerecord-61.gemfile"
|
|
15
|
+
ruby-version: ['3.4', '4.0']
|
|
16
|
+
gemfile:
|
|
17
|
+
- Gemfile
|
|
18
|
+
- gemfiles/activerecord-71.gemfile
|
|
19
|
+
- gemfiles/activerecord-80.gemfile
|
|
23
20
|
env:
|
|
24
21
|
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
25
22
|
|
|
26
23
|
steps:
|
|
27
|
-
- uses: actions/checkout@
|
|
24
|
+
- uses: actions/checkout@v7
|
|
28
25
|
- name: Set up Ruby
|
|
29
26
|
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
|
30
27
|
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
@@ -34,3 +31,89 @@ jobs:
|
|
|
34
31
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
35
32
|
- name: Run tests
|
|
36
33
|
run: bundle exec rake
|
|
34
|
+
|
|
35
|
+
test-mysql:
|
|
36
|
+
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
strategy:
|
|
39
|
+
matrix:
|
|
40
|
+
ruby-version: ['3.4', '4.0']
|
|
41
|
+
gemfile:
|
|
42
|
+
- Gemfile
|
|
43
|
+
- gemfiles/activerecord-71.gemfile
|
|
44
|
+
- gemfiles/activerecord-80.gemfile
|
|
45
|
+
services:
|
|
46
|
+
mysql:
|
|
47
|
+
image: mysql:8.4
|
|
48
|
+
env:
|
|
49
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
|
50
|
+
MYSQL_DATABASE: test
|
|
51
|
+
ports:
|
|
52
|
+
- 3306:3306
|
|
53
|
+
options: >-
|
|
54
|
+
--health-cmd "mysqladmin ping -h 127.0.0.1"
|
|
55
|
+
--health-interval 5s
|
|
56
|
+
--health-timeout 5s
|
|
57
|
+
--health-retries 20
|
|
58
|
+
env:
|
|
59
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
60
|
+
DB: mysql
|
|
61
|
+
DB_HOST: 127.0.0.1
|
|
62
|
+
DB_PORT: 3306
|
|
63
|
+
DB_USERNAME: root
|
|
64
|
+
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v7
|
|
67
|
+
- name: Set up Ruby
|
|
68
|
+
uses: ruby/setup-ruby@v1
|
|
69
|
+
with:
|
|
70
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
71
|
+
bundler-cache: true
|
|
72
|
+
- name: Run tests
|
|
73
|
+
run: bundle exec rake
|
|
74
|
+
env:
|
|
75
|
+
# `job` context is only available in step level
|
|
76
|
+
MYSQL_RESTART_COMMAND: >-
|
|
77
|
+
docker restart ${{ job.services.mysql.id }} &&
|
|
78
|
+
until docker exec ${{ job.services.mysql.id }} mysqladmin ping -h 127.0.0.1 --silent; do sleep 1; done
|
|
79
|
+
|
|
80
|
+
test-postgresql:
|
|
81
|
+
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
strategy:
|
|
84
|
+
matrix:
|
|
85
|
+
ruby-version: ['3.4', '4.0']
|
|
86
|
+
gemfile:
|
|
87
|
+
- Gemfile
|
|
88
|
+
- gemfiles/activerecord-71.gemfile
|
|
89
|
+
- gemfiles/activerecord-80.gemfile
|
|
90
|
+
services:
|
|
91
|
+
postgres:
|
|
92
|
+
image: postgres:18
|
|
93
|
+
env:
|
|
94
|
+
POSTGRES_PASSWORD: postgres
|
|
95
|
+
POSTGRES_DB: test
|
|
96
|
+
ports:
|
|
97
|
+
- 5432:5432
|
|
98
|
+
options: >-
|
|
99
|
+
--health-cmd "pg_isready -h 127.0.0.1 -U postgres"
|
|
100
|
+
--health-interval 5s
|
|
101
|
+
--health-timeout 5s
|
|
102
|
+
--health-retries 20
|
|
103
|
+
env:
|
|
104
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
|
105
|
+
DB: postgresql
|
|
106
|
+
DB_HOST: 127.0.0.1
|
|
107
|
+
DB_PORT: 5432
|
|
108
|
+
DB_USERNAME: postgres
|
|
109
|
+
DB_PASSWORD: postgres
|
|
110
|
+
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v7
|
|
113
|
+
- name: Set up Ruby
|
|
114
|
+
uses: ruby/setup-ruby@v1
|
|
115
|
+
with:
|
|
116
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
117
|
+
bundler-cache: true
|
|
118
|
+
- name: Run tests
|
|
119
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -25,8 +25,38 @@ Or install it yourself as:
|
|
|
25
25
|
|
|
26
26
|
$ gem install crono_trigger
|
|
27
27
|
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
CronoTrigger fetches executable records with `SELECT ... FOR UPDATE SKIP LOCKED`.
|
|
31
|
+
So the database must support `SKIP LOCKED` (MySQL 8.0+ or PostgreSQL 9.5+).
|
|
32
|
+
|
|
33
|
+
CronoTrigger also requires ActiveRecord 7.1+.
|
|
34
|
+
|
|
35
|
+
If your database does not support `SKIP LOCKED` or you use ActiveRecord < 7.1, use crono_trigger < 1.0.0.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
gem 'crono_trigger', '< 1.0.0'
|
|
39
|
+
```
|
|
40
|
+
|
|
28
41
|
## Breaking Changes
|
|
29
42
|
|
|
43
|
+
### Update from v0.8.x
|
|
44
|
+
|
|
45
|
+
- The database must support `SKIP LOCKED` (MySQL 8.0+ or PostgreSQL 9.5+).
|
|
46
|
+
Records locked by other workers are skipped instead of waited for.
|
|
47
|
+
If your database does not support `SKIP LOCKED`, keep using crono_trigger < 1.0.0.
|
|
48
|
+
- ActiveRecord < 7.1 is no longer supported. If you use it, keep using crono_trigger < 1.0.0.
|
|
49
|
+
- The default value of `fetch_records` is changed from `executor_thread * 3` to `1`.
|
|
50
|
+
See [Tuning `fetch_records`](#tuning-fetch_records).
|
|
51
|
+
- The `worker_count` argument of `executables_with_lock` and `CronoTrigger::PollingThread#worker_count=` are removed.
|
|
52
|
+
- `exponential_backoff` actually doubles the retry interval on each retry (`retry_interval * 2 ** retry_count`).
|
|
53
|
+
Previously, it increased the interval linearly after the third retry.
|
|
54
|
+
- `crono_trigger` command without MODEL arguments polls all models including `CronoTrigger::Schedulable` as described in its help.
|
|
55
|
+
Previously, it polled no models.
|
|
56
|
+
|
|
57
|
+
Because a locking read locks the index records which it scans, make sure that the index starting with `next_execute_at` (See [Migration sample](#migration-sample)) exists.
|
|
58
|
+
Without the index, a polling query might lock much more records than it fetches.
|
|
59
|
+
|
|
30
60
|
### Update from v0.6.x
|
|
31
61
|
|
|
32
62
|
In previous version, now is `2023-06-07T18:00:00+00:00`, a cron definition is `0 1 * * * *`, and `started_at` is `2023-06-08T1:00:00+00:00`.
|
|
@@ -170,7 +200,7 @@ Usage: crono_trigger [options] MODEL [MODEL..]
|
|
|
170
200
|
-p, --polling-thread=SIZE Polling thread size (Default: 1)
|
|
171
201
|
-i, --polling-interval=SECOND Polling interval seconds (Default: 5)
|
|
172
202
|
-c, --concurrency=SIZE Execute thread size (Default: 25)
|
|
173
|
-
-r, --fetch-records=SIZE Record count fetched by polling thread (Default:
|
|
203
|
+
-r, --fetch-records=SIZE Record count fetched by polling thread at once (Default: 1)
|
|
174
204
|
-l, --log=LOGFILE Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)
|
|
175
205
|
--log-level=LEVEL Set log level (Default: info)
|
|
176
206
|
-d, --daemonize Daemon mode
|
|
@@ -178,6 +208,18 @@ Usage: crono_trigger [options] MODEL [MODEL..]
|
|
|
178
208
|
-h, --help Prints this help
|
|
179
209
|
```
|
|
180
210
|
|
|
211
|
+
#### Tuning `fetch_records`
|
|
212
|
+
|
|
213
|
+
A polling thread fetches and locks `fetch_records` records in a transaction, and repeats it while it can fetch `fetch_records` records.
|
|
214
|
+
By default, it fetches records one by one. It is enough in most cases, and it keeps locking duration short and distributes records evenly to workers.
|
|
215
|
+
|
|
216
|
+
If there are many jobs and each job finishes quickly, fetching one by one requires a few round trips to database per record and it can be a bottleneck.
|
|
217
|
+
In such case, increase `fetch_records` (for example, the same value as `--concurrency`).
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
$ crono_trigger --concurrency=25 --fetch-records=25 MailNotification
|
|
221
|
+
```
|
|
222
|
+
|
|
181
223
|
#### Handle errors
|
|
182
224
|
|
|
183
225
|
This gem provides the following options to handle errors:
|
|
@@ -228,6 +270,10 @@ CronoTrigger.configure do |config|
|
|
|
228
270
|
end
|
|
229
271
|
```
|
|
230
272
|
|
|
273
|
+
NOTE: If the connection is lost right after the transaction that fetches and locks records is committed, the polling thread cannot know that the records have been locked.
|
|
274
|
+
The retry fetches other records, and the locked records are not executed until `execute_lock_timeout` passes.
|
|
275
|
+
They are executed after that as usual, so they are not lost but delayed.
|
|
276
|
+
|
|
231
277
|
|
|
232
278
|
## Specification
|
|
233
279
|
|
data/crono_trigger.gemspec
CHANGED
|
@@ -28,15 +28,16 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.add_dependency "tzinfo"
|
|
29
29
|
spec.add_dependency "sinatra"
|
|
30
30
|
spec.add_dependency "rack-contrib"
|
|
31
|
-
spec.add_dependency "
|
|
32
|
-
spec.add_dependency "activerecord", ">= 4.2"
|
|
31
|
+
spec.add_dependency "activerecord", ">= 7.1"
|
|
33
32
|
spec.add_dependency "retriable"
|
|
33
|
+
spec.add_dependency "ostruct"
|
|
34
34
|
|
|
35
|
-
spec.add_development_dependency "sqlite3", "
|
|
35
|
+
spec.add_development_dependency "sqlite3", ">= 1.3"
|
|
36
36
|
spec.add_development_dependency "mysql2"
|
|
37
|
+
spec.add_development_dependency "pg"
|
|
37
38
|
spec.add_development_dependency "timecop"
|
|
38
39
|
spec.add_development_dependency "rollbar"
|
|
39
|
-
spec.add_development_dependency "bundler", "
|
|
40
|
+
spec.add_development_dependency "bundler", ">= 2.0"
|
|
40
41
|
spec.add_development_dependency "rake"
|
|
41
42
|
spec.add_development_dependency "rspec"
|
|
42
43
|
spec.add_development_dependency "simplecov"
|
data/lib/crono_trigger/cli.rb
CHANGED
|
@@ -34,7 +34,7 @@ opt_parser = OptionParser.new do |opts|
|
|
|
34
34
|
options[:executor_thread] = i
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
opts.on("-r", "--fetch-records=SIZE", Integer, "Record count fetched by polling thread (Default:
|
|
37
|
+
opts.on("-r", "--fetch-records=SIZE", Integer, "Record count fetched by polling thread at once (Default: 1)") do |i|
|
|
38
38
|
options[:fetch_records] = i
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -75,7 +75,7 @@ CronoTrigger.load_config(options[:config], options[:env]) if options[:config]
|
|
|
75
75
|
CronoTrigger.config[name] = options[name] if options[name]
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
CronoTrigger.config.model_names = ARGV
|
|
78
|
+
CronoTrigger.config.model_names = ARGV unless ARGV.empty?
|
|
79
79
|
|
|
80
80
|
se = ServerEngine.create(nil, CronoTrigger::Worker, {
|
|
81
81
|
daemonize: options[:daemonize],
|
|
@@ -10,15 +10,16 @@ module CronoTrigger
|
|
|
10
10
|
|
|
11
11
|
def handle_exception(ex)
|
|
12
12
|
handlers = CronoTrigger.config.error_handlers + Array(@record.crono_trigger_options[:error_handlers])
|
|
13
|
-
handlers.each do |
|
|
14
|
-
callable, arity = ensure_callable(
|
|
13
|
+
handlers.each do |handler|
|
|
14
|
+
callable, arity = ensure_callable(handler)
|
|
15
15
|
args = [ex, @record]
|
|
16
16
|
args = arity < 0 ? args : args.take(arity)
|
|
17
17
|
callable.call(*args)
|
|
18
|
+
# Rescue each handler so that an error in one handler does not prevent the following handlers
|
|
19
|
+
rescue Exception => e
|
|
20
|
+
@record.logger&.error("CronoTrigger error handler raises error")
|
|
21
|
+
@record.logger&.error(e)
|
|
18
22
|
end
|
|
19
|
-
rescue Exception => e
|
|
20
|
-
@record.logger.error("CronoTrigger error handler raises error")
|
|
21
|
-
@record.logger.error(e)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
private
|
|
@@ -28,6 +29,8 @@ module CronoTrigger
|
|
|
28
29
|
return callable, callable.arity
|
|
29
30
|
elsif callable.is_a?(Symbol)
|
|
30
31
|
return @record.method(callable), 1
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "error handler must respond to `call` or be a Symbol: #{callable.inspect}"
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
end
|
|
@@ -6,16 +6,17 @@ module CronoTrigger
|
|
|
6
6
|
|
|
7
7
|
def handle_global_exception(ex)
|
|
8
8
|
handlers = CronoTrigger.config.global_error_handlers
|
|
9
|
-
handlers.each do |
|
|
10
|
-
callable, arity = ensure_callable(
|
|
9
|
+
handlers.each do |handler|
|
|
10
|
+
callable, arity = ensure_callable(handler)
|
|
11
11
|
|
|
12
12
|
args = [ex]
|
|
13
13
|
args = arity < 0 ? args : args.take(arity)
|
|
14
14
|
callable.call(*args)
|
|
15
|
+
# Rescue each handler so that an error in one handler does not prevent the following handlers
|
|
16
|
+
rescue Exception => e
|
|
17
|
+
ActiveRecord::Base.logger&.error("CronoTrigger error handler raises error")
|
|
18
|
+
ActiveRecord::Base.logger&.error(e)
|
|
15
19
|
end
|
|
16
|
-
rescue Exception => e
|
|
17
|
-
ActiveRecord::Base.logger.error("CronoTrigger error handler raises error")
|
|
18
|
-
ActiveRecord::Base.logger.error(e)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
private
|
|
@@ -23,6 +24,8 @@ module CronoTrigger
|
|
|
23
24
|
def ensure_callable(callable)
|
|
24
25
|
if callable.respond_to?(:call)
|
|
25
26
|
return callable, callable.arity
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, "global error handler must respond to `call`: #{callable.inspect}"
|
|
26
29
|
end
|
|
27
30
|
end
|
|
28
31
|
end
|
|
@@ -7,23 +7,13 @@ module CronoTrigger
|
|
|
7
7
|
|
|
8
8
|
scope :recently, ->(schedule_type:) { where(schedule_type: schedule_type).order(executed_at: :desc) }
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
else
|
|
19
|
-
enum status: {
|
|
20
|
-
executing: "executing",
|
|
21
|
-
completed: "completed",
|
|
22
|
-
failed: "failed",
|
|
23
|
-
retrying: "retrying",
|
|
24
|
-
aborted: "aborted",
|
|
25
|
-
}
|
|
26
|
-
end
|
|
10
|
+
enum :status, {
|
|
11
|
+
executing: "executing",
|
|
12
|
+
completed: "completed",
|
|
13
|
+
failed: "failed",
|
|
14
|
+
retrying: "retrying",
|
|
15
|
+
aborted: "aborted",
|
|
16
|
+
}
|
|
27
17
|
|
|
28
18
|
def self.create_with_timestamp!
|
|
29
19
|
create!(executed_at: Time.current, status: :executing, worker_id: CronoTrigger.config.worker_id)
|
|
@@ -5,11 +5,7 @@ module CronoTrigger
|
|
|
5
5
|
|
|
6
6
|
IGNORE_THRESHOLD = 300
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
enum :signal, {TERM: "TERM", USR1: "USR1", CONT: "CONT", TSTP: "TSTP"}
|
|
10
|
-
else
|
|
11
|
-
enum signal: {TERM: "TERM", USR1: "USR1", CONT: "CONT", TSTP: "TSTP"}
|
|
12
|
-
end
|
|
8
|
+
enum :signal, {TERM: "TERM", USR1: "USR1", CONT: "CONT", TSTP: "TSTP"}
|
|
13
9
|
|
|
14
10
|
scope :sent_to_me, proc {
|
|
15
11
|
raise "Must set worker_id" unless CronoTrigger.config.worker_id
|
|
@@ -7,17 +7,9 @@ module CronoTrigger
|
|
|
7
7
|
|
|
8
8
|
ALIVE_THRESHOLD = CronoTrigger::Worker::HEARTBEAT_INTERVAL * 5
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
enum :executor_status, {running: "running", quiet: "quiet", shuttingdown: "shuttingdown", shutdown: "shutdown"}
|
|
12
|
-
else
|
|
13
|
-
enum executor_status: {running: "running", quiet: "quiet", shuttingdown: "shuttingdown", shutdown: "shutdown"}
|
|
14
|
-
end
|
|
10
|
+
enum :executor_status, {running: "running", quiet: "quiet", shuttingdown: "shuttingdown", shutdown: "shutdown"}
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
serialize :polling_model_names, coder: JSON
|
|
18
|
-
else
|
|
19
|
-
serialize :polling_model_names, JSON
|
|
20
|
-
end
|
|
12
|
+
serialize :polling_model_names, coder: JSON
|
|
21
13
|
|
|
22
14
|
scope :alive_workers, proc { where(arel_table[:last_heartbeated_at].gteq(Time.current - ALIVE_THRESHOLD)) }
|
|
23
15
|
end
|
|
@@ -10,7 +10,6 @@ module CronoTrigger
|
|
|
10
10
|
end
|
|
11
11
|
@execution_counter = execution_counter
|
|
12
12
|
@quiet = Concurrent::AtomicBoolean.new(false)
|
|
13
|
-
@worker_count = 1
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def run
|
|
@@ -60,7 +59,7 @@ module CronoTrigger
|
|
|
60
59
|
while maybe_has_next && !@stop_flag.set?
|
|
61
60
|
records, maybe_has_next = CronoTrigger.retry_on_db_errors do
|
|
62
61
|
model.connection_pool.with_connection do
|
|
63
|
-
model.executables_with_lock(limit: CronoTrigger.config.fetch_records
|
|
62
|
+
model.executables_with_lock(limit: CronoTrigger.config.fetch_records)
|
|
64
63
|
end
|
|
65
64
|
end
|
|
66
65
|
|
|
@@ -77,11 +76,6 @@ module CronoTrigger
|
|
|
77
76
|
end
|
|
78
77
|
end
|
|
79
78
|
|
|
80
|
-
def worker_count=(n)
|
|
81
|
-
raise ArgumentError, "worker_count must be greater than 0" if n <= 0
|
|
82
|
-
@worker_count = n
|
|
83
|
-
end
|
|
84
|
-
|
|
85
79
|
private
|
|
86
80
|
|
|
87
81
|
def process_record(record)
|
|
@@ -63,31 +63,28 @@ module CronoTrigger
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
module ClassMethods
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
r = all.lock.find(id)
|
|
76
|
-
unless r.locking?
|
|
77
|
-
r.crono_trigger_lock!
|
|
78
|
-
records << r
|
|
79
|
-
end
|
|
80
|
-
end
|
|
66
|
+
# Requires a database that supports `FOR UPDATE SKIP LOCKED` (MySQL 8.0+, PostgreSQL 9.5+).
|
|
67
|
+
# Rows locked by other workers' transactions are skipped instead of waited for.
|
|
68
|
+
def executables_with_lock(limit: CronoTrigger.config.fetch_records)
|
|
69
|
+
now = Time.current
|
|
70
|
+
attributes = {
|
|
71
|
+
crono_trigger_column_name(:execute_lock) => now.to_i,
|
|
72
|
+
crono_trigger_column_name(:locked_by) => CronoTrigger.config.worker_id,
|
|
73
|
+
}
|
|
74
|
+
attributes["updated_at"] = now if column_names.include?("updated_at")
|
|
81
75
|
|
|
82
|
-
|
|
76
|
+
records = transaction do
|
|
77
|
+
rs = executables(from: now, limit: limit).lock("FOR UPDATE SKIP LOCKED").to_a
|
|
78
|
+
unscoped.where(primary_key => rs.map(&:id)).update_all(attributes) unless rs.empty?
|
|
79
|
+
rs
|
|
83
80
|
end
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
records.each do |r|
|
|
83
|
+
r.assign_attributes(attributes)
|
|
84
|
+
r.clear_changes_information
|
|
85
|
+
end
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
[records, maybe_has_next]
|
|
87
|
+
[records, records.size >= limit]
|
|
91
88
|
end
|
|
92
89
|
|
|
93
90
|
def crono_trigger_column_name(name)
|
|
@@ -112,16 +109,27 @@ module CronoTrigger
|
|
|
112
109
|
|
|
113
110
|
private
|
|
114
111
|
|
|
112
|
+
# Reassign instead of mutating because class_attribute shares the same object with subclasses
|
|
115
113
|
def add_executable_conditions(pr)
|
|
116
|
-
self.executable_conditions
|
|
114
|
+
self.executable_conditions = executable_conditions + [pr]
|
|
117
115
|
end
|
|
118
116
|
|
|
119
117
|
def clear_executable_conditions
|
|
120
|
-
self.executable_conditions
|
|
118
|
+
self.executable_conditions = []
|
|
121
119
|
end
|
|
122
120
|
end
|
|
123
121
|
|
|
122
|
+
# A copy does not hold the lock, but `dup` would share the lock state of the original during an execution
|
|
123
|
+
private def initialize_dup(other)
|
|
124
|
+
super
|
|
125
|
+
@crono_trigger_holding_lock = nil
|
|
126
|
+
end
|
|
127
|
+
|
|
124
128
|
def do_execute
|
|
129
|
+
@crono_trigger_holding_lock = {
|
|
130
|
+
crono_trigger_column_name(:execute_lock) => self[crono_trigger_column_name(:execute_lock)],
|
|
131
|
+
crono_trigger_column_name(:locked_by) => self[crono_trigger_column_name(:locked_by)],
|
|
132
|
+
}
|
|
125
133
|
ExecutionTracker.track(self) do
|
|
126
134
|
do_execute_with_catch
|
|
127
135
|
end
|
|
@@ -129,6 +137,8 @@ module CronoTrigger
|
|
|
129
137
|
logger.error(ex) if logger
|
|
130
138
|
save_last_error_info(ex)
|
|
131
139
|
retry_or_reset!(ex)
|
|
140
|
+
ensure
|
|
141
|
+
@crono_trigger_holding_lock = nil
|
|
132
142
|
end
|
|
133
143
|
|
|
134
144
|
private def do_execute_with_catch
|
|
@@ -181,10 +191,12 @@ module CronoTrigger
|
|
|
181
191
|
logger.info "Retry #{self.class}-#{id}" if logger
|
|
182
192
|
|
|
183
193
|
now = Time.current
|
|
194
|
+
has_retry_count = self.class.column_names.include?("retry_count")
|
|
195
|
+
current_retry_count = has_retry_count ? retry_count.to_i : 0
|
|
184
196
|
if immediately
|
|
185
197
|
wait = 0
|
|
186
198
|
else
|
|
187
|
-
wait = crono_trigger_options[:exponential_backoff] ? retry_interval *
|
|
199
|
+
wait = crono_trigger_options[:exponential_backoff] ? retry_interval * (2 ** current_retry_count) : retry_interval
|
|
188
200
|
end
|
|
189
201
|
attributes = {
|
|
190
202
|
crono_trigger_column_name(:next_execute_at) => now + wait,
|
|
@@ -192,12 +204,12 @@ module CronoTrigger
|
|
|
192
204
|
crono_trigger_column_name(:locked_by) => nil,
|
|
193
205
|
}
|
|
194
206
|
|
|
195
|
-
if
|
|
196
|
-
attributes.merge!(retry_count:
|
|
207
|
+
if has_retry_count
|
|
208
|
+
attributes.merge!(retry_count: current_retry_count + 1)
|
|
197
209
|
end
|
|
198
210
|
|
|
199
211
|
merge_updated_at_for_crono_trigger!(attributes, now)
|
|
200
|
-
|
|
212
|
+
update_crono_trigger_columns(attributes)
|
|
201
213
|
end
|
|
202
214
|
end
|
|
203
215
|
|
|
@@ -225,7 +237,7 @@ module CronoTrigger
|
|
|
225
237
|
end
|
|
226
238
|
|
|
227
239
|
merge_updated_at_for_crono_trigger!(attributes, now)
|
|
228
|
-
|
|
240
|
+
update_crono_trigger_columns(attributes)
|
|
229
241
|
end
|
|
230
242
|
|
|
231
243
|
def abort_execution!
|
|
@@ -392,7 +404,31 @@ module CronoTrigger
|
|
|
392
404
|
end
|
|
393
405
|
|
|
394
406
|
merge_updated_at_for_crono_trigger!(attributes)
|
|
395
|
-
|
|
407
|
+
update_crono_trigger_columns(attributes) unless attributes.empty?
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# While executing, update only if this record still holds the lock acquired before the execution.
|
|
411
|
+
# Otherwise, the record has been locked by another worker after execute_lock_timeout, and updating it would
|
|
412
|
+
# overwrite the schedule which another worker is executing.
|
|
413
|
+
# Outside of executions (e.g. operations from the admin web), update unconditionally as before.
|
|
414
|
+
def update_crono_trigger_columns(attributes)
|
|
415
|
+
return update_columns(attributes) unless @crono_trigger_holding_lock
|
|
416
|
+
|
|
417
|
+
updated_count = self.class.unscoped
|
|
418
|
+
.where(self.class.primary_key => id)
|
|
419
|
+
.where(@crono_trigger_holding_lock)
|
|
420
|
+
.update_all(attributes)
|
|
421
|
+
if updated_count.zero?
|
|
422
|
+
logger&.warn("Skip updating #{self.class}-#{id} because the record has been deleted or locked by another worker")
|
|
423
|
+
return false
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
attributes.each { |name, value| self[name] = value }
|
|
427
|
+
clear_attribute_changes(attributes.keys.map(&:to_s))
|
|
428
|
+
@crono_trigger_holding_lock.each_key do |name|
|
|
429
|
+
@crono_trigger_holding_lock[name] = attributes[name] if attributes.key?(name)
|
|
430
|
+
end
|
|
431
|
+
true
|
|
396
432
|
end
|
|
397
433
|
|
|
398
434
|
def merge_updated_at_for_crono_trigger!(attributes, time = Time.current)
|
data/lib/crono_trigger/web.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "crono_trigger"
|
|
2
2
|
require "sinatra/base"
|
|
3
3
|
require "rack/contrib/post_body_content_type_parser"
|
|
4
|
-
require "
|
|
4
|
+
require "active_support/json"
|
|
5
5
|
|
|
6
6
|
module CronoTrigger
|
|
7
7
|
class Web < Sinatra::Application
|
|
@@ -19,9 +19,9 @@ module CronoTrigger
|
|
|
19
19
|
if params[:format] == "json"
|
|
20
20
|
content_type :json
|
|
21
21
|
@workers = CronoTrigger::Models::Worker.alive_workers
|
|
22
|
-
|
|
22
|
+
{
|
|
23
23
|
records: @workers,
|
|
24
|
-
}
|
|
24
|
+
}.to_json
|
|
25
25
|
else
|
|
26
26
|
raise "unknown format"
|
|
27
27
|
end
|
|
@@ -35,16 +35,18 @@ module CronoTrigger
|
|
|
35
35
|
worker_id = params[:worker_id]
|
|
36
36
|
sig = params[:signal]
|
|
37
37
|
if worker_id && sig
|
|
38
|
-
|
|
38
|
+
# Check before creating because assigning an undefined enum value raises ArgumentError
|
|
39
|
+
if CronoTrigger::Models::Signal.signals.key?(sig)
|
|
40
|
+
CronoTrigger::Models::Signal.send_signal(sig, worker_id)
|
|
39
41
|
status 200
|
|
40
42
|
body ""
|
|
41
43
|
else
|
|
42
44
|
status 422
|
|
43
|
-
|
|
45
|
+
{error: "#{sig} signal is not supported"}.to_json
|
|
44
46
|
end
|
|
45
47
|
else
|
|
46
48
|
status 422
|
|
47
|
-
|
|
49
|
+
{error: "Must set worker_id and signal"}.to_json
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -52,9 +54,9 @@ module CronoTrigger
|
|
|
52
54
|
if params[:format] == "json"
|
|
53
55
|
content_type :json
|
|
54
56
|
@signals = CronoTrigger::Models::Signal.order(sent_at: :desc).limit(30)
|
|
55
|
-
|
|
57
|
+
{
|
|
56
58
|
records: @signals,
|
|
57
|
-
}
|
|
59
|
+
}.to_json
|
|
58
60
|
else
|
|
59
61
|
raise "unknown format"
|
|
60
62
|
end
|
|
@@ -136,9 +138,9 @@ module CronoTrigger
|
|
|
136
138
|
-"delay_sec" => r.locking?(at: now) ? 0 : (now - r[r.crono_trigger_column_name(:next_execute_at)]).to_i,
|
|
137
139
|
}
|
|
138
140
|
end
|
|
139
|
-
|
|
141
|
+
{
|
|
140
142
|
records: records,
|
|
141
|
-
}
|
|
143
|
+
}.to_json
|
|
142
144
|
else
|
|
143
145
|
status 404
|
|
144
146
|
"Model Class is not found"
|
|
@@ -156,9 +158,9 @@ module CronoTrigger
|
|
|
156
158
|
if params[:format] == "json"
|
|
157
159
|
content_type :json
|
|
158
160
|
@models = CronoTrigger::Schedulable.included_by.map(&:name).sort
|
|
159
|
-
|
|
161
|
+
{
|
|
160
162
|
models: @models,
|
|
161
|
-
}
|
|
163
|
+
}.to_json
|
|
162
164
|
else
|
|
163
165
|
raise "unknown format"
|
|
164
166
|
end
|
|
@@ -189,9 +191,9 @@ module CronoTrigger
|
|
|
189
191
|
-"error_reason" => r.error_reason,
|
|
190
192
|
}
|
|
191
193
|
end
|
|
192
|
-
|
|
194
|
+
{
|
|
193
195
|
records: records,
|
|
194
|
-
}
|
|
196
|
+
}.to_json
|
|
195
197
|
else
|
|
196
198
|
status 404
|
|
197
199
|
"Model Class is not found"
|
data/lib/crono_trigger/worker.rb
CHANGED
|
@@ -7,7 +7,6 @@ module CronoTrigger
|
|
|
7
7
|
HEARTBEAT_INTERVAL = 60
|
|
8
8
|
SIGNAL_FETCH_INTERVAL = 10
|
|
9
9
|
MONITOR_INTERVAL = 20
|
|
10
|
-
WORKER_COUNT_UPDATE_INTERVAL = 60
|
|
11
10
|
EXECUTOR_SHUTDOWN_TIMELIMIT = 300
|
|
12
11
|
OTHER_THREAD_SHUTDOWN_TIMELIMIT = 120
|
|
13
12
|
attr_reader :polling_threads
|
|
@@ -19,7 +18,9 @@ module CronoTrigger
|
|
|
19
18
|
@signal_fetch_stop_flag = ServerEngine::BlockingFlag.new
|
|
20
19
|
@monitor_stop_flag = ServerEngine::BlockingFlag.new
|
|
21
20
|
@model_queue = Queue.new
|
|
22
|
-
|
|
21
|
+
# Model names are used as String in PollingThread (`classify`) and serialized to the workers table,
|
|
22
|
+
# but `included_by` holds Class objects.
|
|
23
|
+
@model_names = (CronoTrigger.config.model_names.presence || CronoTrigger::Schedulable.included_by).map(&:to_s)
|
|
23
24
|
@model_names.each do |model_name|
|
|
24
25
|
@model_queue << model_name
|
|
25
26
|
end
|
|
@@ -45,8 +46,6 @@ module CronoTrigger
|
|
|
45
46
|
@polling_threads = polling_threads
|
|
46
47
|
@polling_threads.each(&:run)
|
|
47
48
|
|
|
48
|
-
@worker_count_updater_thread = run_worker_count_updater_thread
|
|
49
|
-
|
|
50
49
|
ServerEngine::SignalThread.new do |st|
|
|
51
50
|
st.trap(:TSTP) do
|
|
52
51
|
@logger.info("[worker_id:#{@crono_trigger_worker_id}] Transit to quiet mode")
|
|
@@ -61,7 +60,6 @@ module CronoTrigger
|
|
|
61
60
|
@executor.wait_for_termination(EXECUTOR_SHUTDOWN_TIMELIMIT)
|
|
62
61
|
@heartbeat_thread.join(OTHER_THREAD_SHUTDOWN_TIMELIMIT)
|
|
63
62
|
@signal_fetcn_thread.join(OTHER_THREAD_SHUTDOWN_TIMELIMIT)
|
|
64
|
-
@worker_count_updater_thread.join(OTHER_THREAD_SHUTDOWN_TIMELIMIT)
|
|
65
63
|
|
|
66
64
|
unregister
|
|
67
65
|
end
|
|
@@ -108,15 +106,6 @@ module CronoTrigger
|
|
|
108
106
|
end
|
|
109
107
|
end
|
|
110
108
|
|
|
111
|
-
def run_worker_count_updater_thread
|
|
112
|
-
update_worker_count
|
|
113
|
-
Thread.start do
|
|
114
|
-
until @stop_flag.wait_for_set(WORKER_COUNT_UPDATE_INTERVAL)
|
|
115
|
-
update_worker_count
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
109
|
def heartbeat
|
|
121
110
|
CronoTrigger.retry_on_db_errors do
|
|
122
111
|
CronoTrigger::Models::Worker.connection_pool.with_connection do
|
|
@@ -209,18 +198,6 @@ module CronoTrigger
|
|
|
209
198
|
CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
|
|
210
199
|
end
|
|
211
200
|
|
|
212
|
-
def update_worker_count
|
|
213
|
-
CronoTrigger.retry_on_db_errors do
|
|
214
|
-
CronoTrigger::Models::Worker.connection_pool.with_connection do
|
|
215
|
-
worker_count = workers_processing_same_models.count
|
|
216
|
-
return if worker_count.zero?
|
|
217
|
-
@polling_threads.each { |th| th.worker_count = worker_count }
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
rescue => ex
|
|
221
|
-
CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
201
|
def workers_processing_same_models
|
|
225
202
|
CronoTrigger.workers.where("polling_model_names = ?", @model_names.to_json)
|
|
226
203
|
end
|
data/lib/crono_trigger.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class CreateCronoTriggerSystemTables < ActiveRecord::Migration<%=
|
|
1
|
+
class CreateCronoTriggerSystemTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
3
|
create_table :crono_trigger_workers, id: :string, primary_key: :worker_id do |t|
|
|
4
4
|
t.integer :max_thread_size, null: false
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class <%= migration_class_name %> < ActiveRecord::Migration<%=
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
3
|
create_table :<%= table_name %><%= respond_to?(:primary_key_type) ? primary_key_type : "" %> do |t|
|
|
4
4
|
<% attributes.each do |attribute| -%>
|
|
@@ -26,7 +26,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::M
|
|
|
26
26
|
t.integer :retry_count, default: 0, null: false
|
|
27
27
|
|
|
28
28
|
<% if options[:timestamps] %>
|
|
29
|
-
t.timestamps
|
|
29
|
+
t.timestamps null: false
|
|
30
30
|
<% end -%>
|
|
31
31
|
end
|
|
32
32
|
<% attributes.select { |attribute| attribute.respond_to?(:token?) && attribute.token? }.each do |attribute| -%>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class <%= migration_class_name %> < ActiveRecord::Migration<%=
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
2
|
def change
|
|
3
3
|
# columns for CronoTrigger::Schedulable
|
|
4
4
|
add_column :<%= table_name %>, :cron, :string
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crono_trigger
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- joker1007
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: chrono
|
|
@@ -95,35 +94,35 @@ dependencies:
|
|
|
95
94
|
- !ruby/object:Gem::Version
|
|
96
95
|
version: '0'
|
|
97
96
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name:
|
|
97
|
+
name: activerecord
|
|
99
98
|
requirement: !ruby/object:Gem::Requirement
|
|
100
99
|
requirements:
|
|
101
100
|
- - ">="
|
|
102
101
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
102
|
+
version: '7.1'
|
|
104
103
|
type: :runtime
|
|
105
104
|
prerelease: false
|
|
106
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
106
|
requirements:
|
|
108
107
|
- - ">="
|
|
109
108
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
109
|
+
version: '7.1'
|
|
111
110
|
- !ruby/object:Gem::Dependency
|
|
112
|
-
name:
|
|
111
|
+
name: retriable
|
|
113
112
|
requirement: !ruby/object:Gem::Requirement
|
|
114
113
|
requirements:
|
|
115
114
|
- - ">="
|
|
116
115
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
116
|
+
version: '0'
|
|
118
117
|
type: :runtime
|
|
119
118
|
prerelease: false
|
|
120
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
120
|
requirements:
|
|
122
121
|
- - ">="
|
|
123
122
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
123
|
+
version: '0'
|
|
125
124
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
125
|
+
name: ostruct
|
|
127
126
|
requirement: !ruby/object:Gem::Requirement
|
|
128
127
|
requirements:
|
|
129
128
|
- - ">="
|
|
@@ -140,14 +139,14 @@ dependencies:
|
|
|
140
139
|
name: sqlite3
|
|
141
140
|
requirement: !ruby/object:Gem::Requirement
|
|
142
141
|
requirements:
|
|
143
|
-
- - "
|
|
142
|
+
- - ">="
|
|
144
143
|
- !ruby/object:Gem::Version
|
|
145
144
|
version: '1.3'
|
|
146
145
|
type: :development
|
|
147
146
|
prerelease: false
|
|
148
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
148
|
requirements:
|
|
150
|
-
- - "
|
|
149
|
+
- - ">="
|
|
151
150
|
- !ruby/object:Gem::Version
|
|
152
151
|
version: '1.3'
|
|
153
152
|
- !ruby/object:Gem::Dependency
|
|
@@ -164,6 +163,20 @@ dependencies:
|
|
|
164
163
|
- - ">="
|
|
165
164
|
- !ruby/object:Gem::Version
|
|
166
165
|
version: '0'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: pg
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - ">="
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '0'
|
|
173
|
+
type: :development
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
167
180
|
- !ruby/object:Gem::Dependency
|
|
168
181
|
name: timecop
|
|
169
182
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -196,14 +209,14 @@ dependencies:
|
|
|
196
209
|
name: bundler
|
|
197
210
|
requirement: !ruby/object:Gem::Requirement
|
|
198
211
|
requirements:
|
|
199
|
-
- - "
|
|
212
|
+
- - ">="
|
|
200
213
|
- !ruby/object:Gem::Version
|
|
201
214
|
version: '2.0'
|
|
202
215
|
type: :development
|
|
203
216
|
prerelease: false
|
|
204
217
|
version_requirements: !ruby/object:Gem::Requirement
|
|
205
218
|
requirements:
|
|
206
|
-
- - "
|
|
219
|
+
- - ">="
|
|
207
220
|
- !ruby/object:Gem::Version
|
|
208
221
|
version: '2.0'
|
|
209
222
|
- !ruby/object:Gem::Dependency
|
|
@@ -284,9 +297,8 @@ files:
|
|
|
284
297
|
- crono_trigger.gemspec
|
|
285
298
|
- exe/crono_trigger
|
|
286
299
|
- exe/crono_trigger-web
|
|
287
|
-
- gemfiles/activerecord-61.gemfile
|
|
288
|
-
- gemfiles/activerecord-70.gemfile
|
|
289
300
|
- gemfiles/activerecord-71.gemfile
|
|
301
|
+
- gemfiles/activerecord-80.gemfile
|
|
290
302
|
- lib/crono_trigger.rb
|
|
291
303
|
- lib/crono_trigger/cli.rb
|
|
292
304
|
- lib/crono_trigger/events.rb
|
|
@@ -353,7 +365,6 @@ homepage: https://github.com/joker1007/crono_trigger
|
|
|
353
365
|
licenses:
|
|
354
366
|
- MIT
|
|
355
367
|
metadata: {}
|
|
356
|
-
post_install_message:
|
|
357
368
|
rdoc_options: []
|
|
358
369
|
require_paths:
|
|
359
370
|
- lib
|
|
@@ -368,8 +379,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
368
379
|
- !ruby/object:Gem::Version
|
|
369
380
|
version: '0'
|
|
370
381
|
requirements: []
|
|
371
|
-
rubygems_version:
|
|
372
|
-
signing_key:
|
|
382
|
+
rubygems_version: 4.0.20
|
|
373
383
|
specification_version: 4
|
|
374
384
|
summary: In Service Asynchronous Job Scheduler for Rails
|
|
375
385
|
test_files: []
|