adrian 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|