queue_map 0.1

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/MIT_LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 LeadTune
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ options = {}
5
+ opt_p = OptionParser.new do |opts|
6
+ opts.banner = "Usage: script/export_sales.rb [options] path/to/consumer.rb"
7
+ opts.separator "Options:"
8
+ opts.on('-f', '--foreground', "Run appraiser in the foreground") do
9
+ options[:foreground] = true
10
+ end
11
+ opts.on_tail("-h", "--help", "Show this message")
12
+ end
13
+ opt_p.parse!
14
+ consumer_path = ARGV[0]
15
+ if consumer_path.nil?
16
+ puts opt_p
17
+ exit 1
18
+ end
19
+
20
+ require "rubygems"
21
+ require File.dirname(__FILE__) + "/../lib/queue_map"
22
+ require File.dirname(__FILE__) + "/../lib/queue_map/consumer"
23
+
24
+ consumer = QueueMap::Consumer.from_file(consumer_path, :strategy => :fork)
25
+
26
+ unless options[:foreground]
27
+ # daemonize
28
+ puts "Daemonizing"
29
+ pid = Kernel.fork
30
+ if pid
31
+ Process.detach(pid)
32
+ puts "Daemonized as #{pid}"
33
+ exit 0
34
+ else
35
+ Signal.trap('HUP', 'IGNORE') # Don't die upon logout
36
+ end
37
+ end
38
+ puts "Starting consumers..."
39
+ consumer.start
@@ -0,0 +1,5 @@
1
+ module Enumerable
2
+ def queue_map(*args)
3
+ QueueMap.map(self, *args)
4
+ end
5
+ end
@@ -0,0 +1,153 @@
1
+ class QueueMap::Consumer
2
+ attr_accessor :count_workers, :worker_proc, :on_exception_proc
3
+ attr_reader :name
4
+ class Configurator
5
+ def initialize(base)
6
+ @base = base
7
+ end
8
+
9
+ def before_fork(&block); @base.before_fork_procs << block; end
10
+ def after_fork(&block); @base.after_fork_procs << block; end
11
+ def between_responses(&block); @base.between_responses_procs << block; end
12
+ def worker(&block); @base.worker_proc = block; end
13
+ def on_exception(&block); @base.on_exception_proc = block; end
14
+ def count_workers(value); @base.count_workers = value; end
15
+ end
16
+
17
+
18
+ def self.new_from_block(name, options = { }, &block)
19
+ consumer = new(name, options)
20
+ Configurator.new(consumer).instance_eval(&block)
21
+ consumer
22
+ end
23
+
24
+ def self.from_file(consumer_path, options = { })
25
+ name = File.basename(consumer_path).gsub(/_consumer\.rb$/, '').to_sym
26
+ consumer = new(name, options)
27
+ Configurator.new(consumer).instance_eval(File.read(consumer_path), consumer_path, 1)
28
+ consumer
29
+ end
30
+
31
+ def initialize(name, options = { })
32
+ @name = name
33
+ case options[:strategy]
34
+ when :fork
35
+ extend(ForkStrategy)
36
+ when :thread
37
+ extend(ThreadStrategy)
38
+ when :test
39
+ nil
40
+ else
41
+ raise "Invalid strategy: #{options[:strategy]}"
42
+ end
43
+ end
44
+
45
+ def after_fork_procs
46
+ @after_fork_procs ||= []
47
+ end
48
+
49
+ def before_fork_procs
50
+ @before_fork_procs ||= []
51
+ end
52
+
53
+ def between_responses_procs
54
+ @between_responses_procs ||= []
55
+ end
56
+
57
+ def start
58
+ raise RuntimeError, "Called start on Consumer without strategy"
59
+ end
60
+
61
+ def stop
62
+ raise RuntimeError, "Called stop on Consumer without strategy"
63
+ end
64
+
65
+ def run_consumer
66
+ QueueMap.with_bunny do |bunny|
67
+ q = bunny.queue(name.to_s, :durable => false, :auto_delete => true)
68
+ while msg = q.pop
69
+ # STDOUT << "[#{Thread.current[:id]}]"
70
+ return if @shutting_down
71
+ begin
72
+ (sleep 0.05; next) if msg == :queue_empty
73
+ msg = Marshal.load(msg)
74
+ result = worker_proc.call(msg[:input])
75
+ bunny.queue(msg[:response_queue]).publish(Marshal.dump(:result => result, :index => msg[:index]))
76
+ between_responses_procs.each { |p| p.call }
77
+ rescue Exception => e
78
+ if on_exception_proc
79
+ on_exception_proc.call(e)
80
+ else
81
+ puts e
82
+ puts e.backtrace
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ module ThreadStrategy
90
+ def start
91
+ @threads = []
92
+ count_workers.times do |c|
93
+ @threads << (Thread.new do
94
+ begin
95
+ run_consumer
96
+ rescue Exception => e
97
+ puts %(#{e}\n#{e.backtrace.join("\n")})
98
+ end
99
+ end)
100
+ end
101
+ end
102
+
103
+ def stop(graceful = true)
104
+ if graceful
105
+ @shutting_down = true
106
+ else
107
+ @threads.each { |t| t.kill }
108
+ end
109
+ end
110
+ end
111
+
112
+ module ForkStrategy
113
+ class ImaChild < Exception; end
114
+
115
+ def start
116
+ @child_pids = []
117
+ @master_pid = Process.pid
118
+ before_fork_procs.each { |p| p.call }
119
+ begin
120
+ (count_workers - 1).times do |c|
121
+ pid = Kernel.fork
122
+ raise ImaChild if pid.nil?
123
+ @child_pids << pid
124
+ end
125
+ $0 = "#{name} queue_map consumer: master"
126
+ rescue ImaChild
127
+ $0 = "#{name} queue_map consumer: child"
128
+ # Parent Child Testing Product was a success! http://is.gd/fXmmZ
129
+ end
130
+ Signal.trap("TERM") { stop(false) }
131
+ Signal.trap("INT") { stop }
132
+ after_fork_procs.each { |p| p.call }
133
+ run_consumer
134
+ end
135
+
136
+ def stop(graceful = true)
137
+ @child_pids.each do |pid|
138
+ begin
139
+ Process.kill(graceful ? "INT" : "TERM", pid)
140
+ rescue Errno::ESRCH => e
141
+ puts "Unable to signal process #{pid}. Does the process not exist?"
142
+ end
143
+ end
144
+
145
+ puts "#{Process.pid}: stopping (graceful: #{graceful})"
146
+ if graceful
147
+ @shutting_down = true
148
+ else
149
+ exit 0
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,3 @@
1
+ module QueueMap
2
+ VERSION=0.1
3
+ end
data/lib/queue_map.rb ADDED
@@ -0,0 +1,98 @@
1
+ require File.dirname(__FILE__) + "/ext/enumerable.rb"
2
+ require "bunny"
3
+ require 'timeout'
4
+
5
+ module QueueMap
6
+ autoload :Consumer, File.dirname(__FILE__) + "/queue_map/consumer"
7
+ BUNNY_MUTEX = Mutex.new
8
+ extend self
9
+ attr_accessor :mode
10
+ attr_writer :consumer_base_path
11
+ attr_accessor :consumer_path
12
+
13
+ DEFAULT_ON_TIMEOUT = lambda { |r| nil }
14
+
15
+ def unique_name
16
+ @unique_name ||= "#{`hostname`.chomp}-#{Process.pid}-#{Time.now.usec}"
17
+ end
18
+
19
+ def consumer_base_path
20
+ @consumer_base_path ||= "lib/consumers"
21
+ end
22
+
23
+ def consumer_path
24
+ @consumer_path ||= Hash.new do |hash, key|
25
+ File.join(consumer_base_path, "#{key}_consumer.rb")
26
+ end
27
+ end
28
+
29
+ def response_queue_name(name)
30
+ @inc ||= 0
31
+ "#{name}_response_#{unique_name}_#{@inc += 1}"
32
+ end
33
+
34
+ def map(collection, name, options = {})
35
+ return queue_map_internal(collection, name) if mode == :test
36
+
37
+ with_bunny do |bunny|
38
+ q = bunny.queue(name.to_s)
39
+ response_queue_name = response_queue_name(name)
40
+ response_queue = bunny.queue(response_queue_name, :durable => false, :exclusive => true, :auto_delete => true)
41
+
42
+ (0..(collection.length - 1)).each do |i|
43
+ q.publish(Marshal.dump(:input => collection[i], :index => i, :response_queue => response_queue_name))
44
+ end
45
+
46
+ results = {}
47
+ begin
48
+ Timeout.timeout(options[:timeout] || 5) do
49
+ collection.length.times do
50
+ sleep 0.05 while (next_response = response_queue.pop) == :queue_empty
51
+ response = Marshal.load(next_response)
52
+ results[response[:index]] = response[:result]
53
+ end
54
+ end
55
+ rescue Timeout::Error => e
56
+ end
57
+
58
+ (0..(collection.length - 1)).map do |i|
59
+ results[i] || (options[:on_timeout] || DEFAULT_ON_TIMEOUT).call(collection[i])
60
+ end
61
+ end
62
+ end
63
+
64
+ def queue_map_internal(collection, name, *args)
65
+ collection.map(&consumer(name).worker_proc)
66
+ end
67
+
68
+ def consumers
69
+ @consumers ||= { }
70
+ end
71
+
72
+ def consumer(name)
73
+ consumers[name] ||= QueueMap::Consumer.from_file(consumer_path[name], :strategy => mode || :thread)
74
+ end
75
+
76
+ def with_bunny(&block)
77
+ bunny = nil
78
+ BUNNY_MUTEX.synchronize do
79
+ bunny = Bunny.new((@connection_info || { }).merge(:spec => '08'))
80
+ bunny.start
81
+ end
82
+ begin
83
+ yield bunny
84
+ ensure
85
+ bunny.close_connection unless bunny.status == :not_connected
86
+ end
87
+ end
88
+
89
+ def connection_info=(connection_info)
90
+ @bunny = nil
91
+ @connection_info = connection_info
92
+ end
93
+
94
+ def mode=(mode)
95
+ @mode = mode
96
+ consumers.clear
97
+ end
98
+ end
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: queue_map
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - Tim Harper
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-11 00:00:00 -06:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bunny
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - "="
27
+ - !ruby/object:Gem::Version
28
+ hash: 13
29
+ segments:
30
+ - 0
31
+ - 5
32
+ - 3
33
+ version: 0.5.3
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - "="
43
+ - !ruby/object:Gem::Version
44
+ hash: 7712058
45
+ segments:
46
+ - 2
47
+ - 0
48
+ - 0
49
+ - rc
50
+ version: 2.0.0.rc
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: background_process
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ type: :development
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ name: ruby-debug
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ type: :development
80
+ version_requirements: *id004
81
+ description: Queue Map
82
+ email:
83
+ - tim@leadtune.com
84
+ executables:
85
+ - queue_map_consumer
86
+ extensions: []
87
+
88
+ extra_rdoc_files: []
89
+
90
+ files:
91
+ - bin/queue_map_consumer
92
+ - lib/ext/enumerable.rb
93
+ - lib/queue_map/consumer.rb
94
+ - lib/queue_map/version.rb
95
+ - lib/queue_map.rb
96
+ - MIT_LICENSE
97
+ has_rdoc: true
98
+ homepage: http://github.com/leadtune/queue_map
99
+ licenses: []
100
+
101
+ post_install_message:
102
+ rdoc_options: []
103
+
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ hash: 3
112
+ segments:
113
+ - 0
114
+ version: "0"
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ hash: 23
121
+ segments:
122
+ - 1
123
+ - 3
124
+ - 6
125
+ version: 1.3.6
126
+ requirements: []
127
+
128
+ rubyforge_project:
129
+ rubygems_version: 1.3.7
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: Map a task across a pool of consumers, and recieve the result when the consumers are done
133
+ test_files: []
134
+