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 CHANGED
@@ -4,3 +4,5 @@ Gemfile.lock
4
4
  pkg/*
5
5
  rbxdb/
6
6
  *.rdb
7
+ .idea/*
8
+
data/History.md CHANGED
@@ -1,6 +1,13 @@
1
1
  Changes
2
2
  ================
3
3
 
4
+ 0.9.7
5
+ ---------
6
+
7
+ * Fix error handling (xshay)
8
+ * Add streaming batch support for adding elements to a batch one at a
9
+ time rather than all at once.
10
+
4
11
  0.9.6
5
12
  ---------
6
13
 
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 `connection\_pool` gem:
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 => [{ :pool => redis_pool}]) do |msg|
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
 
@@ -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(URLS, :size => 3) do |url|
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
 
@@ -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.0'
20
+ s.add_development_dependency 'sinatra', '~> 1.3'
21
21
  s.add_development_dependency 'rake'
22
22
  end
@@ -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 = enumerable.count
19
- @results = Array.new(@size)
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
- @lock.synchronize do
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
@@ -14,7 +14,7 @@ module GirlFriday
14
14
  basedir = File.expand_path(File.dirname(__FILE__) + '/../../server')
15
15
 
16
16
  set :views, "#{basedir}/views"
17
- set :public, "#{basedir}/public"
17
+ set :public_folder, "#{basedir}/public"
18
18
  set :static, true
19
19
 
20
20
  helpers do
@@ -1,3 +1,3 @@
1
1
  module GirlFriday
2
- VERSION = "0.9.6"
2
+ VERSION = "0.9.7"
3
3
  end
@@ -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]) || ErrorHandler.default).map(&:new)
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[block_given? ? Proc.new : nil]
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 !@shutdown && work = @persister.pop
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
- # Redis network error? Log and ignore.
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
- @error_handlers.each { |handler| handler.handle(ex) }
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 !@shutdown && worker = ready_workers.pop
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
- # Redis network error? Log and ignore.
115
- @error_handlers.each { |handler| handler.handle(ex) }
122
+ handle_error(ex)
116
123
  end
117
124
 
118
125
  def ready_workers
119
- @ready_workers ||= begin
120
- workers = []
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 !@shutdown do
136
+ while running? do
136
137
  work = Actor.receive
137
- if !@shutdown
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
- ps = @persister.size
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 !@shutdown
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
- @error_handlers.each { |handler| handler.handle(ex.reason) }
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
@@ -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
- p [thread, thread[:label], thread.object_id]
10
- # puts thread.backtrace.join("\n")
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
@@ -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.1)
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 TestGirlFriday < MiniTest::Unit::TestCase
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
- version: 0.9.6
5
- prerelease:
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-09-26 00:00:00.000000000Z
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
- requirement: &22271180 !ruby/object:Gem::Requirement
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
- type: :runtime
22
+ none: false
23
+ requirement: *2078
23
24
  prerelease: false
24
- version_requirements: *22271180
25
+ type: :runtime
25
26
  - !ruby/object:Gem::Dependency
26
27
  name: sinatra
27
- requirement: &22270680 !ruby/object:Gem::Requirement
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.0'
33
- type: :development
32
+ version: '1.3'
33
+ none: false
34
+ requirement: *2096
34
35
  prerelease: false
35
- version_requirements: *22270680
36
+ type: :development
36
37
  - !ruby/object:Gem::Dependency
37
38
  name: rake
38
- requirement: &22270300 !ruby/object:Gem::Requirement
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
- type: :development
44
+ none: false
45
+ requirement: *2114
45
46
  prerelease: false
46
- version_requirements: *22270300
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.8.6
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
+ ...