pipes 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ services:
5
+ - redis-server
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 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.
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 your conforming to the Resque API in your jobs, so you might have the following:
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::Runner.run([Writers::HTMLWriter, Publishers::Rsyncer])
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
- Pipes::Runner.run([Writers::HTMLWriter], 'http://localhost:3000/page')
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::Runner.run([Writers::HTMLWriter], 'google.com', 80)
150
+ Pipes.enqueue(Writers::HTMLWriter, 'google.com', 80)
138
151
  ```
139
152
 
140
- ## Defining Stage Dependencies
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::Runner.run([Writers::HTMLWriter], 'http://localhost:3000/page')
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
- You can turn off dependency resolution by passing in some additional Pipes options as the third argument:
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::Runner.run([Writers::HTMLWriter], 'http://localhost:3000/page', {resolve: false})
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
- In the above code, only `Writers::HTMLWriter` will be run.
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::Runner.run(Writers::HTMLWriter)
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::Runner.run('Writers::HTMLWriter')
384
+ Pipes.enqueue('Writers::HTMLWriter')
265
385
 
266
386
  # An entire stage
267
- Pipes::Runner.run(:content_writers)
387
+ Pipes.enqueue(:content_writers)
268
388
 
269
389
  # You can pass an array of any of the above, intermixing types
270
- Pipes::Runner.run([:content_writers, 'Publishers::CDNUploader', Notifiers::FileActivator])
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::Runner.run(...) (default true):
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 is currently tested under Ruby 1.9.3.
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::Runner.run([Writers::HTMLWriter], {follow_links: true})
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::Runner.run([Writers::HTMLWriter], {follow_links: true}, {})
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::Runner.run([Writers::HTMLWriter], {follow_links: true}, {resolve: true})
621
+ Pipes.enqueue([Writers::HTMLWriter], {follow_links: true}, {resolve: true})
315
622
  ```
316
623
 
317
624
  ## Future Improvements
@@ -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)
@@ -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{ |strat| dependents_for(strat) }
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, stage)|
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] = stage.inject([]) do |resolved_stage, job|
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, stage|
77
- stage.each do |job|
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
- strat, dependents = job.to_a.first
81
- @dependencies[strat] = dependencies_for_job(dependents)
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 |klasses, job|
122
+ stage.inject([]) do |jobs, job|
111
123
  # Does the job have dependents?
112
124
  if job.is_a? Hash
113
- strat, dependents = job.to_a.first
114
- klasses << strat
115
- klasses << dependencies_for_job(dependents)
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
- klasses << [job] + dependents_for(job)
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(stage)
142
- stage.inject([]) do |arr, job|
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]
@@ -1,3 +1,3 @@
1
1
  module Pipes
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -21,4 +21,5 @@ Gem::Specification.new do |gem|
21
21
  gem.add_dependency 'abyss', '~> 0.4.0'
22
22
 
23
23
  gem.add_development_dependency 'rspec'
24
+ gem.add_development_dependency 'rake'
24
25
  end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'mock_jobs'
3
2
 
4
3
  describe Pipes::Runner do
5
4
 
@@ -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 comlex configuration, intermixing dependent types' do
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: [ { Writers::ContentWriter => [:publishers, Uploaders::Rsync] },
140
- { Writers::AnotherContentWriter => [Emailers::Email] }
141
- ],
142
- publishers: [ { Publishers::Publisher => :emailers } ],
143
- messengers: [ { Messengers::SMS => :uploaders } ],
144
- uploaders: [ { Uploaders::Rsync => Notifiers::Twitter } ],
145
- emailers: [ Emailers::Email, Emailers::AnotherEmail ],
146
- notifiers: [ Notifiers::Twitter ]
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
- Uploaders::Rsync, Notifiers::Twitter] },
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 => [] },
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'mock_jobs'
3
2
 
4
3
  describe Pipes::Store do
5
4
 
@@ -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
@@ -1,6 +1,8 @@
1
1
  require 'rspec'
2
2
  RSpec::Mocks.setup(Object)
3
3
 
4
+ require 'mock_jobs'
5
+
4
6
  require 'pipes'
5
7
 
6
8
  Pipes.namespace = 'test'
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.1.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-21 00:00:00.000000000 Z
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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: !ruby/object:Gem::Requirement
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.24
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: