pushpop 0.2 → 0.3.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 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