adrian 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTING.md +7 -0
- data/README.md +23 -0
- data/lib/adrian.rb +13 -0
- data/lib/adrian/array_queue.rb +26 -0
- data/lib/adrian/composite_queue.rb +22 -0
- data/lib/adrian/directory_queue.rb +85 -0
- data/lib/adrian/dispatcher.rb +57 -0
- data/lib/adrian/failure_handler.rb +36 -0
- data/lib/adrian/file_item.rb +36 -0
- data/lib/adrian/filters.rb +56 -0
- data/lib/adrian/girl_friday_dispatcher.rb +40 -0
- data/lib/adrian/queue.rb +11 -0
- data/lib/adrian/queue_item.rb +10 -0
- data/lib/adrian/version.rb +3 -0
- data/lib/adrian/worker.rb +29 -0
- data/test/array_queue_test.rb +29 -0
- data/test/composite_queue_test.rb +36 -0
- data/test/directory_queue_test.rb +141 -0
- data/test/dispatcher_lifecycle_test.rb +82 -0
- data/test/dispatcher_test.rb +52 -0
- data/test/failure_handler_test.rb +44 -0
- data/test/file_item_test.rb +63 -0
- data/test/filters_test.rb +105 -0
- data/test/girl_friday_dispatcher_test.rb +28 -0
- data/test/test_helper.rb +9 -0
- data/test/worker_test.rb +38 -0
- metadata +152 -0
data/CONTRIBUTING.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Adrian
|
2
|
+
|
3
|
+
Adrian is a work dispatcher and some queue implementations.
|
4
|
+
Adrian does not do any real work, but is really good at delegating it.
|
5
|
+
Adrian **does not care how** real work get's done, but **makes damn sure that it does get done**.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'adrian'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install adrian
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
data/lib/adrian.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'adrian/version'
|
2
|
+
|
3
|
+
module Adrian
|
4
|
+
autoload :ArrayQueue, 'adrian/array_queue'
|
5
|
+
autoload :CompositeQueue, 'adrian/composite_queue'
|
6
|
+
autoload :DirectoryQueue, 'adrian/directory_queue'
|
7
|
+
autoload :Dispatcher, 'adrian/dispatcher'
|
8
|
+
autoload :FileItem, 'adrian/file_item'
|
9
|
+
autoload :Filters, 'adrian/filters'
|
10
|
+
autoload :GirlFridayDispatcher, 'adrian/girl_friday_dispatcher'
|
11
|
+
autoload :QueueItem, 'adrian/queue_item'
|
12
|
+
autoload :Worker, 'adrian/worker'
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'adrian/queue'
|
2
|
+
|
3
|
+
module Adrian
|
4
|
+
class ArrayQueue < Queue
|
5
|
+
def initialize(array = [])
|
6
|
+
@array = array.map { |item| wrap_item(item) }
|
7
|
+
@mutex = Mutex.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def pop
|
11
|
+
@mutex.synchronize { @array.shift }
|
12
|
+
end
|
13
|
+
|
14
|
+
def push(item)
|
15
|
+
item = wrap_item(item)
|
16
|
+
@mutex.synchronize { @array << item }
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def wrap_item(item)
|
23
|
+
item.is_a?(QueueItem) ? item : QueueItem.new(item)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'adrian/queue'
|
2
|
+
|
3
|
+
module Adrian
|
4
|
+
class CompositeQueue < Queue
|
5
|
+
def initialize(*queues)
|
6
|
+
@queues = queues.flatten
|
7
|
+
end
|
8
|
+
|
9
|
+
def pop
|
10
|
+
@queues.each do |q|
|
11
|
+
item = q.pop
|
12
|
+
return item if item
|
13
|
+
end
|
14
|
+
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def push(item)
|
19
|
+
raise "You can not push item to a composite queue"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Adrian
|
4
|
+
class DirectoryQueue < Queue
|
5
|
+
include Filters
|
6
|
+
|
7
|
+
def self.create(options = {})
|
8
|
+
queue = new(options)
|
9
|
+
FileUtils.mkdir_p(queue.available_path)
|
10
|
+
FileUtils.mkdir_p(queue.reserved_path)
|
11
|
+
queue
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :available_path, :reserved_path
|
15
|
+
|
16
|
+
# Note:
|
17
|
+
# There is the possibility of an item being consumed by multiple processes when its still in the queue after its lock expires.
|
18
|
+
# The reason for allowing this is:
|
19
|
+
# 1. It's much simpler than introducing a seperate monitoring process to handle lock expiry.
|
20
|
+
# 2. This is an acceptable and rare event. e.g. it only happens when the process working on the item crashes without being able to release the lock
|
21
|
+
def initialize(options = {})
|
22
|
+
@available_path = options.fetch(:path)
|
23
|
+
@reserved_path = options.fetch(:reserved_path, default_reserved_path)
|
24
|
+
filters << Filters::FileLock.new(:duration => options[:lock_duration], :reserved_path => reserved_path)
|
25
|
+
filters << Filters::Delay.new(:duration => options[:delay]) if options[:delay]
|
26
|
+
end
|
27
|
+
|
28
|
+
def pop
|
29
|
+
items.each do |item|
|
30
|
+
return item if reserve(item)
|
31
|
+
end
|
32
|
+
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def push(value)
|
37
|
+
item = wrap_item(value)
|
38
|
+
item.move(@available_path)
|
39
|
+
item.touch
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def include?(value)
|
44
|
+
item = wrap_item(value)
|
45
|
+
items.include?(item)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def wrap_item(value)
|
51
|
+
value.is_a?(FileItem) ? value : FileItem.new(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def reserve(item)
|
55
|
+
item.move(@reserved_path)
|
56
|
+
item.touch
|
57
|
+
true
|
58
|
+
rescue Errno::ENOENT => e
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
def items
|
63
|
+
items = files.map { |file| FileItem.new(file) }
|
64
|
+
items.reject! { |item| filter?(item) }
|
65
|
+
items.sort_by(&:updated_at)
|
66
|
+
end
|
67
|
+
|
68
|
+
def files
|
69
|
+
(available_files + reserved_files).select { |file| File.file?(file) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def available_files
|
73
|
+
Dir.glob("#{@available_path}/*")
|
74
|
+
end
|
75
|
+
|
76
|
+
def reserved_files
|
77
|
+
Dir.glob("#{@reserved_path}/*")
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_reserved_path
|
81
|
+
File.join(File.dirname(@available_path), 'cur')
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'adrian/failure_handler'
|
2
|
+
|
3
|
+
module Adrian
|
4
|
+
class Dispatcher
|
5
|
+
attr_reader :running
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@failure_handler = FailureHandler.new
|
9
|
+
@stop_when_done = !!options[:stop_when_done]
|
10
|
+
@sleep = options[:sleep] || 0.5
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_failure(*exceptions)
|
15
|
+
@failure_handler.add_rule(*exceptions, &Proc.new)
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_done
|
19
|
+
@failure_handler.add_rule(nil, &Proc.new)
|
20
|
+
end
|
21
|
+
|
22
|
+
def start(queue, worker_class)
|
23
|
+
@running = true
|
24
|
+
|
25
|
+
while @running do
|
26
|
+
if item = queue.pop
|
27
|
+
delegate_work(item, worker_class)
|
28
|
+
else
|
29
|
+
if @stop_when_done
|
30
|
+
stop
|
31
|
+
else
|
32
|
+
sleep(@sleep) if @sleep
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop
|
39
|
+
@running = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def delegate_work(item, worker_class)
|
43
|
+
worker = worker_class.new(item)
|
44
|
+
worker.report_to(self)
|
45
|
+
worker.perform
|
46
|
+
end
|
47
|
+
|
48
|
+
def work_done(item, worker, exception = nil)
|
49
|
+
if handler = @failure_handler.handle(exception)
|
50
|
+
handler.call(item, worker, exception)
|
51
|
+
else
|
52
|
+
raise exception if exception
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Adrian
|
2
|
+
class FailureHandler
|
3
|
+
def initialize
|
4
|
+
@rules = []
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_rule(*exceptions, &block)
|
8
|
+
exceptions.each do |exception_class|
|
9
|
+
@rules << Rule.new(exception_class, block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def handle(exception)
|
14
|
+
if rule = @rules.find { |r| r.match(exception) }
|
15
|
+
rule.block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Rule
|
20
|
+
attr_reader :block
|
21
|
+
|
22
|
+
def initialize(exception_class, block)
|
23
|
+
@exception_class = exception_class
|
24
|
+
@block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
def match(exception)
|
28
|
+
return @exception_class.nil? if exception.nil?
|
29
|
+
|
30
|
+
return false if @exception_class.nil?
|
31
|
+
|
32
|
+
exception.is_a?(@exception_class)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Adrian
|
2
|
+
class FileItem < QueueItem
|
3
|
+
|
4
|
+
def path
|
5
|
+
value
|
6
|
+
end
|
7
|
+
|
8
|
+
def name
|
9
|
+
File.basename(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
other.respond_to?(:name) &&
|
14
|
+
name == other.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def move(destination)
|
18
|
+
destination_path = File.join(destination, File.basename(path))
|
19
|
+
File.rename(path, destination_path)
|
20
|
+
@value = destination_path
|
21
|
+
end
|
22
|
+
|
23
|
+
def updated_at
|
24
|
+
File.mtime(path).utc if exist?
|
25
|
+
end
|
26
|
+
|
27
|
+
def touch(updated_at = Time.new)
|
28
|
+
File.utime(updated_at, updated_at, path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def exist?
|
32
|
+
File.exist?(path)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Adrian
|
2
|
+
module Filters
|
3
|
+
|
4
|
+
def filters
|
5
|
+
@filters ||= []
|
6
|
+
end
|
7
|
+
|
8
|
+
def filter?(item)
|
9
|
+
!filters.all? { |filter| filter.allow?(item) }
|
10
|
+
end
|
11
|
+
|
12
|
+
class Delay
|
13
|
+
FIFTEEN_MINUTES = 900
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def allow?(item)
|
20
|
+
item.updated_at <= (Time.new - duration)
|
21
|
+
end
|
22
|
+
|
23
|
+
def duration
|
24
|
+
@options[:duration] ||= FIFTEEN_MINUTES
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
class FileLock
|
30
|
+
ONE_HOUR = 3_600
|
31
|
+
|
32
|
+
def initialize(options = {})
|
33
|
+
@options = options
|
34
|
+
@reserved_path = @options.fetch(:reserved_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
def allow?(item)
|
38
|
+
!locked?(item) || lock_expired?(item)
|
39
|
+
end
|
40
|
+
|
41
|
+
def lock_expired?(item)
|
42
|
+
item.updated_at <= (Time.new - duration)
|
43
|
+
end
|
44
|
+
|
45
|
+
def locked?(item)
|
46
|
+
@reserved_path == File.dirname(item.path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def duration
|
50
|
+
@options[:duration] ||= ONE_HOUR
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'girl_friday'
|
2
|
+
|
3
|
+
module Adrian
|
4
|
+
class GirlFridayDispatcher < Dispatcher
|
5
|
+
def gf_queue_name
|
6
|
+
@options[:name] || 'adrian_queue'
|
7
|
+
end
|
8
|
+
|
9
|
+
def gf_queue_size
|
10
|
+
@options[:size]
|
11
|
+
end
|
12
|
+
|
13
|
+
def gf_queue
|
14
|
+
@gf_queue ||= GirlFriday::WorkQueue.new(gf_queue_name, :size => gf_queue_size) do |item, worker_class|
|
15
|
+
worker = worker_class.new(item)
|
16
|
+
worker.report_to(self)
|
17
|
+
worker.perform
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def delegate_work(item, worker_class)
|
22
|
+
gf_queue.push([item, worker_class])
|
23
|
+
end
|
24
|
+
|
25
|
+
def wait_for_empty
|
26
|
+
gf_queue.wait_for_empty
|
27
|
+
|
28
|
+
sleep(0.5)
|
29
|
+
|
30
|
+
while gf_queue.status[gf_queue_name][:busy] != 0
|
31
|
+
sleep(0.5)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
super
|
37
|
+
wait_for_empty
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/adrian/queue.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Adrian
|
2
|
+
class Worker
|
3
|
+
attr_reader :item
|
4
|
+
|
5
|
+
def initialize(item)
|
6
|
+
@item = item
|
7
|
+
end
|
8
|
+
|
9
|
+
def report_to(boss)
|
10
|
+
@boss = boss
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
exception = nil
|
15
|
+
|
16
|
+
begin
|
17
|
+
work
|
18
|
+
rescue Exception => e
|
19
|
+
exception = e
|
20
|
+
end
|
21
|
+
|
22
|
+
@boss.work_done(item, self, exception) if @boss
|
23
|
+
end
|
24
|
+
|
25
|
+
def work
|
26
|
+
raise "You need to implement #{self.class.name}#work"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Adrian::ArrayQueue do
|
4
|
+
it 'should allow construction with an array' do
|
5
|
+
q = Adrian::ArrayQueue.new([1,2,3])
|
6
|
+
q.pop.value.must_equal 1
|
7
|
+
q.pop.value.must_equal 2
|
8
|
+
q.pop.value.must_equal 3
|
9
|
+
q.pop.must_be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should allow construction without an array' do
|
13
|
+
q = Adrian::ArrayQueue.new
|
14
|
+
q.pop.must_be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should act as a queue' do
|
18
|
+
q = Adrian::ArrayQueue.new
|
19
|
+
|
20
|
+
q.push(1)
|
21
|
+
q.push(2)
|
22
|
+
q.push(3)
|
23
|
+
|
24
|
+
q.pop.value.must_equal 1
|
25
|
+
q.pop.value.must_equal 2
|
26
|
+
q.pop.value.must_equal 3
|
27
|
+
q.pop.must_be_nil
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Adrian::CompositeQueue do
|
4
|
+
before do
|
5
|
+
@q1 = Adrian::ArrayQueue.new
|
6
|
+
@q2 = Adrian::ArrayQueue.new
|
7
|
+
@q = Adrian::CompositeQueue.new(@q1, @q2)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "popping" do
|
11
|
+
it 'should return nil when all queues are empty' do
|
12
|
+
@q.pop.must_be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should return an item from the first queue that has items' do
|
16
|
+
@q1.push(1)
|
17
|
+
@q1.push(2)
|
18
|
+
@q2.push(3)
|
19
|
+
@q2.push(4)
|
20
|
+
|
21
|
+
@q.pop.value.must_equal(1)
|
22
|
+
@q.pop.value.must_equal(2)
|
23
|
+
@q.pop.value.must_equal(3)
|
24
|
+
@q.pop.value.must_equal(4)
|
25
|
+
@q.pop.must_be_nil
|
26
|
+
@q1.pop.must_be_nil
|
27
|
+
@q2.pop.must_be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "pushing" do
|
32
|
+
it "should not be allowed" do
|
33
|
+
lambda { @q.push(1) }.must_raise(RuntimeError)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
describe Adrian::DirectoryQueue do
|
7
|
+
before do
|
8
|
+
@q = Adrian::DirectoryQueue.create(:path => Dir.mktmpdir('dir_queue_test'))
|
9
|
+
end
|
10
|
+
|
11
|
+
after do
|
12
|
+
FileUtils.rm_r(@q.available_path, :force => true)
|
13
|
+
FileUtils.rm_r(@q.reserved_path, :force => true)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should act as a queue for files' do
|
17
|
+
item1 = Tempfile.new('item1-').path
|
18
|
+
item2 = Tempfile.new('item2-').path
|
19
|
+
item3 = Tempfile.new('item3-').path
|
20
|
+
|
21
|
+
@q.push(item1)
|
22
|
+
@q.push(item2)
|
23
|
+
@q.push(item3)
|
24
|
+
|
25
|
+
@q.pop.must_equal Adrian::FileItem.new(item1)
|
26
|
+
@q.pop.must_equal Adrian::FileItem.new(item2)
|
27
|
+
@q.pop.must_equal Adrian::FileItem.new(item3)
|
28
|
+
@q.pop.must_be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'file backend' do
|
32
|
+
|
33
|
+
describe 'pop' do
|
34
|
+
before do
|
35
|
+
@item = Adrian::FileItem.new(Tempfile.new('item').path)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'provides an available file' do
|
39
|
+
@q.push(@item)
|
40
|
+
assert_equal @item, @q.pop
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'moves the file to the reserved directory' do
|
44
|
+
@q.push(@item)
|
45
|
+
original_path = @item.path
|
46
|
+
item = @q.pop
|
47
|
+
assert_equal @item, item
|
48
|
+
|
49
|
+
assert_equal false, File.exist?(original_path)
|
50
|
+
assert_equal true, File.exist?(File.join(@q.reserved_path, @item.name))
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'reserves the file for an hour by default' do
|
54
|
+
@q.push(@item)
|
55
|
+
reserved_item = @q.pop
|
56
|
+
assert reserved_item
|
57
|
+
one_hour = 3_600
|
58
|
+
|
59
|
+
Time.stub(:new, reserved_item.updated_at + one_hour - 1) do
|
60
|
+
assert_equal nil, @q.pop
|
61
|
+
end
|
62
|
+
|
63
|
+
Time.stub(:new, reserved_item.updated_at + one_hour) do
|
64
|
+
assert_equal @item, @q.pop
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'touches the item' do
|
70
|
+
@q.push(@item)
|
71
|
+
now = Time.new - 100
|
72
|
+
item = nil
|
73
|
+
Time.stub(:new, now) { item = @q.pop }
|
74
|
+
|
75
|
+
assert_equal now.to_i, item.updated_at.to_i
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'skips the file when moved by another process' do
|
79
|
+
def @q.files
|
80
|
+
[ 'no/longer/exists' ]
|
81
|
+
end
|
82
|
+
assert_equal nil, @q.pop
|
83
|
+
end
|
84
|
+
|
85
|
+
it "only provides normal files" do
|
86
|
+
not_file = Dir.mktmpdir(@q.available_path, 'directory_queue_x')
|
87
|
+
assert_equal nil, @q.pop
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'push' do
|
93
|
+
before do
|
94
|
+
@item = Adrian::FileItem.new(Tempfile.new('item').path)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'moves the file to the available directory' do
|
98
|
+
original_path = @item.path
|
99
|
+
@q.push(@item)
|
100
|
+
|
101
|
+
assert_equal false, File.exist?(original_path)
|
102
|
+
assert_equal true, File.exist?(File.join(@q.available_path, @item.name))
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'touches the item' do
|
106
|
+
now = Time.new - 100
|
107
|
+
Time.stub(:new, now) { @q.push(@item) }
|
108
|
+
|
109
|
+
assert_equal now.to_i, @item.updated_at.to_i
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'filters' do
|
115
|
+
it 'should add a delay filter if the :delay option is given' do
|
116
|
+
q = Adrian::DirectoryQueue.create(:path => Dir.mktmpdir('dir_queue_test'))
|
117
|
+
filter = q.filters.find {|filter| filter.is_a?(Adrian::Filters::Delay)}
|
118
|
+
filter.must_equal nil
|
119
|
+
|
120
|
+
q = Adrian::DirectoryQueue.create(:path => Dir.mktmpdir('dir_queue_test'), :delay => 300)
|
121
|
+
filter = q.filters.find {|filter| filter.is_a?(Adrian::Filters::Delay)}
|
122
|
+
filter.wont_equal nil
|
123
|
+
filter.duration.must_equal 300
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should add a lock filter that can be configured with the :lock_duration option' do
|
127
|
+
q = Adrian::DirectoryQueue.create(:path => Dir.mktmpdir('dir_queue_test'))
|
128
|
+
filter = q.filters.find {|filter| filter.is_a?(Adrian::Filters::FileLock)}
|
129
|
+
filter.wont_equal nil
|
130
|
+
filter.duration.must_equal 3600 # default value
|
131
|
+
|
132
|
+
q = Adrian::DirectoryQueue.create(:path => Dir.mktmpdir('dir_queue_test'), :lock_duration => 300)
|
133
|
+
filter = q.filters.find {|filter| filter.is_a?(Adrian::Filters::FileLock)}
|
134
|
+
filter.wont_equal nil
|
135
|
+
filter.duration.must_equal 300
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe "Adrian::Dispatcher lifecycle" do
|
4
|
+
class Worker < Adrian::Worker
|
5
|
+
def work
|
6
|
+
$done_items << item.value
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
before do
|
11
|
+
$done_items = []
|
12
|
+
@q = Adrian::ArrayQueue.new([1,2,3])
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "stop_when_done" do
|
16
|
+
describe "set to true" do
|
17
|
+
before do
|
18
|
+
@dispatcher = Adrian::Dispatcher.new(:stop_when_done => true)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should have all the work done and stop" do
|
22
|
+
t = Thread.new do
|
23
|
+
@dispatcher.start(@q, Worker)
|
24
|
+
end
|
25
|
+
|
26
|
+
sleep(0.5)
|
27
|
+
|
28
|
+
@q.pop.must_be_nil
|
29
|
+
|
30
|
+
$done_items.must_equal([1,2,3])
|
31
|
+
|
32
|
+
@dispatcher.running.must_equal false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "set to false" do
|
37
|
+
before do
|
38
|
+
@dispatcher = Adrian::Dispatcher.new(:stop_when_done => false)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should have all the work done and continue" do
|
42
|
+
t = Thread.new do
|
43
|
+
@dispatcher.start(@q, Worker)
|
44
|
+
end
|
45
|
+
|
46
|
+
sleep(0.5)
|
47
|
+
|
48
|
+
@q.pop.must_be_nil
|
49
|
+
|
50
|
+
$done_items.must_equal([1,2,3])
|
51
|
+
|
52
|
+
@dispatcher.running.must_equal true
|
53
|
+
t.kill
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#stop" do
|
59
|
+
before do
|
60
|
+
@dispatcher = Adrian::Dispatcher.new(:sleep => 0.1)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should stop a running dispatcher" do
|
64
|
+
t = Thread.new do
|
65
|
+
@dispatcher.start(@q, Worker)
|
66
|
+
end
|
67
|
+
|
68
|
+
sleep(0.5)
|
69
|
+
|
70
|
+
@dispatcher.running.must_equal true
|
71
|
+
t.status.wont_equal false
|
72
|
+
|
73
|
+
@dispatcher.stop
|
74
|
+
|
75
|
+
sleep(0.5)
|
76
|
+
|
77
|
+
@dispatcher.running.must_equal false
|
78
|
+
t.status.must_equal false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Adrian::Dispatcher do
|
4
|
+
before do
|
5
|
+
$done_items = []
|
6
|
+
@q = Adrian::ArrayQueue.new
|
7
|
+
@dispatcher = Adrian::Dispatcher.new(:stop_when_done => true)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "work delegation" do
|
11
|
+
it "should instantiate an instance of the worker for each item and ask it to perform" do
|
12
|
+
worker = Class.new do
|
13
|
+
def initialize(item)
|
14
|
+
@item = item
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
$done_items << [@boss, @item.value]
|
19
|
+
end
|
20
|
+
|
21
|
+
def report_to(boss)
|
22
|
+
@boss = boss
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@q.push(1)
|
27
|
+
@q.push(2)
|
28
|
+
@q.push(3)
|
29
|
+
|
30
|
+
@dispatcher.start(@q, worker)
|
31
|
+
|
32
|
+
$done_items.must_equal([[@dispatcher, 1], [@dispatcher, 2], [@dispatcher, 3]])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "work evaluation" do
|
37
|
+
it "should use the failure handler to handle the result" do
|
38
|
+
@dispatcher.on_failure(RuntimeError) do |item, worker, exception|
|
39
|
+
@q.push(item)
|
40
|
+
end
|
41
|
+
|
42
|
+
@dispatcher.work_done(1, nil)
|
43
|
+
@q.pop.must_be_nil
|
44
|
+
|
45
|
+
@dispatcher.work_done(1, nil, nil)
|
46
|
+
@q.pop.must_be_nil
|
47
|
+
|
48
|
+
@dispatcher.work_done(1, nil, RuntimeError.new)
|
49
|
+
@q.pop.value.must_equal 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
require 'adrian/failure_handler'
|
4
|
+
|
5
|
+
describe Adrian::FailureHandler do
|
6
|
+
before do
|
7
|
+
@handler = Adrian::FailureHandler.new
|
8
|
+
|
9
|
+
$failure = nil
|
10
|
+
|
11
|
+
@handler.add_rule(RuntimeError) { :runtime }
|
12
|
+
@handler.add_rule(StandardError) { :standard }
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should match rules in the order they were added" do
|
16
|
+
block = @handler.handle(RuntimeError.new)
|
17
|
+
assert block
|
18
|
+
block.call.must_equal :runtime
|
19
|
+
|
20
|
+
block = @handler.handle(StandardError.new)
|
21
|
+
assert block
|
22
|
+
block.call.must_equal :standard
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should do nothing when no rules match" do
|
26
|
+
@handler.handle(Exception.new).must_be_nil
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "the success rule" do
|
30
|
+
before do
|
31
|
+
@handler = Adrian::FailureHandler.new
|
32
|
+
@handler.add_rule(nil) { :success }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should match when there is no exception" do
|
36
|
+
@handler.handle(RuntimeError.new).must_be_nil
|
37
|
+
|
38
|
+
block = @handler.handle(nil)
|
39
|
+
assert block
|
40
|
+
block.call.must_equal :success
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
describe Adrian::FileItem do
|
5
|
+
before do
|
6
|
+
@item = Adrian::FileItem.new(Tempfile.new('file_item_test').path)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'aliases value as path' do
|
10
|
+
item = Adrian::FileItem.new('path/a')
|
11
|
+
assert_equal 'path/a', item.value
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'has a name from the path' do
|
15
|
+
item = Adrian::FileItem.new('path/name.ext')
|
16
|
+
assert_equal 'name.ext', item.name
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'is equal to another item when they have the same name' do
|
20
|
+
item1 = Adrian::FileItem.new('path/a')
|
21
|
+
item2 = Adrian::FileItem.new('path/b')
|
22
|
+
assert(item1 != item2)
|
23
|
+
|
24
|
+
item3 = Adrian::FileItem.new('path/a')
|
25
|
+
assert_equal item1, item3
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'move' do
|
29
|
+
before do
|
30
|
+
@destination = Dir.mktmpdir('file_item_move_test')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'moves the file to the given directory' do
|
34
|
+
@item.move(@destination)
|
35
|
+
assert_equal true, File.exist?(File.join(@destination, @item.name))
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'updates the path to its new location' do
|
39
|
+
@item.move(@destination)
|
40
|
+
assert_equal @destination, File.dirname(@item.path)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'touch' do
|
46
|
+
|
47
|
+
it 'changes the update timestamp to the current time' do
|
48
|
+
now = Time.now - 100
|
49
|
+
Time.stub(:new, now) { @item.touch }
|
50
|
+
|
51
|
+
assert_equal now.to_i, @item.updated_at.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'exists when the file at the given path exists' do
|
57
|
+
assert_equal true, @item.exist?
|
58
|
+
File.unlink(@item.path)
|
59
|
+
|
60
|
+
assert_equal false, @item.exist?
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'tmpdir'
|
4
|
+
|
5
|
+
|
6
|
+
describe Adrian::Filters do
|
7
|
+
before do
|
8
|
+
@q = Object.new.extend(Adrian::Filters)
|
9
|
+
@item = Adrian::QueueItem.new("hello")
|
10
|
+
end
|
11
|
+
|
12
|
+
class FakeFilter
|
13
|
+
|
14
|
+
def initialize(options = {})
|
15
|
+
@allow = options.fetch(:allow)
|
16
|
+
end
|
17
|
+
|
18
|
+
def allow?(item)
|
19
|
+
@allow == true
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#filter?" do
|
25
|
+
|
26
|
+
it "is true when any filter denies the item" do
|
27
|
+
@q.filters << FakeFilter.new(:allow => true)
|
28
|
+
@q.filters << FakeFilter.new(:allow => false)
|
29
|
+
|
30
|
+
assert_equal true, @q.filter?(@item)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "is false when all filters allow the item" do
|
34
|
+
@q.filters << FakeFilter.new(:allow => true)
|
35
|
+
assert_equal false, @q.filter?(@item)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
module Updatable
|
41
|
+
attr_accessor :updated_at
|
42
|
+
end
|
43
|
+
|
44
|
+
describe Adrian::Filters::Delay do
|
45
|
+
before do
|
46
|
+
@filter = Adrian::Filters::Delay.new
|
47
|
+
@updatable_item = Adrian::QueueItem.new("hello")
|
48
|
+
@updatable_item.extend(Updatable)
|
49
|
+
@updatable_item.updated_at = Time.new
|
50
|
+
@fifteen_minutes = 900
|
51
|
+
end
|
52
|
+
|
53
|
+
it "allows items that have not been recently updated" do
|
54
|
+
Time.stub(:new, @updatable_item.updated_at + @fifteen_minutes) do
|
55
|
+
assert_equal true, @filter.allow?(@updatable_item)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "denies items that have been recently updated" do
|
60
|
+
assert_equal false, @filter.allow?(@updatable_item)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "has a configurable recently updated duration that defaults to 15 minutes" do
|
64
|
+
assert_equal @fifteen_minutes, @filter.duration
|
65
|
+
configured_filter = Adrian::Filters::Delay.new(:duration => 1)
|
66
|
+
|
67
|
+
assert_equal 1, configured_filter.duration
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
describe Adrian::Filters::FileLock do
|
73
|
+
before do
|
74
|
+
@filter = Adrian::Filters::FileLock.new(:reserved_path => 'path/to/locked')
|
75
|
+
@available_item = Adrian::FileItem.new("path/to/file")
|
76
|
+
@locked_item = Adrian::FileItem.new("path/to/locked/file")
|
77
|
+
@one_hour = 3_600
|
78
|
+
end
|
79
|
+
|
80
|
+
it "allows items that are not locked" do
|
81
|
+
assert_equal true, @filter.allow?(@available_item)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "allows items with an expired lock" do
|
85
|
+
@locked_item.stub(:updated_at, Time.new - @one_hour) do
|
86
|
+
assert_equal true, @filter.allow?(@locked_item)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it "does not allow items with a fresh lock" do
|
91
|
+
@locked_item.stub(:updated_at, Time.new) do
|
92
|
+
assert_equal false, @filter.allow?(@locked_item)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
it "has a configurable lock expiry duration that defaults to one hour" do
|
97
|
+
assert_equal @one_hour, @filter.duration
|
98
|
+
configured_filter = Adrian::Filters::FileLock.new(:duration => 1, :reserved_path => 'path/to/locked')
|
99
|
+
|
100
|
+
assert_equal 1, configured_filter.duration
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Adrian::GirlFridayDispatcher do
|
4
|
+
before do
|
5
|
+
$done_items = []
|
6
|
+
@q = Adrian::ArrayQueue.new
|
7
|
+
@dispatcher = Adrian::GirlFridayDispatcher.new(:stop_when_done => true)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "work delegation" do
|
11
|
+
it "should instantiate an instance of the worker for each item and ask it to perform" do
|
12
|
+
worker = Class.new(Adrian::Worker) do
|
13
|
+
def work
|
14
|
+
sleep(rand)
|
15
|
+
$done_items << @item.value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
@q.push(1)
|
20
|
+
@q.push(2)
|
21
|
+
@q.push(3)
|
22
|
+
|
23
|
+
@dispatcher.start(@q, worker)
|
24
|
+
|
25
|
+
$done_items.sort.must_equal([1, 2, 3])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/test/test_helper.rb
ADDED
data/test/worker_test.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Adrian::Worker do
|
4
|
+
describe "#perform" do
|
5
|
+
before { @item = 2}
|
6
|
+
|
7
|
+
it "should report back to the boss" do
|
8
|
+
worker_class = Class.new(Adrian::Worker) do
|
9
|
+
def work; item + 2; end
|
10
|
+
end
|
11
|
+
|
12
|
+
worker = worker_class.new(@item)
|
13
|
+
boss = MiniTest::Mock.new
|
14
|
+
worker.report_to(boss)
|
15
|
+
|
16
|
+
boss.expect(:work_done, nil, [@item, worker, nil])
|
17
|
+
worker.perform
|
18
|
+
|
19
|
+
boss.verify
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should NEVER raise an exception" do
|
23
|
+
worker_class = Class.new(Adrian::Worker) do
|
24
|
+
def work; raise "STRIKE!"; end
|
25
|
+
end
|
26
|
+
|
27
|
+
worker = worker_class.new(@item)
|
28
|
+
boss = MiniTest::Mock.new
|
29
|
+
worker.report_to(boss)
|
30
|
+
|
31
|
+
boss.expect(:work_done, nil, [@item, worker, RuntimeError])
|
32
|
+
|
33
|
+
worker.perform
|
34
|
+
|
35
|
+
boss.verify
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: adrian
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mick Staugaard
|
9
|
+
- Eric Chapweske
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-10-09 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: minitest
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: debugger
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: girl_friday
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
description: A work dispatcher and some queue implementations
|
80
|
+
email:
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- lib/adrian/array_queue.rb
|
86
|
+
- lib/adrian/composite_queue.rb
|
87
|
+
- lib/adrian/directory_queue.rb
|
88
|
+
- lib/adrian/dispatcher.rb
|
89
|
+
- lib/adrian/failure_handler.rb
|
90
|
+
- lib/adrian/file_item.rb
|
91
|
+
- lib/adrian/filters.rb
|
92
|
+
- lib/adrian/girl_friday_dispatcher.rb
|
93
|
+
- lib/adrian/queue.rb
|
94
|
+
- lib/adrian/queue_item.rb
|
95
|
+
- lib/adrian/version.rb
|
96
|
+
- lib/adrian/worker.rb
|
97
|
+
- lib/adrian.rb
|
98
|
+
- test/array_queue_test.rb
|
99
|
+
- test/composite_queue_test.rb
|
100
|
+
- test/directory_queue_test.rb
|
101
|
+
- test/dispatcher_lifecycle_test.rb
|
102
|
+
- test/dispatcher_test.rb
|
103
|
+
- test/failure_handler_test.rb
|
104
|
+
- test/file_item_test.rb
|
105
|
+
- test/filters_test.rb
|
106
|
+
- test/girl_friday_dispatcher_test.rb
|
107
|
+
- test/test_helper.rb
|
108
|
+
- test/worker_test.rb
|
109
|
+
- README.md
|
110
|
+
- CONTRIBUTING.md
|
111
|
+
homepage: https://github.com/staugaard/adrian
|
112
|
+
licenses: []
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ! '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
hash: -4434037212517463675
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
segments:
|
133
|
+
- 0
|
134
|
+
hash: -4434037212517463675
|
135
|
+
requirements: []
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 1.8.24
|
138
|
+
signing_key:
|
139
|
+
specification_version: 3
|
140
|
+
summary: Adrian does not do any real work, but is really good at delegating it
|
141
|
+
test_files:
|
142
|
+
- test/array_queue_test.rb
|
143
|
+
- test/composite_queue_test.rb
|
144
|
+
- test/directory_queue_test.rb
|
145
|
+
- test/dispatcher_lifecycle_test.rb
|
146
|
+
- test/dispatcher_test.rb
|
147
|
+
- test/failure_handler_test.rb
|
148
|
+
- test/file_item_test.rb
|
149
|
+
- test/filters_test.rb
|
150
|
+
- test/girl_friday_dispatcher_test.rb
|
151
|
+
- test/test_helper.rb
|
152
|
+
- test/worker_test.rb
|