pipes 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +5 -0
- data/README.md +329 -22
- data/lib/pipes.rb +14 -1
- data/lib/pipes/stage_parser.rb +29 -15
- data/lib/pipes/version.rb +1 -1
- data/pipes.gemspec +1 -0
- data/spec/pipes/runner_spec.rb +0 -1
- data/spec/pipes/stage_parser_spec.rb +53 -12
- data/spec/pipes/store_spec.rb +0 -1
- data/spec/pipes_spec.rb +9 -0
- data/spec/spec_helper.rb +2 -0
- metadata +19 -26
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
-
# Pipes
|
1
|
+
# Pipes [![Build Status](https://secure.travis-ci.org/mikepack/pipes.png)](http://travis-ci.org/mikepack/pipes) [![Dependency Status](https://gemnasium.com/mikepack/pipes.png)](https://gemnasium.com/mikepack/pipes) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/mikepack/pipes)
|
2
|
+
|
3
|
+
[RDoc](http://rubydoc.info/github/mikepack/pipes)
|
2
4
|
|
3
5
|
![Pipes](http://i.imgur.com/MND26.png)
|
4
6
|
|
5
7
|
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
8
|
|
9
|
+
Conceivably, Pipes is a lightweight, advanced Resque queue. It can be dropped right in place of Resque.
|
10
|
+
|
7
11
|
## Example
|
8
12
|
|
9
|
-
At Factory
|
13
|
+
At Factory 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
14
|
|
11
15
|
Here's a simplified look at our stages:
|
12
16
|
|
@@ -25,6 +29,10 @@ Here's a simplified look at our stages:
|
|
25
29
|
|
26
30
|
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
31
|
|
32
|
+
This can be visualized as follows:
|
33
|
+
|
34
|
+
![Architecture](http://i.imgur.com/0CEmm.png)
|
35
|
+
|
28
36
|
## Installation
|
29
37
|
|
30
38
|
Add this line to your application's Gemfile:
|
@@ -41,7 +49,7 @@ Or install it yourself as:
|
|
41
49
|
|
42
50
|
## Usage
|
43
51
|
|
44
|
-
Pipes assumes
|
52
|
+
Pipes assumes you're conforming to the Resque API in your jobs, so you might have the following:
|
45
53
|
|
46
54
|
```ruby
|
47
55
|
module Writers
|
@@ -99,10 +107,12 @@ The name of the stage is arbitrary. Above, we have `content_writers`, `publisher
|
|
99
107
|
|
100
108
|
### Running The Jobs
|
101
109
|
|
102
|
-
Once your configuration is set up, you can fire off the jobs
|
110
|
+
Once your configuration is set up, you can fire off the jobs.
|
111
|
+
|
112
|
+
The Pipes API is designed to mimic Resque:
|
103
113
|
|
104
114
|
```ruby
|
105
|
-
Pipes
|
115
|
+
Pipes.enqueue([Writers::HTMLWriter, Publishers::Rsyncer])
|
106
116
|
```
|
107
117
|
|
108
118
|
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:
|
@@ -115,10 +125,13 @@ Writers::HTMLWriter
|
|
115
125
|
Publishers::Rsyncer
|
116
126
|
```
|
117
127
|
|
118
|
-
You can also pass arguments to the jobs, just like Resque:
|
128
|
+
You can also pass arguments to the jobs, just like Resque. In fact, any call to `Resque.enqueue` can be safely replaced with `Pipes.enqueue`, but the reverse is not true:
|
119
129
|
|
120
130
|
```ruby
|
121
|
-
|
131
|
+
# If you currently have:
|
132
|
+
# Resque.enqueue(Writers::HTMLWriter, 'http://localhost:3000/page')
|
133
|
+
# ...you can replace it with:
|
134
|
+
Pipes.enqueue(Writers::HTMLWriter, 'http://localhost:3000/page')
|
122
135
|
```
|
123
136
|
|
124
137
|
In the above case, all jobs' `.perform` methods would receive the `http://localhost:3000/page` argument. You can, of course, pass multiple arguments:
|
@@ -134,10 +147,10 @@ module Writers
|
|
134
147
|
end
|
135
148
|
end
|
136
149
|
|
137
|
-
Pipes
|
150
|
+
Pipes.enqueue(Writers::HTMLWriter, 'google.com', 80)
|
138
151
|
```
|
139
152
|
|
140
|
-
## Defining
|
153
|
+
## Defining Job Dependencies
|
141
154
|
|
142
155
|
Pipes makes it easy to define dependencies between jobs.
|
143
156
|
|
@@ -239,18 +252,125 @@ In the above example, `Notifiers::FileActivator` will also be a dependency of `W
|
|
239
252
|
Running jobs with dependencies is the same as before:
|
240
253
|
|
241
254
|
```ruby
|
242
|
-
Pipes
|
255
|
+
Pipes.enqueue(Writers::HTMLWriter, 'http://localhost:3000/page')
|
243
256
|
```
|
244
257
|
|
245
258
|
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
259
|
|
247
|
-
|
260
|
+
## Defining Stage Dependencies
|
261
|
+
|
262
|
+
Just as jobs can have dependencies, stages can as well.
|
263
|
+
|
264
|
+
Imagine you have multiple jobs in a given stage, all of which have the same dependencies:
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
Pipes.configure do |config|
|
268
|
+
config.stages do
|
269
|
+
content_writers [
|
270
|
+
{Writers::HTMLWriter => :publishers},
|
271
|
+
{Writers::AssetWriter => :publishers}
|
272
|
+
]
|
273
|
+
|
274
|
+
publishers [
|
275
|
+
Publishers::Rsyncer
|
276
|
+
]
|
277
|
+
|
278
|
+
notifiers [
|
279
|
+
Notifiers::FileActivator
|
280
|
+
]
|
281
|
+
end
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
This isn't so DRY. You would be better off adding a stage dependency:
|
286
|
+
|
287
|
+
```ruby
|
288
|
+
Pipes.configure do |config|
|
289
|
+
config.stages do
|
290
|
+
content_writers [
|
291
|
+
Writers::HTMLWriter,
|
292
|
+
Writers::AssetWriter
|
293
|
+
] => :publishers
|
294
|
+
|
295
|
+
publishers [
|
296
|
+
Publishers::Rsyncer
|
297
|
+
]
|
298
|
+
|
299
|
+
notifiers [
|
300
|
+
Notifiers::FileActivator
|
301
|
+
]
|
302
|
+
end
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
This tells Pipes that you would like all jobs in the `:content_writers` stage to have a depencency on all `:publishers`.
|
307
|
+
|
308
|
+
You can intermix types for stage dependencies, just like with job dependencies:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
Pipes.configure do |config|
|
312
|
+
config.stages do
|
313
|
+
content_writers [
|
314
|
+
Writers::HTMLWriter,
|
315
|
+
Writers::AssetWriter
|
316
|
+
] => [:publishers, Notifiers::FileActivator]
|
317
|
+
|
318
|
+
publishers [
|
319
|
+
Publishers::Rsyncer
|
320
|
+
]
|
321
|
+
|
322
|
+
notifiers [
|
323
|
+
Notifiers::FileActivator
|
324
|
+
]
|
325
|
+
end
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
This will ensure that all `:publishers` and the `Notifiers::FileActivator` get run when either of the `:content_writers` are run.
|
330
|
+
|
331
|
+
As you would expect, Pipes will resolve deep dependencies for you as well:
|
248
332
|
|
249
333
|
```ruby
|
250
|
-
Pipes
|
334
|
+
Pipes.configure do |config|
|
335
|
+
config.stages do
|
336
|
+
content_writers [
|
337
|
+
Writers::HTMLWriter,
|
338
|
+
Writers::AssetWriter
|
339
|
+
] => :publishers
|
340
|
+
|
341
|
+
publishers [
|
342
|
+
Publishers::Rsyncer
|
343
|
+
] => :notifiers
|
344
|
+
|
345
|
+
notifiers [
|
346
|
+
Notifiers::FileActivator
|
347
|
+
]
|
348
|
+
end
|
349
|
+
end
|
251
350
|
```
|
252
351
|
|
253
|
-
|
352
|
+
The above will add `Publishers::Rsyncer` and `Notifiers::FileActivator` as dependencies of both `Writers::HTMLWriter` and `Writers::AssetWriter`.
|
353
|
+
|
354
|
+
Intermixing job and stage dependencies works, too, resulting in the same dependency graph as the above example:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
Pipes.configure do |config|
|
358
|
+
config.stages do
|
359
|
+
content_writers [
|
360
|
+
Writers::HTMLWriter,
|
361
|
+
Writers::AssetWriter
|
362
|
+
] => :publishers
|
363
|
+
|
364
|
+
publishers [
|
365
|
+
{Publishers::Rsyncer => :notifiers}
|
366
|
+
]
|
367
|
+
|
368
|
+
notifiers [
|
369
|
+
Notifiers::FileActivator
|
370
|
+
]
|
371
|
+
end
|
372
|
+
end
|
373
|
+
```
|
254
374
|
|
255
375
|
## Acceptable Formats for Jobs
|
256
376
|
|
@@ -258,16 +378,16 @@ Pipes allows you to specify your jobs in a variety of ways:
|
|
258
378
|
|
259
379
|
```ruby
|
260
380
|
# A single job
|
261
|
-
Pipes
|
381
|
+
Pipes.enqueue(Writers::HTMLWriter)
|
262
382
|
|
263
383
|
# A single job as a string. Might be helpful if accepting params from a form
|
264
|
-
Pipes
|
384
|
+
Pipes.enqueue('Writers::HTMLWriter')
|
265
385
|
|
266
386
|
# An entire stage
|
267
|
-
Pipes
|
387
|
+
Pipes.enqueue(:content_writers)
|
268
388
|
|
269
389
|
# You can pass an array of any of the above, intermixing types
|
270
|
-
Pipes
|
390
|
+
Pipes.enqueue([:content_writers, 'Publishers::CDNUploader', Notifiers::FileActivator])
|
271
391
|
```
|
272
392
|
|
273
393
|
## Configuring Pipes
|
@@ -284,7 +404,7 @@ Pipes.configure do |config|
|
|
284
404
|
# config.namespace will specify a Redis namespace to use (default nil):
|
285
405
|
config.namespace = 'my_project'
|
286
406
|
|
287
|
-
# config.resolve tells Pipes to resolve dependencies when calling Pipes
|
407
|
+
# config.resolve tells Pipes to resolve dependencies when calling Pipes.enqueue(...) (default true):
|
288
408
|
config.resolve = false
|
289
409
|
|
290
410
|
config.stages do
|
@@ -295,9 +415,196 @@ end
|
|
295
415
|
|
296
416
|
If you're using Pipes in a Rails app, stick your configuration in `config/initializers/pipes.rb`.
|
297
417
|
|
418
|
+
## Pipes Options
|
419
|
+
|
420
|
+
You can pass a hash of options when enqueueing workers through Pipes.
|
421
|
+
|
422
|
+
**resolve**
|
423
|
+
|
424
|
+
By default, Pipes will resolve and queue up all dependencies of the jobs you are requesting. You can turn off dependency resolution by passing in some additional Pipes options as the third argument:
|
425
|
+
|
426
|
+
```ruby
|
427
|
+
Pipes.enqueue(Writers::HTMLWriter, 'http://localhost:3000/page', {resolve: false})
|
428
|
+
```
|
429
|
+
|
430
|
+
When **resolve** is false, only `Writers::HTMLWriter` will be run, ignoring dependencies.
|
431
|
+
|
432
|
+
**allow_duplicates**
|
433
|
+
|
434
|
+
If jobs are already queued up in Pipes and you'd like to enqueue more jobs, you may need to specify that only certain jobs be duplicated in the queue.
|
435
|
+
|
436
|
+
```ruby
|
437
|
+
Pipes.enqueue(Writers::HTMLWriter, 'http://localhost:3000/page', {allow_duplicates: :content_writers})
|
438
|
+
# ..or an array of stages..
|
439
|
+
Pipes.enqueue(Writers::HTMLWriter, 'http://localhost:3000/page', {allow_duplicates: [:content_writers, :publishers]})
|
440
|
+
```
|
441
|
+
|
442
|
+
When Pipes enqueues `Writers::HTMLWriter` and all its dependencies, it will check whether any jobs with the same class name already exist in the queue. If a job has already been queued up with the same class name and **does not** belong to one of the stages provided to **allow_duplicates**, it is ignored.
|
443
|
+
|
444
|
+
This option helps prevent adding redundant jobs to the queue. See the section *Queueing Up Additional Jobs*.
|
445
|
+
|
446
|
+
## Working With Resque Priorities
|
447
|
+
|
448
|
+
Pipes is designed to work on top of Resque's already-existing queueing system. That is, the queue priorities Resque provides will continue to be honored.
|
449
|
+
|
450
|
+
Combining Resque's priority queues with Pipe's stages can produce fine-grain control over how your jobs get processed. By using the normal `@queue` instance variable, and specifying priorities when starting up your Resque workers, you can control the order in which jobs get processed for each individual stage.
|
451
|
+
|
452
|
+
Say we had the following jobs:
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
module Writers
|
456
|
+
class HTMLWriter
|
457
|
+
@queue = :priority_1
|
458
|
+
|
459
|
+
def self.perform; end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
module Writers
|
464
|
+
class AssetWriter
|
465
|
+
@queue = :priority_2
|
466
|
+
|
467
|
+
def self.perform; end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
```
|
471
|
+
|
472
|
+
Both `Writer::HTMLWriter` and `Writer::AssetWriter` are configured for the same stage:
|
473
|
+
|
474
|
+
```ruby
|
475
|
+
Pipes.configure do |config|
|
476
|
+
config.stages do
|
477
|
+
content_writers [
|
478
|
+
Writers::HTMLWriter,
|
479
|
+
Writers::AssetWriter
|
480
|
+
]
|
481
|
+
end
|
482
|
+
end
|
483
|
+
```
|
484
|
+
|
485
|
+
Start up Resque with the usual priority list:
|
486
|
+
|
487
|
+
```bash
|
488
|
+
$ QUEUES=priority_1,priority_2 rake resque:work
|
489
|
+
```
|
490
|
+
|
491
|
+
Run the jobs through Pipes:
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
Pipes.enqueue(:content_writers)
|
495
|
+
```
|
496
|
+
|
497
|
+
Pipes will queue up both `Writer::HTMLWriter` and `Writer::AssetWriter` in Resque. Resque takes over and respects the queue priorities, first running `Writers::HTMLWriter`, then `Writers::AssetWriter`.
|
498
|
+
|
499
|
+
## Queueing Up Additional Jobs
|
500
|
+
|
501
|
+
Say you have a job, `Writers::HTMLWriter`, whose purpose is to fire off additional jobs to accomplish the real work. This is actual the case for us at Factory Labs. Our `HTMLWriter` fires off additional jobs who do the heavy lifting of parsing page contents, and writing to a file.
|
502
|
+
|
503
|
+
Our `HTMLWriter` fires off additional writers:
|
504
|
+
|
505
|
+
```ruby
|
506
|
+
module Writers
|
507
|
+
class HTMLWriter
|
508
|
+
@queue = :content_writers
|
509
|
+
|
510
|
+
def self.perform(locale)
|
511
|
+
Pages.all.each do |page|
|
512
|
+
# Enqueue additional jobs to do the real work
|
513
|
+
Pipes.enqueue(Writers::PageWriter, page.id, locale, {allow_duplicates: [:content_writers]})
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
class PageWriter
|
519
|
+
@queue = :content_writers
|
520
|
+
|
521
|
+
def self.perform(page_id, locale)
|
522
|
+
# We would normally do stuff with the locale...
|
523
|
+
url = page_url(Page.find(page_id))
|
524
|
+
content = URI.parse(url).read
|
525
|
+
|
526
|
+
File.new('index.html', 'w') do |f|
|
527
|
+
f.write(content)
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
```
|
533
|
+
|
534
|
+
Both jobs are configured for the same stage, with a dependency on `:publishers`:
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
Pipes.configure do |config|
|
538
|
+
config.stages do
|
539
|
+
content_writers [
|
540
|
+
{Writers::HTMLWriter => :publishers},
|
541
|
+
{Writers::PageWriter => :publishers}
|
542
|
+
]
|
543
|
+
|
544
|
+
publishers [
|
545
|
+
Publishers::Rsyncer
|
546
|
+
]
|
547
|
+
end
|
548
|
+
end
|
549
|
+
```
|
550
|
+
|
551
|
+
We fire off just the `HTMLWriter`:
|
552
|
+
|
553
|
+
```ruby
|
554
|
+
Pipes.enqueue(Writer::HTMLWriter, 'en-US')
|
555
|
+
```
|
556
|
+
|
557
|
+
Pipes queues up the `Writer::HTMLWriter` and its dependent, `Publishers::Rsyncer`. So, our queue looks like this:
|
558
|
+
|
559
|
+
```ruby
|
560
|
+
# Stage 1 (content_writers)
|
561
|
+
Writers::HTMLWriter.perform('en-US')
|
562
|
+
|
563
|
+
# Stage 2 (publishers)
|
564
|
+
Publishers::Rsyncer.perform('en-US')
|
565
|
+
```
|
566
|
+
|
567
|
+
After processing the first job, `HTMLWriter`, the Pipes queue looks like this:
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
# Stage 1 (content_writers)
|
571
|
+
Writers::PageWriter.perform(1, 'en-US')
|
572
|
+
Writers::PageWriter.perform(2, 'en-US')
|
573
|
+
Writers::PageWriter.perform(3, 'en-US')
|
574
|
+
...
|
575
|
+
|
576
|
+
# Stage 2 (publishers)
|
577
|
+
Publishers::Rsyncer.perform('en-US')
|
578
|
+
```
|
579
|
+
|
580
|
+
Pipes will ensure your stages stay intact when enqueueing additional jobs mid-pipe. That is, **Stage 2** jobs are still queued *after* additional jobs have been added to **Stage 1**. This applies to jobs added to any stages. You can continue to add jobs to any stage while Pipes is working.
|
581
|
+
|
582
|
+
**The allow_duplicates option**
|
583
|
+
|
584
|
+
By default, Pipes will check for exact duplicate jobs in the queue (eg `Writer::HTMLWriter` with argument `en-US`). If we don't provide the `allow_duplicates` option within the `HTMLWriter`'s `#perform` method, the Pipes queue would look like this:
|
585
|
+
|
586
|
+
```ruby
|
587
|
+
# Stage 1 (content_writers)
|
588
|
+
(DONE) Writers::HTMLWriter.perform('en-US')
|
589
|
+
Writers::PageWriter.perform(1, 'en-US')
|
590
|
+
Writers::PageWriter.perform(2, 'en-US')
|
591
|
+
Writers::PageWriter.perform(3, 'en-US')
|
592
|
+
...
|
593
|
+
|
594
|
+
# Stage 2 (publishers)
|
595
|
+
Publishers::Rsyncer.perform('en-US')
|
596
|
+
Publishers::Rsyncer.perform(1, 'en-US')
|
597
|
+
Publishers::Rsyncer.perform(2, 'en-US')
|
598
|
+
Publishers::Rsyncer.perform(3, 'en-US')
|
599
|
+
```
|
600
|
+
|
601
|
+
We only want to run rsync once, so this is incorrect. To prevent this from happening, we indicate that we only want duplicate `:content_writers` to the **allow_duplicates** option.
|
602
|
+
|
603
|
+
By telling Pipes that we want to only allow duplicate `:content_writers`, we prevent duplicate `Rsyncer`s from being queued up, even though `PageWriter` has a `Rsyncer` dependency. **allow_duplicates** will force Pipes to check whether the `Rsyncer` class already exists in the queue (ignoring job arguments), and if so, skips that job.
|
604
|
+
|
298
605
|
## Support
|
299
606
|
|
300
|
-
Pipes
|
607
|
+
Pipes makes use of Ruby 1.9's ordered hashes. No deliberate support for Ruby 1.8.
|
301
608
|
|
302
609
|
## Known Caveats
|
303
610
|
|
@@ -305,13 +612,13 @@ If your job is expecting a hash as the last argument, you'll need to pass an add
|
|
305
612
|
|
306
613
|
```ruby
|
307
614
|
# Pipes will assume {follow_links: true} is options for Pipes, not your job:
|
308
|
-
Pipes
|
615
|
+
Pipes.enqueue([Writers::HTMLWriter], {follow_links: true})
|
309
616
|
|
310
617
|
# So you should pass a trailing hash to denote that there are no Pipes options:
|
311
|
-
Pipes
|
618
|
+
Pipes.enqueue([Writers::HTMLWriter], {follow_links: true}, {})
|
312
619
|
|
313
620
|
# Of course, if you do specify options for Pipes, everything will work fine:
|
314
|
-
Pipes
|
621
|
+
Pipes.enqueue([Writers::HTMLWriter], {follow_links: true}, {resolve: true})
|
315
622
|
```
|
316
623
|
|
317
624
|
## Future Improvements
|
data/lib/pipes.rb
CHANGED
@@ -8,13 +8,17 @@ module Pipes
|
|
8
8
|
attr_accessor :namespace, :resolve
|
9
9
|
end
|
10
10
|
|
11
|
+
# Open up the configuration for multiple values.
|
12
|
+
# eg: Pipes.configuration { |config| ... }
|
13
|
+
#
|
11
14
|
def self.configure(*args, &block)
|
12
15
|
yield self
|
13
16
|
end
|
14
17
|
|
15
|
-
# config.redis can be a string or a redis connection
|
18
|
+
# config.redis can be a string or a redis connection.
|
16
19
|
# eg: config.redis = 'localhost:6379'
|
17
20
|
# or config.redis = $MY_REDIS
|
21
|
+
#
|
18
22
|
def self.redis=(redis)
|
19
23
|
if redis.is_a? String
|
20
24
|
host, port = redis.split(':')
|
@@ -24,12 +28,21 @@ module Pipes
|
|
24
28
|
end
|
25
29
|
end
|
26
30
|
|
31
|
+
# Stages, defined in the configuration.
|
32
|
+
#
|
27
33
|
def self.stages(*args, &block)
|
28
34
|
Abyss.configure(*args) do
|
29
35
|
stages &block
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
39
|
+
# Shortcut to queue up a Pipes job. Designed to look
|
40
|
+
# similar to Resque.
|
41
|
+
#
|
42
|
+
def self.enqueue(*args, &block)
|
43
|
+
Runner.run(*args, &block)
|
44
|
+
end
|
45
|
+
|
33
46
|
private
|
34
47
|
|
35
48
|
def self.set_redis(redis)
|
data/lib/pipes/stage_parser.rb
CHANGED
@@ -25,7 +25,7 @@ module Pipes
|
|
25
25
|
if !@dependencies[job] or @dependencies[job].empty?
|
26
26
|
[]
|
27
27
|
else
|
28
|
-
recursive_dependencies = @dependencies[job].map{ |
|
28
|
+
recursive_dependencies = @dependencies[job].map{ |klass| dependents_for(klass) }
|
29
29
|
(@dependencies[job] + recursive_dependencies).flatten.uniq
|
30
30
|
end
|
31
31
|
end
|
@@ -45,9 +45,12 @@ module Pipes
|
|
45
45
|
#
|
46
46
|
def stages_with_resolved_dependencies
|
47
47
|
# Looping over all stages...
|
48
|
-
@stages.inject({}) do |resolved_stages, (name,
|
48
|
+
@stages.inject({}) do |resolved_stages, (name, jobs)|
|
49
|
+
# If it's defined with a stage dependency
|
50
|
+
jobs, _ = jobs.to_a.first if jobs.is_a? Hash
|
51
|
+
|
49
52
|
# Looping over all jobs...
|
50
|
-
resolved_stages[name] =
|
53
|
+
resolved_stages[name] = jobs.inject([]) do |resolved_stage, job|
|
51
54
|
job = job.keys[0] if job.is_a? Hash
|
52
55
|
# Normalze to new hash form
|
53
56
|
resolved_stage << {job => @dependencies[job]}
|
@@ -73,16 +76,25 @@ module Pipes
|
|
73
76
|
@dependencies = {}
|
74
77
|
|
75
78
|
reversed = Hash[@stages.to_a.reverse]
|
76
|
-
reversed.each do |name,
|
77
|
-
|
79
|
+
reversed.each do |name, jobs|
|
80
|
+
if jobs.is_a? Hash
|
81
|
+
# Stage dependency present
|
82
|
+
jobs, stage_dependents = jobs.to_a.first
|
83
|
+
end
|
84
|
+
|
85
|
+
jobs.each do |job|
|
78
86
|
# Does the job have dependents?
|
79
87
|
if job.is_a? Hash
|
80
|
-
|
81
|
-
@dependencies[
|
88
|
+
job, dependents = job.to_a.first
|
89
|
+
@dependencies[job] = dependencies_for_job(dependents)
|
82
90
|
else
|
83
91
|
# Defined job is a simple class (eg Publisher)
|
84
92
|
@dependencies[job] = []
|
85
93
|
end
|
94
|
+
|
95
|
+
if stage_dependents
|
96
|
+
@dependencies[job] += dependencies_for_job(stage_dependents)
|
97
|
+
end
|
86
98
|
end
|
87
99
|
end
|
88
100
|
end
|
@@ -105,17 +117,17 @@ module Pipes
|
|
105
117
|
# Iterate over all jobs for this stage and find dependents.
|
106
118
|
#
|
107
119
|
def dependents_for_stage(stage_name)
|
108
|
-
stage = @stages[stage_name.to_sym]
|
120
|
+
stage = array_for_stage(@stages[stage_name.to_sym])
|
109
121
|
|
110
|
-
stage.inject([]) do |
|
122
|
+
stage.inject([]) do |jobs, job|
|
111
123
|
# Does the job have dependents?
|
112
124
|
if job.is_a? Hash
|
113
|
-
|
114
|
-
|
115
|
-
|
125
|
+
job, dependents = job.to_a.first
|
126
|
+
jobs << job
|
127
|
+
jobs << dependencies_for_job(dependents)
|
116
128
|
else
|
117
129
|
# Defined job is a simple class (eg Publisher)
|
118
|
-
|
130
|
+
jobs << [job] + dependents_for(job)
|
119
131
|
end
|
120
132
|
end.flatten.uniq
|
121
133
|
end
|
@@ -138,8 +150,10 @@ module Pipes
|
|
138
150
|
|
139
151
|
# Just list the jobs in the stage, ignoring dependencies.
|
140
152
|
#
|
141
|
-
def array_for_stage(
|
142
|
-
|
153
|
+
def array_for_stage(jobs)
|
154
|
+
jobs, _ = jobs.to_a.first if jobs.is_a? Hash
|
155
|
+
|
156
|
+
jobs.inject([]) do |arr, job|
|
143
157
|
arr << if job.is_a? Hash
|
144
158
|
# Take just the job class, without any dependents
|
145
159
|
job.keys[0]
|
data/lib/pipes/version.rb
CHANGED
data/pipes.gemspec
CHANGED
data/spec/pipes/runner_spec.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'mock_jobs'
|
3
2
|
|
4
3
|
describe Pipes::StageParser do
|
5
4
|
subject { Pipes::StageParser }
|
@@ -133,27 +132,69 @@ describe Pipes::StageParser do
|
|
133
132
|
end
|
134
133
|
end
|
135
134
|
|
136
|
-
context 'with a
|
135
|
+
context 'with the stage having a dependency on another stage (stage dependencies)' do
|
136
|
+
let(:stages) {
|
137
|
+
{
|
138
|
+
writers: {[ Writers::ContentWriter, Writers::AnotherContentWriter ] => :publishers},
|
139
|
+
publishers: {[ Publishers::Publisher ] => :messengers},
|
140
|
+
messengers: [ Messengers::SMS ]
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
it 'adds all classes within the dependent stages to the list' do
|
145
|
+
expected = {
|
146
|
+
writers: [ { Writers::ContentWriter => [Publishers::Publisher, Messengers::SMS] },
|
147
|
+
{ Writers::AnotherContentWriter => [Publishers::Publisher, Messengers::SMS] } ],
|
148
|
+
publishers: [ { Publishers::Publisher => [Messengers::SMS] } ],
|
149
|
+
messengers: [ { Messengers::SMS => [] } ]
|
150
|
+
}
|
151
|
+
|
152
|
+
subject.new(stages).stages_with_resolved_dependencies.should == expected
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context 'with the stage dependency as a mixed array' do
|
157
|
+
let(:stages) {
|
158
|
+
{
|
159
|
+
writers: {[ Writers::ContentWriter, Writers::AnotherContentWriter ] => [:publishers, Messengers::SMS]},
|
160
|
+
publishers: [ Publishers::Publisher ],
|
161
|
+
messengers: [ Messengers::SMS ]
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
it 'adds all classes within the dependent stages to the list' do
|
166
|
+
expected = {
|
167
|
+
writers: [ { Writers::ContentWriter => [Publishers::Publisher, Messengers::SMS] },
|
168
|
+
{ Writers::AnotherContentWriter => [Publishers::Publisher, Messengers::SMS] } ],
|
169
|
+
publishers: [ { Publishers::Publisher => [] } ],
|
170
|
+
messengers: [ { Messengers::SMS => [] } ]
|
171
|
+
}
|
172
|
+
|
173
|
+
subject.new(stages).stages_with_resolved_dependencies.should == expected
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'with a complex configuration, intermixing dependent types' do
|
137
178
|
let (:stages) {
|
138
179
|
{
|
139
|
-
writers:
|
140
|
-
|
141
|
-
|
142
|
-
publishers: [ { Publishers::Publisher => :emailers } ],
|
143
|
-
messengers:
|
144
|
-
uploaders:
|
145
|
-
emailers:
|
146
|
-
notifiers:
|
180
|
+
writers: [ { Writers::ContentWriter => [:publishers, Uploaders::Rsync] },
|
181
|
+
{ Writers::AnotherContentWriter => [Emailers::Email] }
|
182
|
+
],
|
183
|
+
publishers: {[ { Publishers::Publisher => :emailers } ] => :notifiers},
|
184
|
+
messengers: [ { Messengers::SMS => :uploaders } ],
|
185
|
+
uploaders: [ { Uploaders::Rsync => Notifiers::Twitter } ],
|
186
|
+
emailers: [ Emailers::Email, Emailers::AnotherEmail ],
|
187
|
+
notifiers: [ Notifiers::Twitter ]
|
147
188
|
}
|
148
189
|
}
|
149
190
|
|
150
191
|
it 'resolves all dependencies' do
|
151
192
|
expected = {
|
152
193
|
writers: [ { Writers::ContentWriter => [Publishers::Publisher, Emailers::Email, Emailers::AnotherEmail,
|
153
|
-
|
194
|
+
Notifiers::Twitter, Uploaders::Rsync] },
|
154
195
|
{ Writers::AnotherContentWriter => [Emailers::Email] }
|
155
196
|
],
|
156
|
-
publishers: [ { Publishers::Publisher => [Emailers::Email, Emailers::AnotherEmail] } ],
|
197
|
+
publishers: [ { Publishers::Publisher => [Emailers::Email, Emailers::AnotherEmail, Notifiers::Twitter] } ],
|
157
198
|
messengers: [ { Messengers::SMS => [Uploaders::Rsync, Notifiers::Twitter] } ],
|
158
199
|
uploaders: [ { Uploaders::Rsync => [Notifiers::Twitter] } ],
|
159
200
|
emailers: [ { Emailers::Email => [] },
|
data/spec/pipes/store_spec.rb
CHANGED
data/spec/pipes_spec.rb
CHANGED
@@ -43,4 +43,13 @@ describe Pipes do
|
|
43
43
|
|
44
44
|
end
|
45
45
|
|
46
|
+
describe '.enqueue' do
|
47
|
+
|
48
|
+
it 'delegates to Runner.run' do
|
49
|
+
Pipes::Runner.should_receive(:run).with(Writers::ContentWriter, 'some arg', {resolve: true})
|
50
|
+
Pipes.enqueue(Writers::ContentWriter, 'some arg', {resolve: true})
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
46
55
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pipes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: resque
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirement: &70304995066400 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,15 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.22.0
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ~>
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: 1.22.0
|
24
|
+
version_requirements: *70304995066400
|
30
25
|
- !ruby/object:Gem::Dependency
|
31
26
|
name: redis-objects
|
32
|
-
requirement: !ruby/object:Gem::Requirement
|
27
|
+
requirement: &70304995064640 !ruby/object:Gem::Requirement
|
33
28
|
none: false
|
34
29
|
requirements:
|
35
30
|
- - ~>
|
@@ -37,15 +32,10 @@ dependencies:
|
|
37
32
|
version: 0.5.3
|
38
33
|
type: :runtime
|
39
34
|
prerelease: false
|
40
|
-
version_requirements:
|
41
|
-
none: false
|
42
|
-
requirements:
|
43
|
-
- - ~>
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
version: 0.5.3
|
35
|
+
version_requirements: *70304995064640
|
46
36
|
- !ruby/object:Gem::Dependency
|
47
37
|
name: abyss
|
48
|
-
requirement: !ruby/object:Gem::Requirement
|
38
|
+
requirement: &70304995063140 !ruby/object:Gem::Requirement
|
49
39
|
none: false
|
50
40
|
requirements:
|
51
41
|
- - ~>
|
@@ -53,15 +43,10 @@ dependencies:
|
|
53
43
|
version: 0.4.0
|
54
44
|
type: :runtime
|
55
45
|
prerelease: false
|
56
|
-
version_requirements:
|
57
|
-
none: false
|
58
|
-
requirements:
|
59
|
-
- - ~>
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 0.4.0
|
46
|
+
version_requirements: *70304995063140
|
62
47
|
- !ruby/object:Gem::Dependency
|
63
48
|
name: rspec
|
64
|
-
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirement: &70304995061340 !ruby/object:Gem::Requirement
|
65
50
|
none: false
|
66
51
|
requirements:
|
67
52
|
- - ! '>='
|
@@ -69,12 +54,18 @@ dependencies:
|
|
69
54
|
version: '0'
|
70
55
|
type: :development
|
71
56
|
prerelease: false
|
72
|
-
version_requirements:
|
57
|
+
version_requirements: *70304995061340
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rake
|
60
|
+
requirement: &70304995059980 !ruby/object:Gem::Requirement
|
73
61
|
none: false
|
74
62
|
requirements:
|
75
63
|
- - ! '>='
|
76
64
|
- !ruby/object:Gem::Version
|
77
65
|
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70304995059980
|
78
69
|
description: A Redis-backed concurrency management system
|
79
70
|
email:
|
80
71
|
- mikepackdev@gmail.com
|
@@ -85,6 +76,7 @@ files:
|
|
85
76
|
- .gitignore
|
86
77
|
- .rspec
|
87
78
|
- .rvmrc
|
79
|
+
- .travis.yml
|
88
80
|
- Gemfile
|
89
81
|
- LICENSE.txt
|
90
82
|
- README.md
|
@@ -125,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
117
|
version: '0'
|
126
118
|
requirements: []
|
127
119
|
rubyforge_project:
|
128
|
-
rubygems_version: 1.8.
|
120
|
+
rubygems_version: 1.8.10
|
129
121
|
signing_key:
|
130
122
|
specification_version: 3
|
131
123
|
summary: A Redis-backed concurrency management system
|
@@ -138,3 +130,4 @@ test_files:
|
|
138
130
|
- spec/pipes/utils_spec.rb
|
139
131
|
- spec/pipes_spec.rb
|
140
132
|
- spec/spec_helper.rb
|
133
|
+
has_rdoc:
|