dispatch-rider 1.6.2 → 1.7.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/.gitignore +1 -0
- data/README.md +55 -0
- data/dispatch-rider.gemspec +3 -0
- data/lib/dispatch-rider.rb +1 -0
- data/lib/dispatch-rider/publisher/base.rb +14 -1
- data/lib/dispatch-rider/scheduled_job.rb +59 -0
- data/lib/dispatch-rider/scheduled_job/migration.rb +15 -0
- data/lib/dispatch-rider/version.rb +1 -1
- data/spec/lib/dispatch-rider/scheduled_job_spec.rb +73 -0
- data/spec/spec_helper.rb +18 -0
- metadata +34 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ceff36f00f29c392c12751a5b9be98ded76af731
|
4
|
+
data.tar.gz: 4252fb5a29899475e8dd465cd9d9073a81c8bd4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f1f68c73b69964a68b0222fc02319e0f954d380af74a255d5eb69c15ca48c3c6374a0d14f29a81f41f7be664edf43cf785f223d8b2f4b1b0f853246242011bb
|
7
|
+
data.tar.gz: cf671844773968e6930f9e2ff3245903e658c04bbf40f2c41eac2a01b7ca6b7f2ce8c4dad9c6a77042163897800ec777c2ec4620c302303b7814788e0e7c179c
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -448,6 +448,61 @@ DispatchRider.config do |config|
|
|
448
448
|
config.error_handler = DispatchRider::AirbrakeErrorHandler
|
449
449
|
end
|
450
450
|
```
|
451
|
+
## Scheduling Job
|
452
|
+
|
453
|
+
Scheduled jobs currently require `ActiveRecord`. Support for other ORM will be added in the future depending on demand.
|
454
|
+
|
455
|
+
1. Add and run the DB migration below:
|
456
|
+
```ruby
|
457
|
+
class CreateScheduledJobsTable < ActiveRecord::Migration
|
458
|
+
include DispatchRider::ScheduledJob::Migration
|
459
|
+
|
460
|
+
def change
|
461
|
+
create_scheduled_jobs_table
|
462
|
+
end
|
463
|
+
end
|
464
|
+
```
|
465
|
+
|
466
|
+
2. Schedule jobs:
|
467
|
+
```ruby
|
468
|
+
# Use `#publish_later`
|
469
|
+
class NewsPublisher < DispatchRider::Publisher::Base
|
470
|
+
destinations :sns_message_queue
|
471
|
+
subject "read_news"
|
472
|
+
|
473
|
+
def self.midnight_publish(news)
|
474
|
+
new.publish_later("headlines" => news.headlines, at: Date.tomorrow.midnight)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
# Or create a scheduled job manually
|
479
|
+
DispatchRider::ScheduledJob.create! scheduled_at: Date.tomorrow.midnight,
|
480
|
+
destinations: [:sns_message_queue],
|
481
|
+
message: {
|
482
|
+
subject: "read_news",
|
483
|
+
body: { "headlines" => news.headlines }
|
484
|
+
}
|
485
|
+
```
|
486
|
+
|
487
|
+
3. Run scheduled publishing.
|
488
|
+
```ruby
|
489
|
+
# Run once
|
490
|
+
|
491
|
+
# Ideally run on a cron. Where the cron is responsible for the publishing
|
492
|
+
# frequency. Any jobs due at the time this is run by the cron will be
|
493
|
+
# published.
|
494
|
+
DispatchRider::ScheduledJob.publish_due_jobs
|
495
|
+
|
496
|
+
# Loop. Warning: Loops are blocking. Run this on a separate thread if it's not
|
497
|
+
# the sole purpose of the app.
|
498
|
+
|
499
|
+
# publish every minute
|
500
|
+
DispatchRider::ScheduledJob.publish_due_jobs every: 1.minute
|
501
|
+
|
502
|
+
# publish every half an hour
|
503
|
+
DispatchRider::ScheduledJob.publish_due_jobs every: 30.minutes
|
504
|
+
```
|
505
|
+
|
451
506
|
## Deployment
|
452
507
|
|
453
508
|
In order to deploy a new version of the gem into the wild ...
|
data/dispatch-rider.gemspec
CHANGED
@@ -41,6 +41,9 @@ Gem::Specification.new do |gem|
|
|
41
41
|
|
42
42
|
gem.add_runtime_dependency 'activesupport', ">= 3.2.0"
|
43
43
|
gem.add_runtime_dependency 'activemodel', ">= 3.2.0"
|
44
|
+
gem.add_runtime_dependency "activerecord", ">= 3.2.0"
|
44
45
|
gem.add_runtime_dependency 'daemons', "~> 1.2"
|
45
46
|
gem.add_runtime_dependency 'retries', "~> 0.0", ">= 0.0.5"
|
47
|
+
|
48
|
+
gem.add_development_dependency "sqlite3"
|
46
49
|
end
|
data/lib/dispatch-rider.rb
CHANGED
@@ -30,10 +30,19 @@ module DispatchRider
|
|
30
30
|
|
31
31
|
# @param [Hash] body
|
32
32
|
def publish(body)
|
33
|
-
|
33
|
+
validate_body(body)
|
34
34
|
publisher.publish(destinations: destinations, message: { subject: subject, body: body })
|
35
35
|
end
|
36
36
|
|
37
|
+
# @param [Hash] body
|
38
|
+
# @param [Time] at
|
39
|
+
def publish_later(body, at:)
|
40
|
+
validate_body(body)
|
41
|
+
DispatchRider::ScheduledJob.create! scheduled_at: at,
|
42
|
+
destinations: destinations,
|
43
|
+
message: { subject: subject, body: body }
|
44
|
+
end
|
45
|
+
|
37
46
|
private
|
38
47
|
|
39
48
|
def publisher
|
@@ -47,5 +56,9 @@ module DispatchRider
|
|
47
56
|
def subject
|
48
57
|
self.class.instance_variable_get(:@subject)
|
49
58
|
end
|
59
|
+
|
60
|
+
def validate_body(body)
|
61
|
+
raise ArgumentError, 'body should be a hash' unless body.is_a?(Hash)
|
62
|
+
end
|
50
63
|
end
|
51
64
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "active_record"
|
2
|
+
|
3
|
+
# @note: Later this could be pulled out to its own gem and included depending on what ORM the user
|
4
|
+
# prefers. Something like:
|
5
|
+
# @example
|
6
|
+
# gem "dispatch_rider-active_record"
|
7
|
+
# gem "dispatch_rider-rom" # Ruby Object Mapper
|
8
|
+
# gem "dispatch_rider-mongo_mapper"
|
9
|
+
# gem "dispatch_rider-ohm"
|
10
|
+
module DispatchRider
|
11
|
+
class ScheduledJob < ActiveRecord::Base
|
12
|
+
class << self
|
13
|
+
def publisher
|
14
|
+
@publisher ||= Publisher.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [ActiveSupport::Duration] every
|
18
|
+
# @example: DispatchRider::ScheduledJob.publish_due_jobs every 1.minute
|
19
|
+
def publish_due_jobs(every: nil)
|
20
|
+
loop {
|
21
|
+
claim_stub = get_new_claim_stub
|
22
|
+
due.unclaimed.update_all claim_stub
|
23
|
+
due.claimed_by(claim_stub[:claim_id]).find_each(&:publish)
|
24
|
+
every ? sleep(every) : break # until the next loop
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def get_new_claim_stub
|
31
|
+
{ claim_id: SecureRandom.uuid, claim_expires_at: 30.minutes.from_now }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
serialize :destinations
|
36
|
+
serialize :message
|
37
|
+
|
38
|
+
validates :scheduled_at,
|
39
|
+
:destinations,
|
40
|
+
:message,
|
41
|
+
presence: true
|
42
|
+
|
43
|
+
scope :due, -> (time = Time.now) { where "scheduled_at <= ?", time }
|
44
|
+
scope :claimed_by, -> (claim_id) { where(claim_id: claim_id).where "claim_expires_at > ?", Time.now }
|
45
|
+
scope :unclaimed, -> { where "claim_expires_at IS NULL OR claim_expires_at <= ?", Time.now }
|
46
|
+
|
47
|
+
def publish
|
48
|
+
publisher.publish(destinations: destinations, message: message)
|
49
|
+
|
50
|
+
destroy # once published
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
delegate :publisher, to: :"self.class"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
require_relative "scheduled_job/migration"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DispatchRider
|
2
|
+
module ScheduledJob::Migration
|
3
|
+
def create_scheduled_jobs_table
|
4
|
+
create_table :scheduled_jobs do |t|
|
5
|
+
t.datetime :scheduled_at
|
6
|
+
t.text :destinations
|
7
|
+
t.text :message
|
8
|
+
t.string :claim_id
|
9
|
+
t.datetime :claim_expires_at
|
10
|
+
|
11
|
+
t.index :scheduled_at
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DispatchRider::ScheduledJob do
|
4
|
+
let(:message_details) {
|
5
|
+
{
|
6
|
+
destinations: [:allied_forces],
|
7
|
+
message: {
|
8
|
+
subject: :war_update,
|
9
|
+
body: { enigma_machine: :broken }
|
10
|
+
}
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
let!(:due_job) { described_class.create! message_details.merge(scheduled_at: 1.minute.ago) }
|
15
|
+
let!(:later_job) { described_class.create! message_details.merge(scheduled_at: 30.minutes.since) }
|
16
|
+
|
17
|
+
describe ".due" do
|
18
|
+
describe "due now" do
|
19
|
+
subject(:due_jobs) { described_class.due }
|
20
|
+
|
21
|
+
it { expect(due_jobs).to include due_job }
|
22
|
+
it { expect(due_jobs).to_not include later_job }
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "due tomorrow" do
|
26
|
+
subject(:due_jobs) { described_class.due Date.tomorrow.midnight }
|
27
|
+
|
28
|
+
it { expect(due_jobs).to include due_job }
|
29
|
+
it { expect(due_jobs).to include later_job }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ".publish_due_jobs" do
|
34
|
+
example {
|
35
|
+
expect(described_class.publisher).to receive(:publish).once.with destinations: [:allied_forces],
|
36
|
+
message: {
|
37
|
+
subject: :war_update,
|
38
|
+
body: { enigma_machine: :broken }
|
39
|
+
}
|
40
|
+
|
41
|
+
2.times { described_class.publish_due_jobs }
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#destinations serialization" do
|
46
|
+
subject { described_class.find(due_job.id).destinations }
|
47
|
+
|
48
|
+
it { is_expected.to eq [:allied_forces] }
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#message serialization" do
|
52
|
+
subject { described_class.find(due_job.id).message }
|
53
|
+
|
54
|
+
it {
|
55
|
+
is_expected.to eq subject: :war_update,
|
56
|
+
body: { enigma_machine: :broken }
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#publish" do
|
61
|
+
subject(:job) { described_class.find(due_job.id) }
|
62
|
+
|
63
|
+
example {
|
64
|
+
expect(described_class.publisher).to receive(:publish).with destinations: [:allied_forces],
|
65
|
+
message: {
|
66
|
+
subject: :war_update,
|
67
|
+
body: { enigma_machine: :broken }
|
68
|
+
}
|
69
|
+
|
70
|
+
job.publish
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,6 +6,8 @@ Bundler.require
|
|
6
6
|
require 'aws'
|
7
7
|
require 'rake'
|
8
8
|
require 'tempfile'
|
9
|
+
require "sqlite3"
|
10
|
+
|
9
11
|
|
10
12
|
require 'dispatch-rider'
|
11
13
|
Dir['./spec/support/**/*.rb'].each { |fn| require(fn) }
|
@@ -25,10 +27,26 @@ RSpec.configure do |config|
|
|
25
27
|
|
26
28
|
config.include IntegrationSupport
|
27
29
|
|
30
|
+
config.before(:suite) do
|
31
|
+
FileUtils.mkdir_p "tmp"
|
32
|
+
FileUtils.rm_f "tmp/lite.db"
|
33
|
+
FileUtils.rm_rf "spectmp"
|
34
|
+
SQLite3::Database.new "tmp/lite.db"
|
35
|
+
ActiveRecord::Base.establish_connection adapter: :sqlite3, database: File.dirname(__FILE__) + "tmp/lite.db"
|
36
|
+
ActiveRecord::Schema.define(version: 1) do
|
37
|
+
extend DispatchRider::ScheduledJob::Migration
|
38
|
+
create_scheduled_jobs_table
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
28
42
|
config.before do
|
29
43
|
DispatchRider.config.logger = NullLogger.new
|
30
44
|
end
|
31
45
|
|
46
|
+
config.after do
|
47
|
+
DispatchRider::ScheduledJob.destroy_all
|
48
|
+
end
|
49
|
+
|
32
50
|
config.include FactoryGirl::Syntax::Methods
|
33
51
|
end
|
34
52
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dispatch-rider
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Suman Mukherjee
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2015-
|
14
|
+
date: 2015-07-17 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: activesupport
|
@@ -41,6 +41,20 @@ dependencies:
|
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: 3.2.0
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: activerecord
|
46
|
+
requirement: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 3.2.0
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 3.2.0
|
44
58
|
- !ruby/object:Gem::Dependency
|
45
59
|
name: daemons
|
46
60
|
requirement: !ruby/object:Gem::Requirement
|
@@ -75,6 +89,20 @@ dependencies:
|
|
75
89
|
- - ">="
|
76
90
|
- !ruby/object:Gem::Version
|
77
91
|
version: 0.0.5
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: sqlite3
|
94
|
+
requirement: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
78
106
|
description: "\n Messaging system based on the reactor pattern.\n\n You can
|
79
107
|
publish messages to a queue and then a demultiplexer\n runs an event loop which
|
80
108
|
pops items from the queue and hands\n it over to a dispatcher.\n\n The dispatcher
|
@@ -159,6 +187,8 @@ files:
|
|
159
187
|
- lib/dispatch-rider/registrars/queue_service.rb
|
160
188
|
- lib/dispatch-rider/registrars/sns_channel.rb
|
161
189
|
- lib/dispatch-rider/runner.rb
|
190
|
+
- lib/dispatch-rider/scheduled_job.rb
|
191
|
+
- lib/dispatch-rider/scheduled_job/migration.rb
|
162
192
|
- lib/dispatch-rider/subscriber.rb
|
163
193
|
- lib/dispatch-rider/version.rb
|
164
194
|
- lib/generators/dispatch_rider/install/USAGE
|
@@ -216,6 +246,7 @@ files:
|
|
216
246
|
- spec/lib/dispatch-rider/registrars/sns_channel_spec.rb
|
217
247
|
- spec/lib/dispatch-rider/registrars_spec.rb
|
218
248
|
- spec/lib/dispatch-rider/runner_spec.rb
|
249
|
+
- spec/lib/dispatch-rider/scheduled_job_spec.rb
|
219
250
|
- spec/lib/dispatch-rider/subscriber_spec.rb
|
220
251
|
- spec/lib/dispatch-rider_spec.rb
|
221
252
|
- spec/spec_helper.rb
|
@@ -295,6 +326,7 @@ test_files:
|
|
295
326
|
- spec/lib/dispatch-rider/registrars/sns_channel_spec.rb
|
296
327
|
- spec/lib/dispatch-rider/registrars_spec.rb
|
297
328
|
- spec/lib/dispatch-rider/runner_spec.rb
|
329
|
+
- spec/lib/dispatch-rider/scheduled_job_spec.rb
|
298
330
|
- spec/lib/dispatch-rider/subscriber_spec.rb
|
299
331
|
- spec/lib/dispatch-rider_spec.rb
|
300
332
|
- spec/spec_helper.rb
|