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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 59e2d5091b3c8f9f670398b61d29f1622316acb8
4
- data.tar.gz: c55ea15fbd789c84a2a4e92a01e7619f4359c69c
2
+ SHA256:
3
+ metadata.gz: 4f3d6ee75adfef16b3bf5c823251802bae3a24ca120e7cce5d14b116ba2307de
4
+ data.tar.gz: 1aa641fb0dc11ba8306c22eaf6026d5ba0fb48eae9ea02ef6fd3d00408cc9e9d
5
5
  SHA512:
6
- metadata.gz: 8da428f05bc52542d04eff590ac7189318c3788544945fdcce98cd66c3a45a8425dd2889816eb3b846ec0572c39020a6318c6682eb9debccfbd046e339a237f7
7
- data.tar.gz: 4399934d5ea49829ba721e45acbf5be03f023e98ba4929c8b94d54794871f69ad6119d111f8eef306c4b9663b68787b65bd163f74029ffa438eee3c316685dec
6
+ metadata.gz: b21b43e59454a76721d0151ba1944ecae8d92dc12b7ebeb5ec6c0c804ab937727de98184ce29b064a349f89d13954137b889cae3c0a12d0eb8420b3d5a4f980c
7
+ data.tar.gz: a34683eed64f9879f1c17c651c4d7ba76c24850a078f4f2136f3d3142bbcaf41b7097637c9aae4560c48e75dbec9f84a2129d435c380cb2ccdafd5e80b943245
data/.travis.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.1.10
5
- - 2.2.8
6
4
  - 2.3.4
7
5
  - 2.4.2
6
+ - 2.5.0
8
7
  before_install: gem install bundler -v 1.16.0
data/README.md CHANGED
@@ -1,7 +1,6 @@
1
- # KickAhead
1
+ # Kick Ahead
2
2
 
3
- Allows you to push code to be executed in the future. The code is organized in Jobs with the same API
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
- ```ruby
15
- MyJob.run_in 3600, "some", "args"
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 the user must provide a persistent storage system as well as a polling
20
- mechanism, that will be used to control the timings.
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
- ## Installation
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
- Add this line to your application's Gemfile:
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
- gem 'kick_ahead'
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
- And then execute:
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
- $ bundle
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
- Or install it yourself as:
77
+ If you want to use an Active Record model for persistence, you can get an already made repository with this:
36
78
 
37
- $ gem install kick_ahead
79
+ ```ruby
80
+ repository = KickAhead.create_active_record_repository_for(KickAheadJob)
81
+ ```
38
82
 
39
- ## Usage
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
- If you fail to call `tick` frequently enough, you may have dropped jobs (see below).
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
- Also importantly, your call to `tick` must be atomic in your application. Only one `tick` must be in execution
51
- at any given time. You must use some sort of locking mechanism to ensure this, like ruby's `Mutex#synchronize`
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
- You'll also need to provide a Repository object, to offer a persistent storage (see below in Configuration).
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.run_in 3600, "some", "args"
62
- MyJob.run_at Time.now + 2 * 3600, "some_args"
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 decrease your tick_interval.
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 polling fail and the `tick` method is not called for a long time, any job that
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 may occur:
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 "raise_exception". The options are:
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, change it's schedule time, etc.). The exception gives
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 change to do specific logics. The first argument, however, will be 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
- # self.out_of_time_strategy = :raise_exception
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 when out of time
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 jobs will not be processed.
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 (ej: fail if the time doesn't fit).
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 incredible high value (ie: 300 years).
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
@@ -1,3 +1,3 @@
1
1
  module KickAhead
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
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
- ALL_STRATEGIES = [RAISE_EXCEPTION_STRATEGY, IGNORE_STRATEGY, HOOK_STRATEGY].freeze
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 way for me to know the current time!'
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.0
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: 2017-11-29 00:00:00.000000000 Z
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.5.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__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here