adrian 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ # Contributing
2
+
3
+ 1. Fork it
4
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
5
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
6
+ 4. Push to the branch (`git push origin my-new-feature`)
7
+ 5. Create new Pull Request
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ module Adrian
2
+ class Queue
3
+ def pop
4
+ raise "#{self.class.name}#pop is not defined"
5
+ end
6
+
7
+ def push(item)
8
+ raise "#{self.class.name}#push is not defined"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Adrian
2
+ class QueueItem
3
+ attr_reader :value, :created_at
4
+
5
+ def initialize(value, created_at = Time.now)
6
+ @value = value
7
+ @created_at = created_at
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Adrian
2
+ VERSION = '1.0.0'
3
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'debugger'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+
7
+ require 'adrian'
8
+
9
+ require 'minitest/autorun'
@@ -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