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.
@@ -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