pipes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ log/*.log
20
+ pkg/
21
+ spec/dummy/db/*.sqlite3
22
+ spec/dummy/log/*.log
23
+ spec/dummy/tmp/
24
+ spec/dummy/.sass-cache
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --order rand
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.3@pipes
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mike Pack
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,331 @@
1
+ # Pipes
2
+
3
+ ![Pipes](http://i.imgur.com/MND26.png)
4
+
5
+ Pipes is a Redis-backed concurrency management system designed around Resque. It provides a DSL for defining "stages" of a process. Each (Resque) job in the stage can be run concurrently, but all must finish before subsequent stages are run.
6
+
7
+ ## Example
8
+
9
+ At Factory Code Labs, we work on a system for which we must deploy static HTML files. We must render any number of HTML pages, assets, .htaccess files, etc so the static HTML-based site can run on Apache.
10
+
11
+ Here's a simplified look at our stages:
12
+
13
+ **Stage 1**
14
+ - Publish HTML files.
15
+ - Publish assets.
16
+ - Publish .htaccess.
17
+
18
+ **Stage 2**
19
+ - rsync files to another server.
20
+ - Upload assets to a CDN.
21
+
22
+ **Stage 3**
23
+ - Activate rynced files.
24
+ - Email people about deploy.
25
+
26
+ We want to ensure that all of **Stage 1** is finished before **Stage 2** begins, and likewise for **Stage 3**. However, the individual components of each stage can execute asynchronously, we just want to make sure they converge when all is finished.
27
+
28
+ ## Installation
29
+
30
+ Add this line to your application's Gemfile:
31
+
32
+ gem 'pipes'
33
+
34
+ And then execute:
35
+
36
+ $ bundle
37
+
38
+ Or install it yourself as:
39
+
40
+ $ gem install pipes
41
+
42
+ ## Usage
43
+
44
+ Pipes assumes your conforming to the Resque API in your jobs, so you might have the following:
45
+
46
+ ```ruby
47
+ module Writers
48
+ class HTMLWriter
49
+ @queue = :content_writers
50
+
51
+ def self.perform(url = 'http://localhost:3000/')
52
+ # ... fetch URL and save HTML ...
53
+ end
54
+ end
55
+ end
56
+ ```
57
+
58
+ You'll generally need to do two things when working with Pipes:
59
+
60
+ 1. Define a set of stages.
61
+ 2. Run the jobs.
62
+
63
+ Let's look at these two steps individually.
64
+
65
+ ### Defining Stages
66
+
67
+ As part of the configuration process, you'll want to define your stages:
68
+
69
+ ```ruby
70
+ Pipes.configure do |config|
71
+ config.stages do
72
+ # Stage 1
73
+ content_writers [
74
+ Writers::HTMLWriter,
75
+ Writers::AssetWriter,
76
+ Writers::HtaccessWriter
77
+ ]
78
+
79
+ # Stage 2
80
+ publishers [
81
+ Publishers::Rsyncer,
82
+ Publishers::CDNUploader
83
+ ]
84
+
85
+ # Stage 3
86
+ notifiers [
87
+ Notifiers::FileActivator
88
+ Notifiers::Emailer
89
+ ]
90
+ end
91
+ end
92
+ ```
93
+
94
+ There's more advanced ways of defining stages, more on that later.
95
+
96
+ Stages are defined lexically. That is, the order in which you define your stages in the config determines the order they will be run.
97
+
98
+ The name of the stage is arbitrary. Above, we have `content_writers`, `publishers` and `notifiers`, though there's no significant meaning. The name of the stage can be later extracted and presented to the user or referenced as a symbol.
99
+
100
+ ### Running The Jobs
101
+
102
+ Once your configuration is set up, you can fire off the jobs:
103
+
104
+ ```ruby
105
+ Pipes::Runner.run([Writers::HTMLWriter, Publishers::Rsyncer])
106
+ ```
107
+
108
+ The above line essentially says "here's the jobs I'm looking to run", at which point Pipes takes over to determine how to partition them into their appropriate stages. Pipes will break these two jobs up as you would expect:
109
+
110
+ ```ruby
111
+ # Stage 1 (content_writers)
112
+ Writers::HTMLWriter
113
+
114
+ # Stage 2 (publishers)
115
+ Publishers::Rsyncer
116
+ ```
117
+
118
+ You can also pass arguments to the jobs, just like Resque:
119
+
120
+ ```ruby
121
+ Pipes::Runner.run([Writers::HTMLWriter], 'http://localhost:3000/page')
122
+ ```
123
+
124
+ In the above case, all jobs' `.perform` methods would receive the `http://localhost:3000/page` argument. You can, of course, pass multiple arguments:
125
+
126
+ ```ruby
127
+ module Writers
128
+ class HTMLWriter
129
+ @queue = :content_writers
130
+
131
+ def self.perform(host = 'localhost', port = 3000)
132
+ # ... fetch URL and save HTML ...
133
+ end
134
+ end
135
+ end
136
+
137
+ Pipes::Runner.run([Writers::HTMLWriter], 'google.com', 80)
138
+ ```
139
+
140
+ ## Defining Stage Dependencies
141
+
142
+ Pipes makes it easy to define dependencies between jobs.
143
+
144
+ Say you want the `Publishers::Rsyncer` to always run after `Writers::HTMLWriter`. You'll first want to modify your config:
145
+
146
+ ```ruby
147
+ Pipes.configure do |config|
148
+ config.stages do
149
+ content_writers [
150
+ {Writers::HTMLWriter => Publishers::Rsyncer}
151
+ ]
152
+
153
+ publishers [
154
+ Publishers::Rsyncer,
155
+ Publishers::CDNUploader
156
+ ]
157
+ end
158
+ end
159
+ ```
160
+
161
+ By converting the individual job into a Hash, you can specify that you want `Publishers::Rsyncer` to always run after `Writers::HTMLWriter`. You can also specify multiple dependencies:
162
+
163
+ ```ruby
164
+ Pipes.configure do |config|
165
+ config.stages do
166
+ content_writers [
167
+ {Writers::HTMLWriter => [Publishers::Rsyncer, Publishers::CDNUploader]}
168
+ ]
169
+
170
+ publishers [
171
+ Publishers::Rsyncer,
172
+ Publishers::CDNUploader
173
+ ]
174
+ end
175
+ end
176
+ ```
177
+
178
+ Defining arrays of dependencies is great, but if you're just reiterating all jobs in a particular stage, you can specify the stage instead:
179
+
180
+ ```ruby
181
+ Pipes.configure do |config|
182
+ config.stages do
183
+ content_writers [
184
+ {Writers::HTMLWriter => :publishers}
185
+ ]
186
+
187
+ publishers [
188
+ Publishers::Rsyncer,
189
+ Publishers::CDNUploader
190
+ ]
191
+ end
192
+ end
193
+ ```
194
+
195
+ If you need to specify multiple dependent stages, you can provide an array of symbols:
196
+
197
+ ```ruby
198
+ Pipes.configure do |config|
199
+ config.stages do
200
+ content_writers [
201
+ {Writers::HTMLWriter => [:publishers, :notifiers]}
202
+ ]
203
+
204
+ publishers [
205
+ Publishers::Rsyncer,
206
+ Publishers::CDNUploader
207
+ ]
208
+
209
+ notifiers [
210
+ Notifiers::FileActivator
211
+ ]
212
+ end
213
+ end
214
+ ```
215
+
216
+ Pipes will also resolve deep dependencies:
217
+
218
+ ```ruby
219
+ Pipes.configure do |config|
220
+ config.stages do
221
+ content_writers [
222
+ {Writers::HTMLWriter => :publishers}
223
+ ]
224
+
225
+ publishers [
226
+ {Publishers::Rsyncer => Notifiers::FileActivator},
227
+ Publishers::CDNUploader
228
+ ]
229
+
230
+ notifiers [
231
+ Notifiers::FileActivator
232
+ ]
233
+ end
234
+ end
235
+ ```
236
+
237
+ In the above example, `Notifiers::FileActivator` will also be a dependency of `Writers::HTMLWriter` because it's a dependency of one of `Writers::HTMLWriters` dependencies (:publishers).
238
+
239
+ Running jobs with dependencies is the same as before:
240
+
241
+ ```ruby
242
+ Pipes::Runner.run([Writers::HTMLWriter], 'http://localhost:3000/page')
243
+ ```
244
+
245
+ The above code will run `Writers::HTMLWriter` in **Stage 1**, `Publishers::Rsyncer` and `Publishers::CDNUploader` in **Stage 2**, and `Notifiers::FileActivator` in **Stage 3**, all receiving the `http://localhost:3000/page' argument.
246
+
247
+ You can turn off dependency resolution by passing in some additional Pipes options as the third argument:
248
+
249
+ ```ruby
250
+ Pipes::Runner.run([Writers::HTMLWriter], 'http://localhost:3000/page', {resolve: false})
251
+ ```
252
+
253
+ In the above code, only `Writers::HTMLWriter` will be run.
254
+
255
+ ## Acceptable Formats for Jobs
256
+
257
+ Pipes allows you to specify your jobs in a variety of ways:
258
+
259
+ ```ruby
260
+ # A single job
261
+ Pipes::Runner.run(Writers::HTMLWriter)
262
+
263
+ # A single job as a string. Might be helpful if accepting params from a form
264
+ Pipes::Runner.run('Writers::HTMLWriter')
265
+
266
+ # An entire stage
267
+ Pipes::Runner.run(:content_writers)
268
+
269
+ # You can pass an array of any of the above, intermixing types
270
+ Pipes::Runner.run([:content_writers, 'Publishers::CDNUploader', Notifiers::FileActivator])
271
+ ```
272
+
273
+ ## Configuring Pipes
274
+
275
+ Pipes allows you to specify a variety of configuration options:
276
+
277
+ ```ruby
278
+ Pipes.configure do |config|
279
+ # config.redis can be a string...
280
+ config.redis = 'localhost:6379'
281
+ # ...or a Redis connection (default $redis):
282
+ config.redis = REDIS
283
+
284
+ # config.namespace will specify a Redis namespace to use (default nil):
285
+ config.namespace = 'my_project'
286
+
287
+ # config.resolve tells Pipes to resolve dependencies when calling Pipes::Runner.run(...) (default true):
288
+ config.resolve = false
289
+
290
+ config.stages do
291
+ # ...
292
+ end
293
+ end
294
+ ```
295
+
296
+ If you're using Pipes in a Rails app, stick your configuration in `config/initializers/pipes.rb`.
297
+
298
+ ## Support
299
+
300
+ Pipes is currently tested under Ruby 1.9.3.
301
+
302
+ ## Known Caveats
303
+
304
+ If your job is expecting a hash as the last argument, you'll need to pass an additional hash so pipes won't think your final argument is the options:
305
+
306
+ ```ruby
307
+ # Pipes will assume {follow_links: true} is options for Pipes, not your job:
308
+ Pipes::Runner.run([Writers::HTMLWriter], {follow_links: true})
309
+
310
+ # So you should pass a trailing hash to denote that there are no Pipes options:
311
+ Pipes::Runner.run([Writers::HTMLWriter], {follow_links: true}, {})
312
+
313
+ # Of course, if you do specify options for Pipes, everything will work fine:
314
+ Pipes::Runner.run([Writers::HTMLWriter], {follow_links: true}, {resolve: true})
315
+ ```
316
+
317
+ ## Future Improvements
318
+
319
+ - Better atomicity
320
+ - Represent jobs and stages as objects, instead of simple data structures
321
+ - Support for runaway workers/jobs
322
+
323
+ ## Credits
324
+
325
+ ![Factory Code Labs](http://i.imgur.com/yV4u1.png)
326
+
327
+ Pipes is maintained by [Factory Code Labs](http://www.factorycodelabs.com).
328
+
329
+ ## License
330
+
331
+ Pipes is Copyright © 2012 Factory Code Labs. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ desc 'Run RSpec code examples'
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.verbose = false
6
+ end
7
+
8
+ task default: :spec
@@ -0,0 +1,46 @@
1
+ module Pipes
2
+ # Default options
3
+ @redis = $redis
4
+ @resolve = true
5
+
6
+ class << self
7
+ attr_reader :redis
8
+ attr_accessor :namespace, :resolve
9
+ end
10
+
11
+ def self.configure(*args, &block)
12
+ yield self
13
+ end
14
+
15
+ # config.redis can be a string or a redis connection
16
+ # eg: config.redis = 'localhost:6379'
17
+ # or config.redis = $MY_REDIS
18
+ def self.redis=(redis)
19
+ if redis.is_a? String
20
+ host, port = redis.split(':')
21
+ set_redis(Redis.new(host: host, port: port))
22
+ else
23
+ set_redis(redis)
24
+ end
25
+ end
26
+
27
+ def self.stages(*args, &block)
28
+ Abyss.configure(*args) do
29
+ stages &block
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def self.set_redis(redis)
36
+ @redis = redis
37
+ Resque.redis = redis
38
+ Redis.current = redis
39
+ end
40
+ end
41
+
42
+ require 'pipes/utils'
43
+ require 'pipes/stage_parser'
44
+ require 'pipes/store'
45
+ require 'pipes/runner'
46
+ require 'pipes/resque_hooks'
@@ -0,0 +1,18 @@
1
+ require 'pipes'
2
+ require 'resque'
3
+
4
+ Resque.before_fork do |job|
5
+ job.payload_class.extend Pipes::ResqueHooks
6
+ end
7
+
8
+ module Pipes
9
+ module ResqueHooks
10
+ def after_perform_pipes(*args)
11
+ Pipes::Store.done
12
+ end
13
+
14
+ def on_failure_pipes(e, *args)
15
+ Pipes::Store.done
16
+ end
17
+ end
18
+ end