kick_ahead 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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__)
|