girl_friday 0.9.6 → 0.9.7

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