collective 0.2.0
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/.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
|