queue_map 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+