pipes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +24 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +331 -0
- data/Rakefile +8 -0
- data/lib/pipes.rb +46 -0
- data/lib/pipes/resque_hooks.rb +18 -0
- data/lib/pipes/runner.rb +112 -0
- data/lib/pipes/stage_parser.rb +152 -0
- data/lib/pipes/store.rb +122 -0
- data/lib/pipes/utils.rb +7 -0
- data/lib/pipes/version.rb +3 -0
- data/pipes.gemspec +24 -0
- data/spec/mock_jobs.rb +58 -0
- data/spec/pipes/resque_hooks_spec.rb +22 -0
- data/spec/pipes/runner_spec.rb +110 -0
- data/spec/pipes/stage_parser_spec.rb +169 -0
- data/spec/pipes/store_spec.rb +181 -0
- data/spec/pipes/utils_spec.rb +14 -0
- data/spec/pipes_spec.rb +46 -0
- data/spec/spec_helper.rb +13 -0
- metadata +140 -0
data/.gitignore
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/lib/pipes.rb
ADDED
@@ -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
|