pushpop 0.2 → 0.3.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
2
  SHA1:
3
- metadata.gz: 79ea1e75806eb688afb2c241020437cef7e6f7b4
4
- data.tar.gz: 17ec6e91478668059c1ad092f2aa3abf141a432c
3
+ metadata.gz: d33b1c0d4e1f844e67237d4a11a2869fbf459974
4
+ data.tar.gz: f96dfd05743df179f3105cc1c514bff5996b2565
5
5
  SHA512:
6
- metadata.gz: ca946cc80d4bc63344fd341bcf0d1fbe63a8d8f3d0f46084f40c8aebda434bb943a4fbd64d36980efb08d2c9f6ae1ff299611dfc7f39cfbf4f52290f273a7549
7
- data.tar.gz: 4d3f0768e22c38c45713640cd4fe9a40547a6aa65fd743ebab2c98e503050a4725ee52b2fc5a63b0adb6cb071d9f1484ba382dc7f51fd3f2175dc684a05d16f7
6
+ metadata.gz: 809afc53fb27b310504568f9a4456a62c73b4d1c71fd73f5cecb60cceec53b414b8c3c5e6778d09211c8a51685713594aea773588a02c28cbe7484c9cf8c2ee0
7
+ data.tar.gz: 9a115b8c0a544ee3654ff2a3bd3f3a677d03e97b52281089e011b240833d62068a59af711a90af0f36b99be51f8ca1ca8da867194a8a62add7dbe4e28b9973cb
data/README.md CHANGED
@@ -196,6 +196,26 @@ every 1.week, at: 'Monday 12:30'
196
196
 
