kick_ahead 0.1.0 → 0.1.1
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 +5 -5
- data/.travis.yml +1 -2
- data/README.md +108 -159
- data/lib/kick_ahead/ar_repository.rb +32 -0
- data/lib/kick_ahead/version.rb +1 -1
- data/lib/kick_ahead.rb +8 -2
- metadata +4 -5
- data/bin/console +0 -14
- data/bin/setup +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4f3d6ee75adfef16b3bf5c823251802bae3a24ca120e7cce5d14b116ba2307de
|
4
|
+
data.tar.gz: 1aa641fb0dc11ba8306c22eaf6026d5ba0fb48eae9ea02ef6fd3d00408cc9e9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b21b43e59454a76721d0151ba1944ecae8d92dc12b7ebeb5ec6c0c804ab937727de98184ce29b064a349f89d13954137b889cae3c0a12d0eb8420b3d5a4f980c
|
7
|
+
data.tar.gz: a34683eed64f9879f1c17c651c4d7ba76c24850a078f4f2136f3d3142bbcaf41b7097637c9aae4560c48e75dbec9f84a2129d435c380cb2ccdafd5e80b943245
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# Kick Ahead
|
2
2
|
|
3
|
-
|
4
|
-
sidekiq has:
|
3
|
+
Kick Ahead allows you to push code to be executed in the future. The code is organized in Jobs with a simple API:
|
5
4
|
|
6
5
|
```ruby
|
7
6
|
class MyJob < KickAhead::Job
|
@@ -9,65 +8,130 @@ class MyJob < KickAhead::Job
|
|
9
8
|
# Your code
|
10
9
|
end
|
11
10
|
end
|
12
|
-
```
|
13
11
|
|
14
|
-
|
15
|
-
MyJob.
|
16
|
-
MyJob.run_at Time.now + 2 * 3600, "some", "args"
|
12
|
+
MyJob.run_in 1.hour, "some", "args"
|
13
|
+
MyJob.run_at 2.hours.from_now, "some_args"
|
17
14
|
```
|
18
15
|
|
19
|
-
This library is minimalist and
|
20
|
-
|
16
|
+
This library is minimalist and has no gem dependencies. However, in order to accomplish its task it requires
|
17
|
+
the host app to provide certain features which this lib depends upon. By expressing dependencies in this
|
18
|
+
way instead of by "hardcoded" gem dependencies, the user is free to reuse their existing solutions to common
|
19
|
+
problems.
|
20
|
+
|
21
|
+
|
22
|
+
## Satisfying requirements
|
23
|
+
|
24
|
+
We'll see first what requirements must be satisfied by the app.
|
25
|
+
|
26
|
+
|
27
|
+
### A Regular clock
|
21
28
|
|
29
|
+
The most important thing is to have a regular clock available, which allows you to call code at regular intervals.
|
22
30
|
|
23
|
-
|
31
|
+
Possible solutions to this problem are "plugins" for background systems (like sidekiq-cron for sidekiq),
|
32
|
+
the traditional cron system in a server, or maybe having an independent process control times with a loop manually
|
33
|
+
or with rufus-scheduler.
|
24
34
|
|
25
|
-
|
35
|
+
Once you have a regular clock, you must use it to call `KickAhead.tick`. Also, the interval between your clock ticks
|
36
|
+
must be specified in `KickAhead.tick_interval` in seconds, for example:
|
37
|
+
|
38
|
+
`KickAhead.tick_interval = 3600 # every hour`
|
39
|
+
|
40
|
+
Additionally, you must make sure that no two `KickAhead.tick` can be running at the same time across you entire
|
41
|
+
app (maybe across different servers).
|
42
|
+
|
43
|
+
### A Persistence repository
|
44
|
+
|
45
|
+
Next you'll have to provide a persistence layer to store jobs, as a Repository object. A repository is any
|
46
|
+
object that responds to the following methods and signatures:
|
47
|
+
|
48
|
+
- `create(klass, schedule_at, *args)`: Used to create a new job. `klass` is a string representing the
|
49
|
+
job class, `schedule_at` is a datetime representing the moment in time when this is expected to be executed,
|
50
|
+
and `*args` is the list of arguments.
|
51
|
+
|
52
|
+
This method is expected to return an identifier as a string, whatever you want that to be, so that it can be
|
53
|
+
used in the future to reference this job in the persistence repository.
|
54
|
+
|
55
|
+
- `each_job_in_the_past`: Returns a collection of job objects. In no particular order, but always
|
56
|
+
jobs that are in the past respect current time.
|
57
|
+
|
58
|
+
A job object is a hash with the following properties, example:
|
26
59
|
|
27
60
|
```ruby
|
28
|
-
|
29
|
-
|
61
|
+
{
|
62
|
+
id: "11",
|
63
|
+
job_class: "MyJob",
|
64
|
+
scheduled_at: Time.new(2017, 1, 1, 1, 1, 1),
|
65
|
+
job_args: [1, "foo"]
|
66
|
+
}
|
67
|
+
```
|
30
68
|
|
31
|
-
|
69
|
+
* id: The identifier you gave to the job
|
70
|
+
* job_class: same first argument of the `create` call.
|
71
|
+
* scheduled_at: same second argument of the `create` call.
|
72
|
+
* job_args: same third argument of the `create` call.
|
32
73
|
|
33
|
-
|
74
|
+
- `delete(id)`: Used to remove a job from the persistent storage. The given "id" is the identifier of the
|
75
|
+
job as returned by the `create` or `each_job_in_the_past` methods.
|
34
76
|
|
35
|
-
|
77
|
+
If you want to use an Active Record model for persistence, you can get an already made repository with this:
|
36
78
|
|
37
|
-
|
79
|
+
```ruby
|
80
|
+
repository = KickAhead.create_active_record_repository_for(KickAheadJob)
|
81
|
+
```
|
38
82
|
|
39
|
-
|
83
|
+
The table structure must be as follows, taken from a rails schema (note you can name the table and the
|
84
|
+
model whatever you like):
|
40
85
|
|
86
|
+
create_table "kick_ahead_jobs", force: :cascade do |t|
|
87
|
+
t.string "job_class", null: false
|
88
|
+
t.jsonb "job_args", null: false
|
89
|
+
t.datetime "scheduled_at", null: false
|
90
|
+
t.index ["scheduled_at"], name: "index_kick_ahead_jobs_on_scheduled_at"
|
91
|
+
end
|
41
92
|
|
42
|
-
First, you need to provide a polling mechanism that allows you to call `KickAhead.tick` at regular intervals.
|
43
|
-
This interval must be also configured at `KickAhead.tick_interval`. You application can call `tick` more
|
44
|
-
frequently than what you establish here, but never less frequently. If your polling source is not accurate
|
45
|
-
but have a predictable behavior / margin of error, configure the `tick_interval` to be your maximum possible
|
46
|
-
frequency even if your real polling source frequency is usually higher.
|
47
93
|
|
48
|
-
|
94
|
+
### Current time
|
95
|
+
|
96
|
+
Finally, since this library is heavily based on time, it cannot make any assumption about how are you managing
|
97
|
+
time in your application. Ruby's default behavior is to return the system time, but for a distributed
|
98
|
+
application that may run in different machines this is usually not desirable, and instead you might use
|
99
|
+
some other way to get the current time (like rails `Time.current`).
|
49
100
|
|
50
|
-
|
51
|
-
|
52
|
-
if you only have one process that may tick in your application, or some other application-wide lock mechanism
|
53
|
-
otherwise (i.e. using redis or postgres advisory locks).
|
101
|
+
Since this is a choice of the host app, you must also configure how KickAhead should get the current time by
|
102
|
+
providing a lambda to return it.
|
54
103
|
|
55
|
-
|
104
|
+
```ruby
|
105
|
+
KickAhead.current_time = -> { Time.current }
|
106
|
+
```
|
107
|
+
|
108
|
+
This way you can control what is considered to be the current time, and then make sure that this value is consistent
|
109
|
+
with the `each_job_in_the_past` method in the repository, so time comparisons work as expected.
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
## Usage
|
56
114
|
|
57
115
|
The basic idea is that you write code in Jobs, and then "push" those jobs to be executed at some point in the
|
58
116
|
future. Examples:
|
59
117
|
|
60
118
|
```ruby
|
61
|
-
MyJob
|
62
|
-
|
119
|
+
class MyJob < KickAhead::Job
|
120
|
+
def perform(some, args)
|
121
|
+
# Your code
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
MyJob.run_in 1.hour, "some", "args"
|
126
|
+
MyJob.run_at 2.hours.from_now, "some_args"
|
63
127
|
```
|
64
128
|
|
65
129
|
This lib guarantees that your job will be executed between your given time and your given time + your configured
|
66
|
-
tick_interval at most. If you want more precision, you'll need to
|
130
|
+
tick_interval at most. If you want more precision, you'll need to reduce your tick_interval.
|
67
131
|
|
68
|
-
If, for some reason, your
|
132
|
+
If, for some reason, your clock fails and the `tick` method is not called for a long time, any job that
|
69
133
|
was configured to run during that time will not run as expected. On the next tick, we'll detect those
|
70
|
-
stale jobs and then the following
|
134
|
+
stale jobs and then the following logic will apply:
|
71
135
|
|
72
136
|
If the job is configured with a `tolerance` value, and we're still inside the tolerance period, the job will
|
73
137
|
still run normally.
|
@@ -89,161 +153,46 @@ Or externally as well:
|
|
89
153
|
`MyJob.tolerance = 2.hours`
|
90
154
|
|
91
155
|
If tolerance is not satisfied, then the behavior depends on the `out_of_time_strategy` configured. This
|
92
|
-
can be configured on a per job basis and by default is
|
156
|
+
can be configured on a per job basis and by default is `raise_exception`. The options are:
|
93
157
|
|
94
158
|
- `raise_exception`: An exception will be raised. The job is kept in the repository, waiting for an external
|
95
|
-
action to correct the situation (remove the job,
|
159
|
+
action to correct the situation (remove the job, run it, or fix the situation in some other way). The exception gives
|
96
160
|
information about the job class and arguments. No further jobs will be executed until this is corrected.
|
97
161
|
|
98
162
|
- `ignore`: The out of time jobs will be simply deleted with no execution.
|
99
163
|
|
100
164
|
- `hook`: In this case, the method `out_of_time_hook` will be called on the job instance in the same way
|
101
|
-
the `perform` method would, giving you the
|
165
|
+
the `perform` method would, giving you the chance to do specific logics. The first argument, however, will be the
|
102
166
|
original scheduling time, so you can perform comparisons with this information.
|
103
167
|
|
104
168
|
Configure it with:
|
105
169
|
|
106
170
|
```ruby
|
107
171
|
class MyJob < KickAhead::Job
|
108
|
-
|
172
|
+
self.out_of_time_strategy = :raise_exception
|
109
173
|
# self.out_of_time_strategy = :ignore
|
110
|
-
self.out_of_time_strategy = :hook
|
174
|
+
# self.out_of_time_strategy = :hook
|
111
175
|
|
112
176
|
def perform(some, args)
|
113
177
|
# Your code
|
114
178
|
end
|
115
179
|
|
116
180
|
def out_of_time_hook(scheduled_at, some, args)
|
117
|
-
# Your code
|
181
|
+
# Your code
|
118
182
|
end
|
119
183
|
end
|
120
184
|
```
|
121
185
|
|
122
186
|
In case an exception occurs inside your Job, nothing extraordinary will happen. Kick Ahead will not capture
|
123
|
-
the exception, any other possible
|
124
|
-
|
125
|
-
Your jobs are expected to be quick. If a heavy work has to be done, delegate it to the background.
|
126
|
-
|
127
|
-
|
128
|
-
## Configuration
|
129
|
-
|
130
|
-
### Polling interval
|
187
|
+
the exception, any other possible job will not be executed.
|
131
188
|
|
132
|
-
`KickAhead.tick_interval = 3600`
|
133
|
-
|
134
|
-
This value must be set to the expected polling interval, in seconds. In the example, the polling frequency
|
135
|
-
is 1 hour. Your code is then expected to call:
|
136
|
-
|
137
|
-
`KickAhead.tick`
|
138
|
-
|
139
|
-
every hour.
|
140
|
-
|
141
|
-
|
142
|
-
### Repository
|
143
|
-
|
144
|
-
You're also expected to provide a repository to implement persistence over the data used to store jobs.
|
145
|
-
|
146
|
-
A repository is any object that responds to the following methods:
|
147
|
-
|
148
|
-
- `create(klass, schedule_at, *args)`: Used to create a new job. `klass` is a string representing the
|
149
|
-
job class, `schedule_at` is a datetime representing the moment in time when this is expected to be executed,
|
150
|
-
and `*args` is an expandable list of arguments (you can use json columns in postgres to store those easily).
|
151
|
-
|
152
|
-
This method is expected to return an identifier as a string, whatever you want that to be, so that it can be
|
153
|
-
used in the future to reference this job in the persistent repository.
|
154
|
-
|
155
|
-
- `each_job_in_the_past`: Returns a collection of job objects (hashes). In no particular order, but always
|
156
|
-
jobs that are in the past respect current time. Kick Ahead will then either execute or discard them depending
|
157
|
-
on the configurations.
|
158
|
-
|
159
|
-
A job is a hash with the following properties, example:
|
160
|
-
|
161
|
-
```ruby
|
162
|
-
{
|
163
|
-
id: "11",
|
164
|
-
job_class: "MyJob",
|
165
|
-
job_args: [1, "foo"],
|
166
|
-
scheduled_at: Time.new(2017, 1, 1, 1, 1, 1)
|
167
|
-
}
|
168
|
-
```
|
169
|
-
|
170
|
-
- id: The identifier you gave to the job
|
171
|
-
- job_class: same first argument of the `create` call.
|
172
|
-
- job_args: same third argument of the `create` call.
|
173
|
-
- scheduled_at: same second argument of the `create` call.
|
174
|
-
|
175
|
-
- `delete(id)`: Used to remove a job from the persistent storage. The given "id" is the identifier of the
|
176
|
-
job as returned by the `create` or `each_job_in_the_past` methods.
|
177
|
-
|
178
|
-
|
179
|
-
### Current time
|
180
|
-
|
181
|
-
Since this library is heavily based on time, it cannot make any assumption about how are you managing
|
182
|
-
the time in your application. Ruby's default behavior is to return the system time, but for a distributed
|
183
|
-
application that may run in different machines this is usually not desirable, and instead you should instead use
|
184
|
-
some other way to get the current time (i.e. rails `Time.current`).
|
185
|
-
|
186
|
-
Since this is a choice of the host app, you must also configure how KickAhead should get the current time by
|
187
|
-
providing a lambda to return it.
|
188
|
-
|
189
|
-
```ruby
|
190
|
-
KickAhead.current_time = -> { Time.current }
|
191
|
-
```
|
192
|
-
|
193
|
-
This way you can control what is considered to be the current time, and then make sure that this value is consistent
|
194
|
-
with the `each_job_in_the_past` method in the repository, so time comparisons work as expected.
|
195
|
-
|
196
|
-
|
197
|
-
### Repository example with ActiveRecord
|
198
|
-
|
199
|
-
```ruby
|
200
|
-
# create_table "kick_ahead_jobs", force: :cascade do |t|
|
201
|
-
# t.string "job_class", null: false
|
202
|
-
# t.jsonb "job_args", null: false
|
203
|
-
# t.datetime "scheduled_at", null: false
|
204
|
-
# t.index ["scheduled_at"], name: "index_kick_ahead_jobs_on_scheduled_at"
|
205
|
-
# end
|
206
|
-
class KickAheadJob < ActiveRecord::Base
|
207
|
-
end
|
208
|
-
|
209
|
-
module RepositoryExample
|
210
|
-
extend self
|
211
|
-
|
212
|
-
def each_job_in_the_past
|
213
|
-
KickAheadJob.where('scheduled_at <= ?', Time.current).find_each do |job|
|
214
|
-
yield(as_hash(job))
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def create(klass, schedule_at, *args)
|
219
|
-
job = KickAheadJob.create! job_class: klass, job_args: args, scheduled_at: schedule_at
|
220
|
-
job.id
|
221
|
-
end
|
222
|
-
|
223
|
-
def delete(id)
|
224
|
-
KickAheadJob.find(id).delete
|
225
|
-
end
|
226
|
-
|
227
|
-
private
|
228
|
-
|
229
|
-
def as_hash(job)
|
230
|
-
{
|
231
|
-
id: job.id,
|
232
|
-
job_class: job.job_class,
|
233
|
-
job_args: job.job_args,
|
234
|
-
scheduled_at: job.scheduled_at
|
235
|
-
}
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
```
|
240
189
|
|
241
190
|
## FAQS
|
242
191
|
|
243
|
-
Q: What If I don't care about jobs executing out of time? I want the job to execute after time X, but after that, I
|
244
|
-
don't need to be specific
|
192
|
+
- Q: What If I don't care about jobs executing out of time? I want the job to execute after time X, but after that, I
|
193
|
+
don't need to be specific.
|
245
194
|
|
246
|
-
A: You can set the tolerance value to an
|
195
|
+
A: You can set the tolerance value to an very high value (ie: 300 years).
|
247
196
|
|
248
197
|
|
249
198
|
## Development
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class ArRepository
|
2
|
+
def initialize(klass)
|
3
|
+
@klass = klass
|
4
|
+
end
|
5
|
+
|
6
|
+
def each_job_in_the_past
|
7
|
+
@klass.where('scheduled_at <= ?', KickAhead.current_time.call).find_each do |job|
|
8
|
+
yield(as_hash(job))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def create(job_class, schedule_at, *args)
|
13
|
+
job = @klass.create! job_class: job_class, job_args: args, scheduled_at: schedule_at
|
14
|
+
job.id
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(id)
|
18
|
+
raise 'Not possible!' if id.nil?
|
19
|
+
@klass.find(id).delete
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def as_hash(job)
|
25
|
+
{
|
26
|
+
id: job.id,
|
27
|
+
job_class: job.job_class,
|
28
|
+
job_args: job.job_args,
|
29
|
+
scheduled_at: job.scheduled_at
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
data/lib/kick_ahead/version.rb
CHANGED
data/lib/kick_ahead.rb
CHANGED
@@ -2,16 +2,18 @@
|
|
2
2
|
|
3
3
|
require "kick_ahead/version"
|
4
4
|
require "kick_ahead/job"
|
5
|
+
require "kick_ahead/ar_repository"
|
5
6
|
|
6
7
|
module KickAhead
|
7
8
|
OutOfInterval = Class.new(StandardError)
|
8
9
|
NoTickIntervalConfigured = Class.new(RuntimeError)
|
10
|
+
NoCurrentTimeConfigured = Class.new(RuntimeError)
|
9
11
|
|
10
12
|
RAISE_EXCEPTION_STRATEGY = :raise_exception
|
11
13
|
IGNORE_STRATEGY = :ignore
|
12
14
|
HOOK_STRATEGY = :hook
|
13
15
|
|
14
|
-
|
16
|
+
STRATEGIES = [RAISE_EXCEPTION_STRATEGY, IGNORE_STRATEGY, HOOK_STRATEGY].freeze
|
15
17
|
|
16
18
|
class << self
|
17
19
|
attr_accessor :tick_interval
|
@@ -24,7 +26,7 @@ module KickAhead
|
|
24
26
|
end
|
25
27
|
|
26
28
|
if current_time.nil?
|
27
|
-
raise 'You must configure a
|
29
|
+
raise NoCurrentTimeConfigured, 'You must configure a current_time lambda (i.e.: KickAhead.current_time = -> { Time.current })'
|
28
30
|
end
|
29
31
|
|
30
32
|
KickAhead.repository.each_job_in_the_past do |job|
|
@@ -61,6 +63,10 @@ module KickAhead
|
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
66
|
+
def create_active_record_repository_for(ar_model)
|
67
|
+
ArRepository.new(ar_model)
|
68
|
+
end
|
69
|
+
|
64
70
|
private
|
65
71
|
|
66
72
|
# Extracted from activesupport/lib/active_support/inflector/methods.rb, line 258
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kick_ahead
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roger Campos
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -79,10 +79,9 @@ files:
|
|
79
79
|
- Gemfile.lock
|
80
80
|
- README.md
|
81
81
|
- Rakefile
|
82
|
-
- bin/console
|
83
|
-
- bin/setup
|
84
82
|
- kick_ahead.gemspec
|
85
83
|
- lib/kick_ahead.rb
|
84
|
+
- lib/kick_ahead/ar_repository.rb
|
86
85
|
- lib/kick_ahead/job.rb
|
87
86
|
- lib/kick_ahead/version.rb
|
88
87
|
homepage: https://github.com/rogercampos/kick_ahead
|
@@ -105,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
104
|
version: '0'
|
106
105
|
requirements: []
|
107
106
|
rubyforge_project:
|
108
|
-
rubygems_version: 2.
|
107
|
+
rubygems_version: 2.7.3
|
109
108
|
signing_key:
|
110
109
|
specification_version: 4
|
111
110
|
summary: Push code to execute in the future, without dependencies
|
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "kick_ahead"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|