girl_friday 0.9.6 → 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/History.md +7 -0
- data/README.md +2 -2
- data/examples/batch.rb +4 -1
- data/girl_friday.gemspec +1 -1
- data/lib/girl_friday/batch.rb +31 -8
- data/lib/girl_friday/persistence.rb +4 -6
- data/lib/girl_friday/server.rb +1 -1
- data/lib/girl_friday/version.rb +1 -1
- data/lib/girl_friday/work_queue.rb +25 -25
- data/test/helper.rb +9 -2
- data/test/test_batch.rb +32 -1
- data/test/test_girl_friday_queue.rb +16 -1
- metadata +31 -23
data/History.md
CHANGED
data/README.md
CHANGED
@@ -36,13 +36,13 @@ In your Rails app, create a `config/initializers/girl_friday.rb` which defines y
|
|
36
36
|
:size is the number of workers to spin up and defaults to 5. Keep in mind, ActiveRecord defaults to a connection pool size of 5 so if your workers are accessing the database you'll want to ensure that the connection pool is large enough by modifying `config/database.yml`.
|
37
37
|
|
38
38
|
In order to use the Redis backend, you must use a connection pool to share a set of Redis connections with
|
39
|
-
other threads and GirlFriday queues using the `
|
39
|
+
other threads and GirlFriday queues using the `connection_pool` gem:
|
40
40
|
|
41
41
|
require 'connection_pool'
|
42
42
|
|
43
43
|
redis_pool = ConnectionPool.new(:size => 5, :timeout => 5) { Redis.new }
|
44
44
|
|
45
|
-
CLEAN_FILTER_QUEUE = GirlFriday::WorkQueue.new(:clean_filter, :store => GirlFriday::Store::Redis, :store_config =>
|
45
|
+
CLEAN_FILTER_QUEUE = GirlFriday::WorkQueue.new(:clean_filter, :store => GirlFriday::Store::Redis, :store_config => { :pool => redis_pool }) do |msg|
|
46
46
|
Filter.clean(msg)
|
47
47
|
end
|
48
48
|
|
data/examples/batch.rb
CHANGED
@@ -7,11 +7,14 @@ class UrlProcessor
|
|
7
7
|
URLS = %w(http://www.bing.com http://www.google.com http://www.yahoo.com)
|
8
8
|
|
9
9
|
def parallel
|
10
|
-
batch = GirlFriday::Batch.new(
|
10
|
+
batch = GirlFriday::Batch.new(nil, :size => 3) do |url|
|
11
11
|
html = open(url)
|
12
12
|
doc = Nokogiri::HTML(html.read)
|
13
13
|
doc.css('span').count
|
14
14
|
end
|
15
|
+
URLS.each do |url|
|
16
|
+
batch << url
|
17
|
+
end
|
15
18
|
p URLS.zip(batch.results)
|
16
19
|
end
|
17
20
|
|
data/girl_friday.gemspec
CHANGED
@@ -17,6 +17,6 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
s.add_dependency 'connection_pool', '~> 0.1.0'
|
20
|
-
s.add_development_dependency 'sinatra', '~> 1.
|
20
|
+
s.add_development_dependency 'sinatra', '~> 1.3'
|
21
21
|
s.add_development_dependency 'rake'
|
22
22
|
end
|
data/lib/girl_friday/batch.rb
CHANGED
@@ -12,17 +12,23 @@ module GirlFriday
|
|
12
12
|
#
|
13
13
|
# TODO Errors are not handled well at all.
|
14
14
|
class Batch
|
15
|
-
def initialize(enumerable, options, &block)
|
15
|
+
def initialize(enumerable=nil, options={}, &block)
|
16
16
|
@queue = GirlFriday::Queue.new(:batch, options, &block)
|
17
17
|
@complete = 0
|
18
|
-
@size =
|
19
|
-
@results =
|
18
|
+
@size = 0
|
19
|
+
@results = []
|
20
|
+
if enumerable
|
21
|
+
@size = enumerable.count
|
22
|
+
@results = Array.new(@size)
|
23
|
+
end
|
20
24
|
@lock = Mutex.new
|
21
25
|
@condition = ConditionVariable.new
|
26
|
+
@frozen = false
|
22
27
|
start(enumerable)
|
23
28
|
end
|
24
29
|
|
25
30
|
def results(timeout=nil)
|
31
|
+
@frozen = true
|
26
32
|
@lock.synchronize do
|
27
33
|
@condition.wait(@lock, timeout) if @complete != @size
|
28
34
|
@queue.shutdown
|
@@ -30,17 +36,34 @@ module GirlFriday
|
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
39
|
+
def push(msg)
|
40
|
+
raise ArgumentError, "Batch is frozen, you cannot push more items into it" if @frozen
|
41
|
+
@lock.synchronize do
|
42
|
+
@results << nil
|
43
|
+
@size += 1
|
44
|
+
index = @results.size - 1
|
45
|
+
@queue.push(msg) do |result|
|
46
|
+
completion(result, index)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :<<, :push
|
51
|
+
|
33
52
|
private
|
34
53
|
|
35
54
|
def start(operations)
|
36
55
|
operations.each_with_index do |packet, index|
|
37
56
|
@queue.push(packet) do |result|
|
38
|
-
|
39
|
-
@complete += 1
|
40
|
-
@results[index] = result
|
41
|
-
@condition.signal if @complete == @size
|
42
|
-
end
|
57
|
+
completion(result, index)
|
43
58
|
end
|
59
|
+
end if operations
|
60
|
+
end
|
61
|
+
|
62
|
+
def completion(result, index)
|
63
|
+
@lock.synchronize do
|
64
|
+
@complete += 1
|
65
|
+
@results[index] = result
|
66
|
+
@condition.signal if @complete == @size
|
44
67
|
end
|
45
68
|
end
|
46
69
|
|
@@ -23,25 +23,23 @@ module GirlFriday
|
|
23
23
|
class Redis
|
24
24
|
def initialize(name, options)
|
25
25
|
@opts = options
|
26
|
-
unless @opts[:pool]
|
27
|
-
raise ArgumentError, "you must pass in a :pool"
|
28
|
-
end
|
26
|
+
raise ArgumentError, "you must pass in a :pool" unless @opts[:pool]
|
29
27
|
@key = "girl_friday-#{name}-#{environment}"
|
30
28
|
end
|
31
29
|
|
32
30
|
def push(work)
|
33
31
|
val = Marshal.dump(work)
|
34
|
-
redis{ |r| r.rpush(@key, val) }
|
32
|
+
redis { |r| r.rpush(@key, val) }
|
35
33
|
end
|
36
34
|
alias_method :<<, :push
|
37
35
|
|
38
36
|
def pop
|
39
|
-
val = redis{ |r| r.lpop(@key) }
|
37
|
+
val = redis { |r| r.lpop(@key) }
|
40
38
|
Marshal.load(val) if val
|
41
39
|
end
|
42
40
|
|
43
41
|
def size
|
44
|
-
redis.llen(@key)
|
42
|
+
redis { |r| r.llen(@key) }
|
45
43
|
end
|
46
44
|
|
47
45
|
private
|
data/lib/girl_friday/server.rb
CHANGED
data/lib/girl_friday/version.rb
CHANGED
@@ -11,7 +11,7 @@ module GirlFriday
|
|
11
11
|
@name = name.to_s
|
12
12
|
@size = options[:size] || 5
|
13
13
|
@processor = block
|
14
|
-
@error_handlers = (Array(options[:error_handler]
|
14
|
+
@error_handlers = (Array(options[:error_handler] || ErrorHandler.default)).map(&:new)
|
15
15
|
|
16
16
|
@shutdown = false
|
17
17
|
@busy_workers = []
|
@@ -70,17 +70,26 @@ module GirlFriday
|
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
-
def shutdown
|
73
|
+
def shutdown(&block)
|
74
74
|
# Runtime state should never be modified by caller thread,
|
75
75
|
# only the Supervisor thread.
|
76
|
-
@supervisor << Shutdown[
|
76
|
+
@supervisor << Shutdown[block]
|
77
77
|
end
|
78
78
|
|
79
79
|
private
|
80
80
|
|
81
|
+
def running?
|
82
|
+
!@shutdown
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_error(ex)
|
86
|
+
# Redis network error? Log and ignore.
|
87
|
+
@error_handlers.each { |handler| handler.handle(ex) }
|
88
|
+
end
|
89
|
+
|
81
90
|
def on_ready(who)
|
82
91
|
@total_processed += 1
|
83
|
-
if
|
92
|
+
if running? && work = @persister.pop
|
84
93
|
who.this << work
|
85
94
|
drain
|
86
95
|
else
|
@@ -88,22 +97,21 @@ module GirlFriday
|
|
88
97
|
ready_workers << who.this
|
89
98
|
end
|
90
99
|
rescue => ex
|
91
|
-
|
92
|
-
@error_handlers.each { |handler| handler.handle(ex) }
|
100
|
+
handle_error(ex)
|
93
101
|
end
|
94
102
|
|
95
103
|
def shutdown_complete
|
96
104
|
begin
|
97
105
|
@when_shutdown.call(self) if @when_shutdown
|
98
106
|
rescue Exception => ex
|
99
|
-
|
107
|
+
handle_error(ex)
|
100
108
|
end
|
101
109
|
end
|
102
110
|
|
103
111
|
def on_work(work)
|
104
112
|
@total_queued += 1
|
105
113
|
|
106
|
-
if
|
114
|
+
if running? && worker = ready_workers.pop
|
107
115
|
@busy_workers << worker
|
108
116
|
worker << work
|
109
117
|
drain
|
@@ -111,19 +119,12 @@ module GirlFriday
|
|
111
119
|
@persister << work
|
112
120
|
end
|
113
121
|
rescue => ex
|
114
|
-
|
115
|
-
@error_handlers.each { |handler| handler.handle(ex) }
|
122
|
+
handle_error(ex)
|
116
123
|
end
|
117
124
|
|
118
125
|
def ready_workers
|
119
|
-
|
120
|
-
|
121
|
-
@size.times do
|
122
|
-
# start N workers
|
123
|
-
workers << Actor.spawn_link(&@work_loop)
|
124
|
-
end
|
125
|
-
workers
|
126
|
-
end
|
126
|
+
# start N workers
|
127
|
+
@ready_workers ||= Array.new(@size) { Actor.spawn_link(&@work_loop) }
|
127
128
|
end
|
128
129
|
|
129
130
|
def start
|
@@ -132,9 +133,9 @@ module GirlFriday
|
|
132
133
|
supervisor = Actor.current
|
133
134
|
@work_loop = Proc.new do
|
134
135
|
Thread.current[:label] = "#{name}-worker"
|
135
|
-
while
|
136
|
+
while running? do
|
136
137
|
work = Actor.receive
|
137
|
-
if
|
138
|
+
if running?
|
138
139
|
result = @processor.call(work.msg)
|
139
140
|
work.callback.call(result) if work.callback
|
140
141
|
supervisor << Ready[Actor.current]
|
@@ -155,8 +156,7 @@ module GirlFriday
|
|
155
156
|
|
156
157
|
def drain
|
157
158
|
# give as much work to as many ready workers as possible
|
158
|
-
|
159
|
-
todo = ready_workers.size < ps ? ready_workers.size : ps
|
159
|
+
todo = [@persister.size, ready_workers.size].min
|
160
160
|
todo.times do
|
161
161
|
worker = ready_workers.pop
|
162
162
|
@busy_workers << worker
|
@@ -183,18 +183,18 @@ module GirlFriday
|
|
183
183
|
return
|
184
184
|
end
|
185
185
|
f.when(Actor::DeadActorError) do |ex|
|
186
|
-
if
|
186
|
+
if running?
|
187
187
|
# TODO Provide current message contents as error context
|
188
188
|
@total_errors += 1
|
189
189
|
@busy_workers.delete(ex.actor)
|
190
190
|
ready_workers << Actor.spawn_link(&@work_loop)
|
191
|
-
|
191
|
+
handle_error(ex.reason)
|
192
192
|
end
|
193
193
|
end
|
194
194
|
end
|
195
195
|
end
|
196
196
|
end
|
197
|
-
|
198
197
|
end
|
198
|
+
|
199
199
|
Queue = WorkQueue
|
200
200
|
end
|
data/test/helper.rb
CHANGED
@@ -2,12 +2,16 @@ $testing = true
|
|
2
2
|
puts RUBY_DESCRIPTION
|
3
3
|
|
4
4
|
at_exit do
|
5
|
+
# queue.shutdown is async so sleep a little to minimize
|
6
|
+
# race conditions between us and other threads
|
7
|
+
# that haven't yet been GC'd.
|
8
|
+
sleep 0.1
|
5
9
|
if Thread.list.size > 1
|
6
10
|
Thread.list.each do |thread|
|
7
11
|
next if thread.status == 'run'
|
8
12
|
puts "WARNING: lingering threads found. All threads should be shutdown and garbage collected."
|
9
|
-
|
10
|
-
|
13
|
+
puts "This is normal if a test failed so a queue did not get shutdown."
|
14
|
+
p [thread, thread[:label]]
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
@@ -30,6 +34,9 @@ class MiniTest::Unit::TestCase
|
|
30
34
|
q = TimedQueue.new
|
31
35
|
yield Proc.new { q << nil }
|
32
36
|
q.timed_pop(time)
|
37
|
+
ensure
|
38
|
+
count = GirlFriday.shutdown!(1)
|
39
|
+
puts "Unable to shutdown queue (#{count})" if count != 0
|
33
40
|
end
|
34
41
|
|
35
42
|
end
|
data/test/test_batch.rb
CHANGED
@@ -16,7 +16,7 @@ class TestBatch < MiniTest::Unit::TestCase
|
|
16
16
|
# asking for the results should block
|
17
17
|
results = batch.results(1.0)
|
18
18
|
c = Time.now
|
19
|
-
assert_in_delta(0.5, (c - b), 0.
|
19
|
+
assert_in_delta(0.5, (c - b), 0.3)
|
20
20
|
|
21
21
|
assert_equal 10, results.size
|
22
22
|
assert_kind_of Time, results[0]
|
@@ -39,4 +39,35 @@ class TestBatch < MiniTest::Unit::TestCase
|
|
39
39
|
# http://redmine.ruby-lang.org/issues/5342
|
40
40
|
sleep 0.1
|
41
41
|
end
|
42
|
+
|
43
|
+
def test_empty_batch
|
44
|
+
batch = GirlFriday::Batch.new(nil, :size => 4) do |msg|
|
45
|
+
sleep msg
|
46
|
+
'x'
|
47
|
+
end
|
48
|
+
values = batch.results
|
49
|
+
values.must_be_kind_of Array
|
50
|
+
values.must_equal []
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_streaming_batch_api
|
54
|
+
batch = GirlFriday::Batch.new(nil, :size => 4) do |msg|
|
55
|
+
sleep msg
|
56
|
+
'x'
|
57
|
+
end
|
58
|
+
a = Time.now
|
59
|
+
batch << 0.1
|
60
|
+
batch << 0.1
|
61
|
+
batch << 0.1
|
62
|
+
batch << 0.1
|
63
|
+
values = batch.results
|
64
|
+
b = Time.now
|
65
|
+
values.must_be_kind_of Array
|
66
|
+
values.must_equal %w(x x x x)
|
67
|
+
assert_in_delta 0.2, (b - a), 0.1
|
68
|
+
|
69
|
+
assert_raises ArgumentError do
|
70
|
+
batch << 0.1
|
71
|
+
end
|
72
|
+
end
|
42
73
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
class
|
3
|
+
class TestGirlFridayQueue < MiniTest::Unit::TestCase
|
4
4
|
|
5
5
|
class TestErrorHandler
|
6
6
|
include MiniTest::Assertions
|
@@ -36,6 +36,20 @@ class TestGirlFriday < MiniTest::Unit::TestCase
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
def test_should_use_a_default_error_handler_when_none_specified
|
40
|
+
async_test do |cb|
|
41
|
+
queue = GirlFriday::WorkQueue.new('error') do |msg|
|
42
|
+
end
|
43
|
+
queue.shutdown do
|
44
|
+
cb.call
|
45
|
+
end
|
46
|
+
queue.push(:text => 'foo') # Redundant
|
47
|
+
|
48
|
+
# Not an ideal method, but I can't see a better way without complex stubbing.
|
49
|
+
assert queue.instance_eval { @error_handlers }.length > 0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
39
53
|
def test_should_call_callback_when_complete
|
40
54
|
async_test do |cb|
|
41
55
|
queue = GirlFriday::WorkQueue.new('callback', :size => 1) do |msg|
|
@@ -118,6 +132,7 @@ class TestGirlFriday < MiniTest::Unit::TestCase
|
|
118
132
|
total.times do
|
119
133
|
queue.push(:text => 'foo')
|
120
134
|
end
|
135
|
+
refute_nil queue.status['redis-pool'][:backlog]
|
121
136
|
end
|
122
137
|
end
|
123
138
|
|
metadata
CHANGED
@@ -1,49 +1,50 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: girl_friday
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
4
|
+
prerelease:
|
5
|
+
version: 0.9.7
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Mike Perham
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-
|
12
|
+
date: 2011-11-28 00:00:00.000000000 -08:00
|
13
|
+
default_executable:
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
16
|
name: connection_pool
|
16
|
-
|
17
|
-
none: false
|
17
|
+
version_requirements: &2078 !ruby/object:Gem::Requirement
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
21
|
version: 0.1.0
|
22
|
-
|
22
|
+
none: false
|
23
|
+
requirement: *2078
|
23
24
|
prerelease: false
|
24
|
-
|
25
|
+
type: :runtime
|
25
26
|
- !ruby/object:Gem::Dependency
|
26
27
|
name: sinatra
|
27
|
-
|
28
|
-
none: false
|
28
|
+
version_requirements: &2096 !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '1.
|
33
|
-
|
32
|
+
version: '1.3'
|
33
|
+
none: false
|
34
|
+
requirement: *2096
|
34
35
|
prerelease: false
|
35
|
-
|
36
|
+
type: :development
|
36
37
|
- !ruby/object:Gem::Dependency
|
37
38
|
name: rake
|
38
|
-
|
39
|
-
none: false
|
39
|
+
version_requirements: &2114 !ruby/object:Gem::Requirement
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: '0'
|
44
|
-
|
44
|
+
none: false
|
45
|
+
requirement: *2114
|
45
46
|
prerelease: false
|
46
|
-
|
47
|
+
type: :development
|
47
48
|
description: Background processing, simplified
|
48
49
|
email:
|
49
50
|
- mperham@gmail.com
|
@@ -83,28 +84,35 @@ files:
|
|
83
84
|
- test/test_girl_friday.rb
|
84
85
|
- test/test_girl_friday_immediately.rb
|
85
86
|
- test/test_girl_friday_queue.rb
|
87
|
+
has_rdoc: true
|
86
88
|
homepage: http://github.com/mperham/girl_friday
|
87
89
|
licenses: []
|
88
|
-
post_install_message:
|
90
|
+
post_install_message:
|
89
91
|
rdoc_options: []
|
90
92
|
require_paths:
|
91
93
|
- lib
|
92
94
|
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
-
none: false
|
94
95
|
requirements:
|
95
96
|
- - ! '>='
|
96
97
|
- !ruby/object:Gem::Version
|
97
98
|
version: '0'
|
98
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
99
|
none: false
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
101
|
requirements:
|
101
102
|
- - ! '>='
|
102
103
|
- !ruby/object:Gem::Version
|
103
104
|
version: '0'
|
105
|
+
none: false
|
104
106
|
requirements: []
|
105
107
|
rubyforge_project: girl_friday
|
106
|
-
rubygems_version: 1.
|
107
|
-
signing_key:
|
108
|
+
rubygems_version: 1.6.2
|
109
|
+
signing_key:
|
108
110
|
specification_version: 3
|
109
111
|
summary: Background processing, simplified
|
110
|
-
test_files:
|
112
|
+
test_files:
|
113
|
+
- test/helper.rb
|
114
|
+
- test/test_batch.rb
|
115
|
+
- test/test_girl_friday.rb
|
116
|
+
- test/test_girl_friday_immediately.rb
|
117
|
+
- test/test_girl_friday_queue.rb
|
118
|
+
...
|