197
197
  See the full range of possibilities on the [Clockwork README](https://github.com/tomykaira/clockwork#event-parameters).
198
198
 
199
+ ##### Webhooks
200
+
201
+ Jobs can also be triggered via a webhook, instead of scheduling via `every`. Simply use `webhook` instead of `every`, and pass in a path that should trigger that job. `webhook` also accepts a block, which becomes the first step of the job.
202
+
203
+ ``` ruby
204
+ webhook '/trigger' do
205
+ if params[:secret] == '12345'
206
+ params[:name]
207
+ else
208
+ false # Returning false cancels the job
209
+ end
210
+ end
211
+ ```
212
+
213
+ The webhooks are simply a [Sinatra](http://www.sinatrarb.com) app under the hood, so you can reuse a lot of the features that are built-in to Sinatra. Their [routing features](http://www.sinatrarb.com/intro.html#Routes) work out of the box. The webhook block that gets called for every request is also run in the [Sinatra request scope](http://www.sinatrarb.com/intro.html#Request/Instance%20Scope), so you can access the full app (via `settings`), and request params via `params`.
214
+
215
+ The return value of the webhook block will be passed in as `response` for the first step, and will be stored under `step_responses['webhook']` for all future steps.
216
+
217
+ *You may want to read about running a [custom HTTP server](#custom-http-server-for-webhooks) if you're going to be using webhooks in production.*
218
+
199
219
  ##### Job workflow
200
220
 
201
221
  When a job kicks off, steps are run serially in the order they are specified. Each step is invoked with 2
@@ -217,7 +237,7 @@ job do
217
237
  end
218
238
  step 'add previous steps' do |response, step_responses|
219
239
  puts response # prints 6
220
- puts step_responses['one'] + step_responses['two'] # prints 6
240
+ puts step_responses['one'] + step_responses['two'] # prints 7
221
241
  end
222
242
  end
223
243
  ```
@@ -267,6 +287,7 @@ end
267
287
 
268
288
  In this example, the `twilio` step will only be ran if the `keen` step returned a count greater than 0.
269
289
 
290
+
270
291
  #### Steps
271
292
 
272
293
  Steps have the following attributes:
@@ -300,6 +321,43 @@ Here's a very simple template that uses the `response` variable in context:
300
321
  <p>We got <%= response %> new users today!</p>
301
322
  ```
302
323
 
324
+ ## Custom HTTP Server for Webhooks
325
+
326
+ If you're running Pushpop locally, you can continue to use the CLI for running jobs - `jobs:run` will start the Sinatra app internally if you have any webhooks configured. However, running the app with the CLI in a production environment may not scale well. If your webhooks are going to be hit rapidly in production, you may want to use a beefier HTTP server than the default [WEBrick](http://ruby-doc.org/stdlib-1.9.3/libdoc/webrick/rdoc/WEBrick.html) built in to Ruby.
327
+
328
+ Here's an example of getting Pushpop running on [Unicorn](http://unicorn.bogomips.org/)
329
+
330
+ **unicorn.rb**
331
+
332
+ ``` ruby
333
+ require 'pushpop'
334
+
335
+ # Set this to whatever you want.
336
+ worker_processes 2
337
+
338
+ # This loads all of the job files in /jobs
339
+ Pushpop.load_jobs
340
+
341
+ # This configures Clockwork for any scheduled jobs you have.
342
+ # You can omit this if all you are using is Webhooks
343
+ Pushpop.schedule
344
+
345
+ # This tells Clockwork to actually start running jobs
346
+ # You can omit this if all you are using is Webhooks
347
+ Pushpop.start_clock
348
+ ```
349
+
350
+ **config.ru**
351
+ ``` ruby
352
+ # This tells Unicorn what to run whenever it starts up a worker.. which is the Pushpop web app
353
+ run Pushpop.web.app
354
+ ```
355
+
356
+ And then to run it, just do:
357
+ ``` bash
358
+ unicorn -c unicorn.rb
359
+ ```
360
+
303
361
  ## Recipes
304
362
 
305
363
  The community-driven [pushpop-recipes](https://github.com/pushpop-project/pushpop-recipes) repository contains jobs and templates
@@ -4,6 +4,7 @@ require 'pushpop/version'
4
4
  require 'pushpop/job'
5
5
  require 'pushpop/step'
6
6
  require 'pushpop/cli'
7
+ require 'pushpop/web'
7
8
 
8
9
  module Pushpop
9
10
  class << self
@@ -30,6 +31,23 @@ module Pushpop
30
31
  @@jobs
31
32
  end
32
33
 
34
+ def web
35
+ @web ||= Web.new
36
+ end
37
+
38
+ def start_webserver
39
+ # If we start this thread with no routes, it will throw off the all_waits listener
40
+ # and we don't want to start the web server willy nilly, because it looks weird
41
+ # on the CLI interface
42
+ if web.routes.length > 0
43
+ Thread.new do
44
+ @web.app.run!
45
+ end
46
+ else
47
+ false
48
+ end
49
+ end
50
+
33
51
  # for jobs and steps
34
52
  def random_name
35
53
  (0...8).map { (65 + rand(26)).chr }.join
@@ -48,6 +66,29 @@ module Pushpop
48
66
  self.jobs.map &:schedule
49
67
  end
50
68
 
69
+ def start_clock
70
+ Thread.new do
71
+ Clockwork.manager.run
72
+ end
73
+ end
74
+
75
+ def require_file(file = nil)
76
+ if file
77
+ if File.directory?(file)
78
+ Dir.glob("#{file}/**/*.rb").each { |file|
79
+ load "#{Dir.pwd}/#{file}"
80
+ }
81
+ else
82
+ load file
83
+ end
84
+ else
85
+ Dir.glob("#{Dir.pwd}/jobs/**/*.rb").each { |file|
86
+ load file
87
+ }
88
+ end
89
+ end
90
+ alias :load_jobs :require_file
91
+
51
92
  def load_plugin(name)
52
93
  load "#{File.expand_path("../plugins/#{name}", __FILE__)}.rb"
53
94
  end
@@ -1,6 +1,7 @@
1
1
  require 'thor'
2
2
  require 'dotenv'
3
3
  require 'pushpop'
4
+ require 'thwait'
4
5
 
5
6
  module Pushpop
6
7
  class CLI < Thor
@@ -24,7 +25,7 @@ module Pushpop
24
25
 
25
26
  def describe_jobs
26
27
  Dotenv.load
27
- require_file(options[:file])
28
+ Pushpop.require_file(options[:file])
28
29
  Pushpop.jobs.tap do |jobs|
29
30
  jobs.each do |job|
30
31
  puts job.name
@@ -38,7 +39,7 @@ module Pushpop
38
39
 
39
40
  def run_jobs_once
40
41
  Dotenv.load
41
- require_file(options[:file])
42
+ Pushpop.require_file(options[:file])
42
43
  Pushpop.run
43
44
  end
44
45
 
@@ -48,29 +49,32 @@ module Pushpop
48
49
 
49
50
  def run_jobs
50
51
  Dotenv.load
51
- require_file(options[:file])
52
+ Pushpop.require_file(options[:file])
52
53
  Pushpop.schedule
53
- Clockwork.manager.run
54
- end
55
54
 
56
- private
55
+ threads = []
56
+ threads << Pushpop.start_clock
57
+
58
+ Pushpop.web.app.traps = false
59
+ web_thread = Pushpop.start_webserver
60
+ threads << web_thread if web_thread
57
61
 
58
- def require_file(file)
59
- if file
60
- if File.directory?(file)
61
- Dir.glob("#{file}/**/*.rb").each { |file|
62
- load "#{Dir.pwd}/#{file}"
63
- }
64
- else
65
- load file
62
+ # Listen to exit signals, so the CLI doesn't hang infinitely on clock
63
+ [:INT, :TERM].each do |signal|
64
+ trap(signal) do
65
+ threads.each do |thread|
66
+ thread.exit
67
+ end
66
68
  end
67
- else
68
- Dir.glob("#{Dir.pwd}/jobs/**/*.rb").each { |file|
69
- load file
70
- }
71
69
  end
72
- end
73
70
 
71
+ # Wait for both the clock thread and the sinatra thread to close before exiting
72
+ ThreadsWait.all_waits(threads) do
73
+ threads.each do |thread|
74
+ thread.exit
75
+ end
76
+ end
77
+ end
74
78
  end
75
79
  end
76
80
 
@@ -19,6 +19,8 @@ module Pushpop
19
19
 
20
20
  attr_accessor :name
21
21
  attr_accessor :period
22
+ attr_accessor :webhook_url
23
+ attr_accessor :webhook_proc
22
24
  attr_accessor :every_options
23
25
  attr_accessor :steps
24
26
 
@@ -34,6 +36,16 @@ module Pushpop
34
36
  self.every_options = options
35
37
  end
36
38
 
39
+ def webhook(url, &block)
40
+ raise 'Webhook is already set' if @webhook_url
41
+ raise 'Webhook must be set before steps' if self.steps.length > 0
42
+
43
+ self.webhook_url = url
44
+ self.webhook_proc = block
45
+
46
+ Pushpop.web.add_route url, self
47
+ end
48
+
37
49
  def step(name=nil, plugin=nil, &block)
38
50
  if plugin
39
51
 
@@ -51,18 +63,16 @@ module Pushpop
51
63
  end
52
64
 
53
65
  def schedule
54
- raise 'Set job period via "every"' unless self.period
55
- Clockwork.manager.every(period, name, every_options) do
56
- run
66
+ raise 'Set job period via "every"' unless self.period || @webhook_url
67
+
68
+ if self.period
69
+ Clockwork.manager.every(period, name, every_options) do
70
+ run
71
+ end
57
72
  end
58
73
  end
59
74
 
60
- def run
61
-
62
- # track the last response, and all responses
63
- last_response = nil
64
- step_responses = {}
65
-
75
+ def run(last_response = nil, step_responses = {})
66
76
  self.steps.each do |step|
67
77
 
68
78
  # track the last_response and all responses
@@ -4,7 +4,7 @@ module Pushpop
4
4
 
5
5
  class Step
6
6
 
7
- TEMPLATES_DIRECTORY = File.expand_path('../../../templates', __FILE__)
7
+ TEMPLATES_DIRECTORY = File.expand_path('templates', Dir.pwd)
8
8
 
9
9
  class ERBContext
10
10
  attr_accessor :response
@@ -1,3 +1,3 @@
1
1
  module Pushpop
2
- VERSION = '0.2'
2
+ VERSION = '0.3.1'
3
3
  end
@@ -0,0 +1,48 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+
4
+ module Pushpop
5
+ class Web
6
+
7
+ def app
8
+ Sinatra::Application
9
+ end
10
+
11
+ def routes
12
+ @routes ||= []
13
+ end
14
+
15
+ def add_route(url, job)
16
+
17
+ if url[0] != '/'
18
+ url = "/#{url}"
19
+ end
20
+
21
+ raise "Route #{url} is already set up as a webhook" if routes.include?(url)
22
+
23
+ runner = lambda do
24
+ response = self.instance_eval(&job.webhook_proc)
25
+
26
+ if response
27
+ {
28
+ status: 'success',
29
+ job: job.name
30
+ }.to_json
31
+ else
32
+ {
33
+ status: 'failed',
34
+ job: job.name,
35
+ message: 'webhook step did not pass'
36
+ }.to_json
37
+ end
38
+ end
39
+
40
+ Sinatra::Application.get url, &runner
41
+ Sinatra::Application.post url, &runner
42
+ Sinatra::Application.put url, &runner
43
+
44
+ routes.push(url)
45
+
46
+ end
47
+ end
48
+ end
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.add_dependency "clockwork"
16
16
  s.add_dependency "thor"
17
+ s.add_dependency "sinatra"
17
18
  s.add_dependency "dotenv"
18
19
 
19
20
  s.files = `git ls-files`.split("\n")
@@ -37,6 +37,29 @@ describe Pushpop::Job do
37
37
  end
38
38
  end
39
39
 
40
+ describe '#webhook' do
41
+ it 'sets the webhook url and proc' do
42
+ job = empty_job
43
+ empty_proc = Proc.new{}
44
+ job.webhook('/test', &empty_proc)
45
+ expect(job.webhook_url).to eq('/test')
46
+ expect(job.webhook_proc.class).to be(Proc)
47
+ end
48
+
49
+ it 'raises an error if webhook is already set' do
50
+ job = empty_job
51
+ job.webhook('/test1')
52
+ expect{ job.webhook('/test2') }.to raise_error
53
+ end
54
+
55
+ it 'raises an error if any steps have been created' do
56
+ job = empty_job
57
+ empty_proc = Proc.new{}
58
+ job.step('test', &empty_proc)
59
+ expect{job.webhook('/test')}.to raise_error
60
+ end
61
+ end
62
+
40
63
  describe '#step' do
41
64
  it 'adds the step to the internal list of steps' do
42
65
  empty_proc = Proc.new {}
@@ -108,11 +131,24 @@ describe Pushpop::Job do
108
131
  expect(simple_job.run.first).to eq(4)
109
132
  end
110
133
 
111
- it 'fails if the period was not specified' do
134
+ it 'fails if neither period nor webhook was not specified' do
112
135
  simple_job = Pushpop::Job.new('foo') do end
113
136
  expect {
114
137
  simple_job.schedule
115
138
  }.to raise_error
139
+
140
+ simple_job.period = 5.seconds
141
+
142
+ expect {
143
+ simple_job.schedule
144
+ }.not_to raise_error
145
+
146
+ simple_job.period = nil
147
+ simple_job.webhook_url = '/test'
148
+
149
+ expect {
150
+ simple_job.schedule
151
+ }.not_to raise_error
116
152
  end
117
153
  end
118
154
 
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pushpop::Web do
4
+ web = nil
5
+
6
+ before(:each) do
7
+ web = Pushpop::Web.new
8
+ end
9
+
10
+ it 'returns a Sinatra Application' do
11
+ expect(web.app).to equal(Sinatra::Application)
12
+ end
13
+
14
+ describe 'routes' do
15
+ it 'is an array' do
16
+ expect(web.routes.class).to equal(Array)
17
+ end
18
+
19
+ it 'is empty by default' do
20
+ expect(web.routes.length).to equal(0)
21
+ end
22
+
23
+ it 'gets filled with new routes' do
24
+ web.add_route('/test', Proc.new{})
25
+
26
+ expect(web.routes.length).to equal(1)
27
+ expect(web.routes[0]).to eq('/test')
28
+ end
29
+ end
30
+
31
+ describe 'add_route' do
32
+
33
+ before(:each) do
34
+ Sinatra::Application.reset!
35
+ end
36
+
37
+ it 'raises an error for duplicate routes' do
38
+ empty_proc = Proc.new{}
39
+
40
+ web.add_route('/test', empty_proc)
41
+ expect{web.add_route('/test', empty_proc)}.to raise_error
42
+ end
43
+
44
+ it 'creates GET, POST, and PUT endpoints' do
45
+ web.add_route('/test', Proc.new{})
46
+
47
+ ['GET', 'POST', 'PUT'].each do |method|
48
+ expect(web.app.routes.include?(method)).to be_truthy
49
+ expect(web.app.routes[method].length).to equal(1)
50
+ expect(web.app.routes[method][0][0].match('/test').length).to equal(1)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -8,6 +8,9 @@ describe 'job' do
8
8
  end
9
9
 
10
10
  describe Pushpop do
11
+ before(:each) do
12
+ Pushpop.web.instance_variable_set(:@routes, [])
13
+ end
11
14
 
12
15
  describe 'add_job' do
13
16
  it 'adds a job to the list' do
@@ -23,4 +26,31 @@ describe Pushpop do
23
26
  end
24
27
  end
25
28
 
29
+ describe 'clock' do
30
+ it 'starts clock in a thread' do
31
+ t = Pushpop.start_clock
32
+ expect(t.class).to be(Thread)
33
+
34
+ t.exit
35
+ end
36
+ end
37
+
38
+ describe 'web' do
39
+ it 'gets or creates an instance of Web' do
40
+ expect(Pushpop.web.class).to be(Pushpop::Web)
41
+ end
42
+
43
+ it 'does not start the web app if no routes are defined' do
44
+ expect(Pushpop.start_webserver).to be_falsey
45
+ end
46
+
47
+ it 'starts the web app in a thread' do
48
+ Pushpop.web.add_route('/test', Proc.new{})
49
+ t = Pushpop.start_webserver
50
+ expect(t.class).to be(Thread)
51
+
52
+ t.exit
53
+ end
54
+ end
55
+
26
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pushpop
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Dzielak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-27 00:00:00.000000000 Z
11
+ date: 2015-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clockwork
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sinatra
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: dotenv
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,11 +86,13 @@ files:
72
86
  - lib/pushpop/job.rb
73
87
  - lib/pushpop/step.rb
74
88
  - lib/pushpop/version.rb
89
+ - lib/pushpop/web.rb
75
90
  - pushpop.gemspec
76
91
  - spec/jobs/simple_job.rb
77
92
  - spec/pushpop/cli_spec.rb
78
93
  - spec/pushpop/job_spec.rb
79
94
  - spec/pushpop/step_spec.rb
95
+ - spec/pushpop/web_spec.rb
80
96
  - spec/pushpop_spec.rb
81
97
  - spec/simple_job_spec.rb
82
98
  - spec/spec_helper.rb
@@ -109,6 +125,7 @@ test_files:
109
125
  - spec/pushpop/cli_spec.rb
110
126
  - spec/pushpop/job_spec.rb
111
127
  - spec/pushpop/step_spec.rb
128
+ - spec/pushpop/web_spec.rb
112
129
  - spec/pushpop_spec.rb
113
130
  - spec/simple_job_spec.rb
114
131
  - spec/spec_helper.rb