collective 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +13 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Guardfile +6 -0
- data/README +7 -0
- data/Rakefile +11 -0
- data/bin/collective +37 -0
- data/collective.gemspec +23 -0
- data/demo/demo +36 -0
- data/demo/demo.rb +30 -0
- data/demo/demo3 +36 -0
- data/demo/job1.rb +31 -0
- data/demo/job2.rb +42 -0
- data/demo/job3.rb +44 -0
- data/demo/populate.rb +22 -0
- data/lib/collective.rb +52 -0
- data/lib/collective/checker.rb +51 -0
- data/lib/collective/configuration.rb +219 -0
- data/lib/collective/idler.rb +81 -0
- data/lib/collective/key.rb +48 -0
- data/lib/collective/lifecycle_observer.rb +25 -0
- data/lib/collective/log.rb +29 -0
- data/lib/collective/messager.rb +218 -0
- data/lib/collective/mocks/storage.rb +108 -0
- data/lib/collective/monitor.rb +58 -0
- data/lib/collective/policy.rb +60 -0
- data/lib/collective/pool.rb +180 -0
- data/lib/collective/redis/storage.rb +142 -0
- data/lib/collective/registry.rb +123 -0
- data/lib/collective/squiggly.rb +20 -0
- data/lib/collective/utilities/airbrake_observer.rb +26 -0
- data/lib/collective/utilities/hoptoad_observer.rb +26 -0
- data/lib/collective/utilities/log_observer.rb +40 -0
- data/lib/collective/utilities/observeable.rb +18 -0
- data/lib/collective/utilities/observer_base.rb +59 -0
- data/lib/collective/utilities/process.rb +82 -0
- data/lib/collective/utilities/signal_hook.rb +47 -0
- data/lib/collective/utilities/storage_base.rb +41 -0
- data/lib/collective/version.rb +3 -0
- data/lib/collective/worker.rb +161 -0
- data/spec/checker_spec.rb +20 -0
- data/spec/configuration_spec.rb +24 -0
- data/spec/helper.rb +33 -0
- data/spec/idler_spec.rb +58 -0
- data/spec/key_spec.rb +41 -0
- data/spec/messager_spec.rb +131 -0
- data/spec/mocks/storage_spec.rb +108 -0
- data/spec/monitor_spec.rb +15 -0
- data/spec/policy_spec.rb +43 -0
- data/spec/pool_spec.rb +119 -0
- data/spec/redis/storage_spec.rb +133 -0
- data/spec/registry_spec.rb +52 -0
- data/spec/support/jobs.rb +58 -0
- data/spec/support/redis.rb +22 -0
- data/spec/support/timing.rb +32 -0
- data/spec/utilities/observer_base_spec.rb +50 -0
- data/spec/utilities/process_spec.rb +17 -0
- data/spec/worker_spec.rb +121 -0
- data/unused/times.rb +45 -0
- metadata +148 -0
@@ -0,0 +1,219 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "ruby-debug"
|
5
|
+
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Evaluate a ruby configuration file in the context of a Collective Configuration instance.
|
9
|
+
Offers a DSL to build the jobs as well as setting before/after-fork hooks.
|
10
|
+
|
11
|
+
Collective configuration:
|
12
|
+
|
13
|
+
env()
|
14
|
+
set_env(ENV)
|
15
|
+
--env=ENV
|
16
|
+
Sets the environment.
|
17
|
+
Used in pid file and log file naming.
|
18
|
+
Defaults to RAILS_ENV || RACK_ENV || "test".
|
19
|
+
|
20
|
+
chdir(DIR)
|
21
|
+
--chdir=DIR
|
22
|
+
Changes the working directory. Creates it if necessary.
|
23
|
+
Takes effect immediately.
|
24
|
+
Can only be set once. Has no effect if specified more than once.
|
25
|
+
Defaults to /tmp/$NAME
|
26
|
+
|
27
|
+
name()
|
28
|
+
name=(NAME)
|
29
|
+
--name=NAME
|
30
|
+
Sets the name of the process.
|
31
|
+
Defaults to the base name of the configuration file.
|
32
|
+
Used in pid file and log file naming.
|
33
|
+
|
34
|
+
--path=PATH
|
35
|
+
add_path(PATH)
|
36
|
+
Adds a path to the Ruby load path.
|
37
|
+
Can be used multiple times.
|
38
|
+
|
39
|
+
--require=LIB
|
40
|
+
Requires a library or Ruby gem.
|
41
|
+
Can be used multiple times.
|
42
|
+
|
43
|
+
=end
|
44
|
+
|
45
|
+
class Collective::Configuration
|
46
|
+
|
47
|
+
def self.parse( argv = ARGV )
|
48
|
+
us = new
|
49
|
+
|
50
|
+
optparse = OptionParser.new do |opts|
|
51
|
+
opts.banner = "Usage: #{__FILE__} [options]* configuration_file_rb"
|
52
|
+
opts.on( "-c", "--chdir DIR", "Change working directory." ) { |d| us.chdir(d) }
|
53
|
+
opts.on( "-e", "--env ENV", "Set environment (env).") { |e| us.set_env(e) }
|
54
|
+
opts.on( "-h", "--help", "Display this usage summary." ) { puts opts; exit }
|
55
|
+
opts.on( "-n", "--name NAME", "Set daemon's name.") { |n| us.set_name(n) }
|
56
|
+
opts.on( "-p", "--path PATH", "Add to load path.") { |d| us.add_path(d) }
|
57
|
+
opts.on( "-r", "--require LIB", "Require a library.") { |l| us.require_lib(l) }
|
58
|
+
opts.on( "-s", "--script DSL", "Include DSL script.") { |s| us.load_script(s) }
|
59
|
+
opts.on( "-v", "--verbose", "Print stuff out.") { |s| us.verbose += 1 }
|
60
|
+
opts.on( "--dry-run", "Don't launch the daemon.") { us.dry_run = true }
|
61
|
+
end.parse!(argv)
|
62
|
+
|
63
|
+
while argv.any? && File.exists?(argv.first) do
|
64
|
+
us.load_file( argv.shift )
|
65
|
+
end
|
66
|
+
|
67
|
+
us.args = argv
|
68
|
+
us.finalize
|
69
|
+
end
|
70
|
+
|
71
|
+
include Collective::Log
|
72
|
+
|
73
|
+
attr :env
|
74
|
+
attr :root
|
75
|
+
attr :name, true
|
76
|
+
attr :verbose, true
|
77
|
+
attr :dry_run, true
|
78
|
+
attr :args, true
|
79
|
+
attr :after_forks
|
80
|
+
|
81
|
+
attr :defaults
|
82
|
+
attr :jobs
|
83
|
+
|
84
|
+
def initialize( filename = nil )
|
85
|
+
@verbose = 0
|
86
|
+
@dry_run = false
|
87
|
+
@defaults = {}
|
88
|
+
@jobs = {}
|
89
|
+
load_file(filename) if filename
|
90
|
+
end
|
91
|
+
|
92
|
+
def load_script(string)
|
93
|
+
log "Loading #{string}" if verbose >= 2
|
94
|
+
instance_eval(string)
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_file(filename)
|
98
|
+
log "Loading #{filename}" if verbose >= 1
|
99
|
+
instance_eval(File.read(filename),filename)
|
100
|
+
if ! name then
|
101
|
+
n = File.basename(filename).sub(/\.[^.]*$/,'')
|
102
|
+
@name = n if n.size > 0
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def finalize()
|
107
|
+
if ! env then
|
108
|
+
@env = ( ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test" )
|
109
|
+
log "Defaulting env to #{env}" if verbose >= 1
|
110
|
+
end
|
111
|
+
if ! name then
|
112
|
+
@name = "collective"
|
113
|
+
log "Defaulting name to #{name}" if verbose >= 1
|
114
|
+
end
|
115
|
+
if ! @root then
|
116
|
+
chdir(default_root)
|
117
|
+
end
|
118
|
+
log inspect if verbose >= 2
|
119
|
+
freeze
|
120
|
+
self
|
121
|
+
end
|
122
|
+
|
123
|
+
def options_for_daemon_spawn
|
124
|
+
mkdirp root, "#{root}/log", "#{root}/tmp", "#{root}/tmp/pids" if ! dry_run
|
125
|
+
return {
|
126
|
+
working_dir: root,
|
127
|
+
log_file: "#{root}/log/#{name}_#{env}.log",
|
128
|
+
pid_file: "#{root}/tmp/pids/#{name}_#{env}.pid",
|
129
|
+
sync_log: local?
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
def args_for_daemon_spawn
|
134
|
+
args + [self]
|
135
|
+
end
|
136
|
+
|
137
|
+
# ----------------------------------------------------------------------------
|
138
|
+
# DSL
|
139
|
+
# ----------------------------------------------------------------------------
|
140
|
+
|
141
|
+
def set_env(env)
|
142
|
+
@env = env
|
143
|
+
end
|
144
|
+
|
145
|
+
def set_name(name)
|
146
|
+
@name = name
|
147
|
+
end
|
148
|
+
|
149
|
+
# takes effect immediately
|
150
|
+
def chdir(path)
|
151
|
+
if ! @root then
|
152
|
+
p = File.expand_path(path)
|
153
|
+
mkdirp(p) if ! dry_run
|
154
|
+
Dir.chdir(p)
|
155
|
+
log "Changed working directory (root) to #{p}" if verbose >= 1
|
156
|
+
@root = p
|
157
|
+
else
|
158
|
+
log "Warning: working directory already set to #{root}; not changing to #{path}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# takes effect immediately
|
163
|
+
def add_path(path)
|
164
|
+
p = File.expand_path(path)
|
165
|
+
log "Added #{p} to load path" if verbose >= 2
|
166
|
+
$:.push(p) unless $:.member?(p)
|
167
|
+
end
|
168
|
+
|
169
|
+
# convenience for -r on the command line
|
170
|
+
def require_lib(r)
|
171
|
+
require(r)
|
172
|
+
log "Required #{r}" if verbose >= 2
|
173
|
+
end
|
174
|
+
|
175
|
+
def set_default(key,value)
|
176
|
+
# values which are arrays get merged, but nil will overwrite
|
177
|
+
case value
|
178
|
+
when Array
|
179
|
+
@defaults[key] = (@defaults[key] || []) + value
|
180
|
+
else
|
181
|
+
@defaults[key] = value
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def set_defaults(options)
|
186
|
+
options.each { |k,v| set_default(k,v) }
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_pool(name,options)
|
190
|
+
options = defaults.merge(options)
|
191
|
+
jobs[name] = options
|
192
|
+
log "Added pool for #{name}" if verbose == 1
|
193
|
+
log "Added pool for #{name} with #{options}" if verbose >= 2
|
194
|
+
end
|
195
|
+
|
196
|
+
def after_fork(&block)
|
197
|
+
@after_forks ||= []
|
198
|
+
@after_forks << block
|
199
|
+
end
|
200
|
+
|
201
|
+
# ----------------------------------------------------------------------------
|
202
|
+
private
|
203
|
+
# ----------------------------------------------------------------------------
|
204
|
+
|
205
|
+
LOCAL_ENVS = [ "development", "test" ]
|
206
|
+
|
207
|
+
def local?
|
208
|
+
LOCAL_ENVS.member?(env)
|
209
|
+
end
|
210
|
+
|
211
|
+
def default_root
|
212
|
+
local? ? "." : "/tmp/#{name}"
|
213
|
+
end
|
214
|
+
|
215
|
+
def mkdirp(*ps)
|
216
|
+
ps.each { |p| Dir.mkdir(p) if ! Dir.exists?(p) }
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Idler wraps some other callable (a proc or object which responds to #call)
|
6
|
+
The callable should return a falsy value when it did nothing,
|
7
|
+
or a truthy value when it did something.
|
8
|
+
The idler will sleep when there is nothing to do.
|
9
|
+
|
10
|
+
=end
|
11
|
+
|
12
|
+
class Collective::Idler
|
13
|
+
|
14
|
+
MIN_SLEEP = 0.125
|
15
|
+
MAX_SLEEP = 1.0
|
16
|
+
|
17
|
+
attr :sleep
|
18
|
+
|
19
|
+
def initialize( callable = nil, options = {}, &callable_block )
|
20
|
+
@callable = callable || callable_block
|
21
|
+
raise unless @callable.respond_to?(:call)
|
22
|
+
|
23
|
+
@max_sleep = options[:max_sleep] || MAX_SLEEP
|
24
|
+
raise if @max_sleep <= 0
|
25
|
+
|
26
|
+
@min_sleep = options[:min_sleep] || MIN_SLEEP
|
27
|
+
raise if @min_sleep <= 0
|
28
|
+
raise if @max_sleep < @min_sleep
|
29
|
+
|
30
|
+
@sleep = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def call( *args, &block )
|
34
|
+
|
35
|
+
result = call_with_wakefulness( @callable, *args, &block )
|
36
|
+
|
37
|
+
if result then
|
38
|
+
wake
|
39
|
+
else
|
40
|
+
sleep_more
|
41
|
+
end
|
42
|
+
|
43
|
+
return result
|
44
|
+
end
|
45
|
+
|
46
|
+
def call_with_wakefulness( callable, *args, &block )
|
47
|
+
begin
|
48
|
+
callable.call(*args,&block)
|
49
|
+
rescue Exception # when errors occur,
|
50
|
+
@sleep = @min_sleep # reduce sleeping almost all the way (but not to 0)
|
51
|
+
raise # do not consume any exceptions
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def sleep_more
|
56
|
+
if @sleep then
|
57
|
+
@sleep = [ @sleep * 2, @max_sleep ].min
|
58
|
+
else
|
59
|
+
@sleep = @min_sleep
|
60
|
+
end
|
61
|
+
Kernel.sleep(@sleep) if @sleep # Interrupt will propogate through sleep().
|
62
|
+
end
|
63
|
+
|
64
|
+
def wake
|
65
|
+
@sleep = nil
|
66
|
+
end
|
67
|
+
|
68
|
+
module Utilities
|
69
|
+
# execute test repeatedly, until timeout, or until test returns true
|
70
|
+
def wait_until( timeout = 1, &test )
|
71
|
+
tester = Collective::Idler.new(test)
|
72
|
+
finish = Time.now.to_f + timeout
|
73
|
+
loop do
|
74
|
+
break if tester.call
|
75
|
+
break if finish < Time.now.to_f
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
extend Utilities
|
80
|
+
|
81
|
+
end # Collective::Idler
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
A key uniquely identifies a worker.
|
6
|
+
|
7
|
+
=end
|
8
|
+
|
9
|
+
class Collective::Key
|
10
|
+
|
11
|
+
attr :name
|
12
|
+
attr :pid
|
13
|
+
attr :host
|
14
|
+
|
15
|
+
def initialize( name, pid, host = Collective::Key.local_host )
|
16
|
+
@name = name
|
17
|
+
@pid = pid.to_i
|
18
|
+
@host = host
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
self.equal?(other) ||
|
23
|
+
( name == other.name && pid == other.pid && host == other.host )
|
24
|
+
end
|
25
|
+
|
26
|
+
# e.g. processor-1234@foo.example.com
|
27
|
+
def to_s
|
28
|
+
"%s-%i@%s" % [ name, pid, host ]
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.parse(key_string)
|
32
|
+
key_string =~ /^(.*)-([0-9]+)@([^@]+)$/ or raise MalformedKey.new(key_string)
|
33
|
+
new( $1, $2, $3 )
|
34
|
+
end
|
35
|
+
|
36
|
+
# ----------------------------------------------------------------------------
|
37
|
+
# Utilities
|
38
|
+
# ----------------------------------------------------------------------------
|
39
|
+
|
40
|
+
# @returns something like foo.example.com
|
41
|
+
def self.local_host
|
42
|
+
@local_host ||= `hostname`.chomp.strip
|
43
|
+
end
|
44
|
+
|
45
|
+
class MalformedKey < StandardError
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
class Collective::LifecycleObserver < Collective::Utilities::ObserverBase
|
4
|
+
|
5
|
+
attr :key
|
6
|
+
attr :registry
|
7
|
+
|
8
|
+
def initialize( key, registry )
|
9
|
+
@key = key
|
10
|
+
@registry = registry
|
11
|
+
end
|
12
|
+
|
13
|
+
def worker_started
|
14
|
+
registry.register( key )
|
15
|
+
end
|
16
|
+
|
17
|
+
def worker_heartbeat( upcount = 0 )
|
18
|
+
registry.update( key )
|
19
|
+
end
|
20
|
+
|
21
|
+
def worker_stopped
|
22
|
+
registry.unregister( key )
|
23
|
+
end
|
24
|
+
|
25
|
+
end # Collective::LifecycleObserver
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Collective::Log
|
2
|
+
|
3
|
+
def log( *args )
|
4
|
+
logger.print(format_for_logging(*args))
|
5
|
+
logger.flush
|
6
|
+
end
|
7
|
+
|
8
|
+
def format_for_logging( *args )
|
9
|
+
message = [
|
10
|
+
#(Time.now.strftime "%Y%m%d%H%M%S"),
|
11
|
+
Time.now.to_i,
|
12
|
+
" [",
|
13
|
+
Process.pid,
|
14
|
+
(Thread.current[:name] || Thread.current.object_id unless Thread.current == Thread.main),
|
15
|
+
"] ",
|
16
|
+
args.join(", "),
|
17
|
+
"\n"
|
18
|
+
].compact.join
|
19
|
+
end
|
20
|
+
|
21
|
+
def logger
|
22
|
+
@logger ||= STDOUT
|
23
|
+
end
|
24
|
+
|
25
|
+
def logger=( other )
|
26
|
+
@logger = other
|
27
|
+
end
|
28
|
+
|
29
|
+
end # Collective::Log
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "digest/md5"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
=begin
|
7
|
+
|
8
|
+
Messager is used to send messages between processes, and receive responses.
|
9
|
+
Messager messages are asynchronous and not ordered.
|
10
|
+
|
11
|
+
=end
|
12
|
+
|
13
|
+
class Collective::Messager
|
14
|
+
|
15
|
+
attr :callbacks
|
16
|
+
attr :storage
|
17
|
+
attr :my_address
|
18
|
+
attr :to_address
|
19
|
+
|
20
|
+
# @param options[:to_address] is optional
|
21
|
+
# @param options[:my_address] is required
|
22
|
+
def initialize( storage, options = {} )
|
23
|
+
@callbacks = {}
|
24
|
+
@storage = storage
|
25
|
+
@to_address = options[:to_address]
|
26
|
+
@my_address = options[:my_address] or raise "must specify my address"
|
27
|
+
# type checking
|
28
|
+
storage.get("test")
|
29
|
+
end
|
30
|
+
|
31
|
+
# write to another queue
|
32
|
+
# @param options[:to] is required if :to_address was not given
|
33
|
+
# @returns an id
|
34
|
+
def send( body, options = {} )
|
35
|
+
to = options[:to] || to_address or raise "must specify to address"
|
36
|
+
from = options[:from] || my_address or raise "must specify from address"
|
37
|
+
now = options[:at] || Time.now
|
38
|
+
message = Message.new( options.merge( to: to, from: my_address, at: now, body: body ) )
|
39
|
+
blob = message.to_json
|
40
|
+
|
41
|
+
storage.queue_add( queue_name(to), blob, now.to_i )
|
42
|
+
message.id
|
43
|
+
end
|
44
|
+
|
45
|
+
# register a handler for a given id
|
46
|
+
# the handler is removed when it is called
|
47
|
+
def expect( match, &callback )
|
48
|
+
@callbacks[match] = callback
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# sends a new message to the original message source and with reply_to_id from the original message
|
53
|
+
# @param options[:to] must be the original message
|
54
|
+
# @e.g. reply "Ok", to: question
|
55
|
+
def reply( body, options )
|
56
|
+
original = options[:to] or raise "must reply to: message"
|
57
|
+
send( body, to: original.from, reply_to_id: original.id )
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param reply_block takes (body, headers)
|
61
|
+
def expect_reply( src_id, &reply_block )
|
62
|
+
raise
|
63
|
+
end
|
64
|
+
|
65
|
+
# read from my queue
|
66
|
+
# check to see if there are any messages, and dispatch them
|
67
|
+
# @returns true if processed a message, false otherwise
|
68
|
+
def receive()
|
69
|
+
now = Time.now.to_i
|
70
|
+
json = storage.queue_pop( queue_name, now )
|
71
|
+
if json then
|
72
|
+
message = Message.parse(json)
|
73
|
+
callback = find_callback( message )
|
74
|
+
callback.call( message )
|
75
|
+
true
|
76
|
+
else
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# ----------------------------------------------------------------------------
|
82
|
+
# Message contains the body and critical headers for Messager
|
83
|
+
# ----------------------------------------------------------------------------
|
84
|
+
|
85
|
+
class Message
|
86
|
+
|
87
|
+
attr :to # destination host
|
88
|
+
attr :from # source host
|
89
|
+
attr :at # timestamp of message generation
|
90
|
+
attr :body # JSON-compatible
|
91
|
+
attr :id # autogenerated if not supplied
|
92
|
+
attr :reply_to_id # optional
|
93
|
+
|
94
|
+
def initialize( data )
|
95
|
+
data = ::Collective::Messager.symbolize(data)
|
96
|
+
@to = data[:to] or raise "must specify to address"
|
97
|
+
@from = data[:from] or raise "must specify from address"
|
98
|
+
@at = (data[:at] || Time.now).to_f
|
99
|
+
@body = data[:body]
|
100
|
+
@id = data[:id] || Digest::MD5.hexdigest([from,at,body].join)
|
101
|
+
@reply_to_id = data[:reply_to_id]
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_hash
|
105
|
+
blob = { to: to, from: from, at: at, body: body, id: id }
|
106
|
+
blob[:reply_to_id] = reply_to_id if reply_to_id
|
107
|
+
blob
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_json
|
111
|
+
to_hash.to_json
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_s
|
115
|
+
to_json
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.parse( json )
|
119
|
+
new( JSON.parse(json) )
|
120
|
+
end
|
121
|
+
|
122
|
+
end # Message
|
123
|
+
|
124
|
+
# ----------------------------------------------------------------------------
|
125
|
+
# Utilities
|
126
|
+
# ----------------------------------------------------------------------------
|
127
|
+
|
128
|
+
def self.stringify(map)
|
129
|
+
Hash[ map.map { |k,v| [ k.to_s, v ] } ]
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.symbolize(map)
|
133
|
+
Hash[ map.map { |k,v| [ k.to_sym, v ] } ]
|
134
|
+
end
|
135
|
+
|
136
|
+
# ----------------------------------------------------------------------------
|
137
|
+
protected
|
138
|
+
# ----------------------------------------------------------------------------
|
139
|
+
|
140
|
+
def queue_name( other_address = nil )
|
141
|
+
if other_address then
|
142
|
+
"messages:#{other_address}"
|
143
|
+
else
|
144
|
+
@queue_name ||= "messages:#{my_address}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# ----------------------------------------------------------------------------
|
149
|
+
# Match
|
150
|
+
# ----------------------------------------------------------------------------
|
151
|
+
|
152
|
+
class NoMatch < Exception
|
153
|
+
end
|
154
|
+
|
155
|
+
class Counter
|
156
|
+
def match
|
157
|
+
@value ||= 0
|
158
|
+
@value += 1
|
159
|
+
end
|
160
|
+
def fail
|
161
|
+
@value = nil
|
162
|
+
raise NoMatch
|
163
|
+
end
|
164
|
+
def value
|
165
|
+
raise NoMatch if !@value
|
166
|
+
@value
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def find_callback( message )
|
171
|
+
best_result = nil
|
172
|
+
best_score = nil
|
173
|
+
callbacks.each do |match,callback|
|
174
|
+
begin
|
175
|
+
counter = Counter.new
|
176
|
+
compare_match( message, match, counter )
|
177
|
+
if !best_score || counter.value > best_score then
|
178
|
+
best_score = counter.value
|
179
|
+
best_result = callback
|
180
|
+
end
|
181
|
+
rescue NoMatch
|
182
|
+
# next
|
183
|
+
end
|
184
|
+
end
|
185
|
+
return best_result if best_result
|
186
|
+
debugger
|
187
|
+
raise NoMatch
|
188
|
+
end
|
189
|
+
|
190
|
+
def compare_match( message, match, counter )
|
191
|
+
case match
|
192
|
+
when String, Regexp
|
193
|
+
compare( message.body, match, counter )
|
194
|
+
when Hash
|
195
|
+
compare( message.to_hash, match, counter )
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def compare( item, match, counter )
|
200
|
+
case match
|
201
|
+
when Numeric
|
202
|
+
return item.kind_of?(Numeric) && item == match ? counter.match : counter.fail
|
203
|
+
when String
|
204
|
+
return item.kind_of?(String) && item == match ? counter.match : counter.fail
|
205
|
+
when Regexp
|
206
|
+
return item.kind_of?(String) && item =~ match ? counter.match : counter.fail
|
207
|
+
when Hash
|
208
|
+
counter.fail if ! item.kind_of?(Hash)
|
209
|
+
match.each do |k,v|
|
210
|
+
counter.fail if ! item.has_key?(k)
|
211
|
+
compare( item[k], match[k], counter )
|
212
|
+
end
|
213
|
+
else
|
214
|
+
raise "Can not compare using #{match.inspect}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|