hayeah-ASS 0.0.1 → 0.0.2
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/ASS.gemspec +2 -2
- data/README.textile +162 -0
- data/VERSION.yml +1 -1
- data/lib/ass.rb +338 -137
- metadata +2 -2
data/ASS.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{ASS}
|
5
|
-
s.version = "0.0.
|
5
|
+
s.version = "0.0.2"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Howard Yeh"]
|
9
|
-
s.date = %q{2009-08-
|
9
|
+
s.date = %q{2009-08-17}
|
10
10
|
s.email = %q{hayeah@gmail.com}
|
11
11
|
s.extra_rdoc_files = [
|
12
12
|
"LICENSE",
|
data/README.textile
CHANGED
@@ -0,0 +1,162 @@
|
|
1
|
+
|
2
|
+
Asynchronous Service Stages (ASS) is a way to
|
3
|
+
organize distributed services by decoupling the
|
4
|
+
what and how of computation from the when and
|
5
|
+
where. Built on top of RabbitMQ (an implementation
|
6
|
+
of AMQP), ASS helps you to build robust and
|
7
|
+
scalable distributed applications.
|
8
|
+
|
9
|
+
End of project pimping, let's get started.
|
10
|
+
|
11
|
+
h1. Install
|
12
|
+
|
13
|
+
You need
|
14
|
+
|
15
|
+
(1) Erlang
|
16
|
+
(2) RabbitMQ
|
17
|
+
(3) AMQP gem
|
18
|
+
|
19
|
+
Thread.new { EM.run }
|
20
|
+
AMQP.start
|
21
|
+
|
22
|
+
^C to exit
|
23
|
+
|
24
|
+
|
25
|
+
h1. The Basics
|
26
|
+
|
27
|
+
A service component is a ruby script that
|
28
|
+
communicates with RabbitMQ. You need to define the
|
29
|
+
AMQP server your ASS depends on. Something like this,
|
30
|
+
|
31
|
+
|
32
|
+
require 'rubygems'
|
33
|
+
require 'ass'
|
34
|
+
AMQP.start(:host => 'localhost',
|
35
|
+
#:vhost => "/ass-test",
|
36
|
+
:logging => false) do
|
37
|
+
# ASS definition
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
To start a server
|
42
|
+
|
43
|
+
server = ASS.new("echo")
|
44
|
+
# => #<ASS::Server echo>
|
45
|
+
|
46
|
+
But it doesn't do anything yet. You define the
|
47
|
+
behaviour of the server by setting its
|
48
|
+
callback. The callback can be a class, so that for
|
49
|
+
each client request an object is created from the
|
50
|
+
class to process the request. Like so,
|
51
|
+
|
52
|
+
|
53
|
+
server.react(SomeReactorClass)
|
54
|
+
|
55
|
+
|
56
|
+
However, often you just want something simple. The
|
57
|
+
react method can take a block and construct an
|
58
|
+
anonymous callback class from which the server
|
59
|
+
creates an callback object for each request. Here
|
60
|
+
we ask the server to react to @foo@ or @bar@.
|
61
|
+
|
62
|
+
|
63
|
+
server.react {
|
64
|
+
def foo(input)
|
65
|
+
[:server,:foo,input]
|
66
|
+
end
|
67
|
+
|
68
|
+
def oof(input)
|
69
|
+
[:server,:oof,input]
|
70
|
+
end
|
71
|
+
}
|
72
|
+
|
73
|
+
|
74
|
+
The react method accepts for the callback either
|
75
|
+
a Class, a Module, a block, or any object. When an
|
76
|
+
object is used, it's considered a singleton, which
|
77
|
+
is used to process all the requests.
|
78
|
+
|
79
|
+
Now that we have a server, we need to get a client
|
80
|
+
so to call the server. Because the call is
|
81
|
+
asynchronous (the client doesn't wait for the
|
82
|
+
result), to process the result when it gets back,
|
83
|
+
we need to define callback for the client (just as
|
84
|
+
we did for the server). For each call to the
|
85
|
+
remote server, the result is processed at the
|
86
|
+
client side by a method of the same name,
|
87
|
+
|
88
|
+
|
89
|
+
client = server.client.react {
|
90
|
+
def foo(output)
|
91
|
+
p [:client,:foo,output]
|
92
|
+
end
|
93
|
+
def oof(output)
|
94
|
+
p [:client,:oof,output]
|
95
|
+
end
|
96
|
+
}
|
97
|
+
|
98
|
+
c.call(:foo,42)
|
99
|
+
c.call(:oof,24)
|
100
|
+
|
101
|
+
# [:client,:foo,[:server,:foo,42]]
|
102
|
+
# [:client,:foo,[:server,:foo,24]]
|
103
|
+
|
104
|
+
|
105
|
+
> ruby server.rb
|
106
|
+
> ruby client.rb
|
107
|
+
|
108
|
+
> ruby server.rb
|
109
|
+
^C
|
110
|
+
|
111
|
+
While the server is down, the requests the client
|
112
|
+
is making is queued by the underlying message
|
113
|
+
middleware (RabbitMQ), so in some future time when
|
114
|
+
we restart the server, we wouldn't lose any
|
115
|
+
request. Let's restart the server.
|
116
|
+
|
117
|
+
> ruby server.rb
|
118
|
+
|
119
|
+
See that the server caught up with all the pending
|
120
|
+
requests. To increase service capacity, we can
|
121
|
+
just increase the number of server instances.
|
122
|
+
|
123
|
+
> ruby server.rb
|
124
|
+
|
125
|
+
Now the load is distributed between these two
|
126
|
+
instances. We can also start more clients to
|
127
|
+
handle more load.
|
128
|
+
|
129
|
+
> ruby client.rb
|
130
|
+
|
131
|
+
You can see requests coming in from two clients.
|
132
|
+
|
133
|
+
|
134
|
+
h1. Service Configuration
|
135
|
+
|
136
|
+
-how the name of a service map to AMQP entities.
|
137
|
+
-various options for different functional characteristics.
|
138
|
+
|
139
|
+
-using routing_key
|
140
|
+
-using reply_to
|
141
|
+
|
142
|
+
|
143
|
+
rabbitmqctl list_exchanges
|
144
|
+
rabbitmqctl list_queues
|
145
|
+
|
146
|
+
|
147
|
+
h1. RPC service
|
148
|
+
|
149
|
+
The RPC client provides a synchronous API to the
|
150
|
+
asynchronous services. This is so the users of an
|
151
|
+
ASS application don't have to bother with the
|
152
|
+
difficulties of asynchronous programming style
|
153
|
+
with callbacks.
|
154
|
+
|
155
|
+
The RPC client is intended to be used as the
|
156
|
+
gateway into some reliable internal
|
157
|
+
services. While the internal services coordinated
|
158
|
+
with ASS needs to be robust to component failures,
|
159
|
+
there's no such requirements for gateways. It is
|
160
|
+
ok for a gateway to fail, and fail in delivering a
|
161
|
+
response, as long as the internal services carry
|
162
|
+
out the task without a hitch.
|
data/VERSION.yml
CHANGED
data/lib/ass.rb
CHANGED
@@ -1,137 +1,209 @@
|
|
1
1
|
require 'mq'
|
2
|
-
class Ass
|
3
|
-
|
4
|
-
attr_reader :server_exchange
|
5
2
|
|
6
|
-
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
# uh... we'll assume that the exchanges are all direct exchanges.
|
11
|
-
def initialize(server_exchange)
|
12
|
-
@server_exchange = get_exchange(server_exchange)
|
13
|
-
end
|
3
|
+
# TODO a way to specify serializer (json, marshal...)
|
4
|
+
module ASS
|
14
5
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
Client.new(@server_exchange,@client_exchange,q)
|
6
|
+
# non-destructive get. Fail if server's not started.
|
7
|
+
def self.get(name)
|
8
|
+
ASS::Server.new(name,:passive => true)
|
19
9
|
end
|
20
10
|
|
21
|
-
def
|
22
|
-
Server.new(
|
11
|
+
def self.new(name,opts={})
|
12
|
+
ASS::Server.new(name)
|
23
13
|
end
|
24
14
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
exchanges = exchange[0]
|
29
|
-
opts = exchange[1]
|
30
|
-
else
|
31
|
-
exchange = arg
|
32
|
-
opts = nil
|
33
|
-
end
|
34
|
-
opts = {} if opts.nil?
|
35
|
-
exchange = exchange.is_a?(MQ::Exchange) ? exchange : MQ.direct(exchange,opts)
|
36
|
-
raise "accepts only direct exchanges" unless exchange.type == :direct
|
37
|
-
exchange
|
15
|
+
def self.peep(server_name,callback=nil,&block)
|
16
|
+
callback = block if callback.nil?
|
17
|
+
ASS::Peeper.new(server_name,callback)
|
38
18
|
end
|
39
|
-
|
40
|
-
# can specify a key to create a queue for that subdomain
|
41
|
-
def get_queue(exchange,*args)
|
42
|
-
case args[0]
|
43
|
-
when Hash
|
44
|
-
key = nil
|
45
|
-
opts = args[0]
|
46
|
-
when String
|
47
|
-
key = args[0]
|
48
|
-
opts = args[1]
|
49
|
-
end
|
50
|
-
opts = {} if opts.nil?
|
51
|
-
if key
|
52
|
-
name = "#{exchange.name}--#{key}"
|
53
|
-
q = MQ.queue(name,opts)
|
54
|
-
q.bind(exchange,{ :routing_key => key})
|
55
|
-
else
|
56
|
-
q = MQ.queue(exchange.name,opts)
|
57
|
-
q.bind(exchange,{ :routing_key => exchange.name })
|
58
|
-
end
|
59
|
-
q
|
60
|
-
end
|
61
|
-
|
19
|
+
|
62
20
|
module Callback
|
21
|
+
module MagicMethods
|
22
|
+
def header
|
23
|
+
@__header__
|
24
|
+
end
|
25
|
+
|
26
|
+
def meta
|
27
|
+
@__meta__
|
28
|
+
end
|
29
|
+
|
30
|
+
def service
|
31
|
+
@__service__
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(method,data=nil,meta=nil,opts={})
|
35
|
+
@__service__.call(method,data,meta,opts)
|
36
|
+
end
|
37
|
+
end
|
63
38
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
39
|
+
# called to initiate a callback
|
40
|
+
def build_callback(callback)
|
41
|
+
c = case callback
|
42
|
+
when Proc
|
43
|
+
Class.new &callback
|
44
|
+
when Class
|
45
|
+
callback
|
46
|
+
when Module
|
47
|
+
Class.new { include callback }
|
48
|
+
when Object
|
49
|
+
callback # use singleton objcet as callback
|
50
|
+
end
|
51
|
+
case c
|
68
52
|
when Class
|
69
|
-
|
70
|
-
|
71
|
-
|
53
|
+
c.instance_eval { include MagicMethods }
|
54
|
+
else
|
55
|
+
c.extend MagicMethods
|
72
56
|
end
|
57
|
+
c
|
73
58
|
end
|
74
|
-
|
75
|
-
|
59
|
+
|
60
|
+
# called for each request
|
61
|
+
def prepare_callback(callback,info,payload)
|
76
62
|
# method,data,meta
|
77
|
-
if
|
78
|
-
|
63
|
+
if callback.is_a? Class
|
64
|
+
if callback.respond_to? :version
|
65
|
+
klass = callback.get_version(payload[:version])
|
66
|
+
else
|
67
|
+
klass = callback
|
68
|
+
end
|
69
|
+
obj = klass.new
|
79
70
|
else
|
80
|
-
|
71
|
+
obj = callback
|
81
72
|
end
|
82
|
-
obj
|
83
|
-
service = self
|
84
|
-
obj.instance_variable_set("@__service__",service)
|
73
|
+
obj.instance_variable_set("@__service__",self)
|
85
74
|
obj.instance_variable_set("@__header__",info)
|
86
75
|
obj.instance_variable_set("@__meta__",payload[:meta])
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
76
|
+
#p [:call,payload]
|
77
|
+
obj
|
78
|
+
end
|
79
|
+
end
|
91
80
|
|
92
|
-
|
93
|
-
|
94
|
-
end
|
95
|
-
|
96
|
-
def service
|
97
|
-
@__service__
|
98
|
-
end
|
81
|
+
class Server
|
82
|
+
include Callback
|
99
83
|
|
100
|
-
|
101
|
-
|
102
|
-
|
84
|
+
def initialize(name,opts={})
|
85
|
+
@server_exchange = MQ.fanout(name,opts)
|
86
|
+
end
|
87
|
+
|
88
|
+
def name
|
89
|
+
self.exchange.name
|
90
|
+
end
|
91
|
+
|
92
|
+
def exchange
|
93
|
+
@server_exchange
|
94
|
+
end
|
95
|
+
|
96
|
+
# takes options available to MQ::Exchange
|
97
|
+
def client(opts={})
|
98
|
+
ASS::Client.new(self,opts)
|
99
|
+
end
|
100
|
+
|
101
|
+
def client_name
|
102
|
+
"#{self.exchange.name}--"
|
103
|
+
end
|
104
|
+
|
105
|
+
# takes options available to MQ::Queue# takes options available to MQ::Queue#subscribe
|
106
|
+
def rpc(opts={})
|
107
|
+
ASS::RPC.new(self,opts)
|
108
|
+
end
|
109
|
+
|
110
|
+
def queue(opts={})
|
111
|
+
unless @queue
|
112
|
+
@queue ||= MQ.queue(self.name,opts)
|
113
|
+
@queue.bind(self.exchange)
|
103
114
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
115
|
+
self
|
116
|
+
end
|
117
|
+
|
118
|
+
# takes options available to MQ::Queue# takes options available to MQ::Queue#subscribe
|
119
|
+
def react(callback=nil,opts=nil,&block)
|
120
|
+
if block
|
121
|
+
opts = callback
|
122
|
+
callback = block
|
123
|
+
end
|
124
|
+
opts = {} if opts.nil?
|
125
|
+
|
126
|
+
@callback = build_callback(callback)
|
127
|
+
@ack = opts[:ack]
|
128
|
+
self.queue unless @queue
|
129
|
+
@queue.subscribe(opts) do |info,payload|
|
130
|
+
payload = ::Marshal.load(payload)
|
131
|
+
#p [info,info.reply_to,payload]
|
132
|
+
obj = prepare_callback(@callback,info,payload)
|
133
|
+
data2 = obj.send(payload[:method],payload[:data])
|
134
|
+
payload2 = payload.merge :data => data2
|
135
|
+
# the client MUST exist, otherwise it's an error.
|
136
|
+
## FIXME it's bad if the server dies b/c
|
137
|
+
## the client isn't there. It's bad that
|
138
|
+
## this can cause the server to fail.
|
139
|
+
MQ.direct(info.reply_to,:passive => true).
|
140
|
+
publish(::Marshal.dump(payload2),
|
141
|
+
:routing_key => info.routing_key,
|
142
|
+
:message_id => info.message_id) if info.reply_to
|
143
|
+
info.ack if @ack
|
144
|
+
end
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
def inspect
|
149
|
+
"#<#{self.class} #{self.name}>"
|
107
150
|
end
|
108
151
|
end
|
109
152
|
|
110
153
|
class Client
|
111
154
|
include Callback
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
@
|
155
|
+
|
156
|
+
# takes options available to MQ::Exchange
|
157
|
+
def initialize(server,opts={})
|
158
|
+
@server = server
|
159
|
+
# the routing key is also used as the name of the client
|
160
|
+
@key = opts.delete :key
|
161
|
+
@key = @key.to_s if @key
|
162
|
+
@client_exchange = MQ.direct @server.client_name, opts
|
116
163
|
end
|
117
|
-
|
164
|
+
|
165
|
+
def name
|
166
|
+
self.exchange.name
|
167
|
+
end
|
168
|
+
|
169
|
+
def exchange
|
170
|
+
@client_exchange
|
171
|
+
end
|
172
|
+
|
173
|
+
# takes options available to MQ::Queue
|
174
|
+
def queue(opts={})
|
175
|
+
unless @queue
|
176
|
+
# if key is not given, the queue name is
|
177
|
+
# the same as the exchange name.
|
178
|
+
@queue ||= MQ.queue("#{self.name}#{@key}",opts)
|
179
|
+
@queue.bind(self.exchange,:routing_key => @key || self.name)
|
180
|
+
end
|
181
|
+
self # return self to allow chaining
|
182
|
+
end
|
183
|
+
|
184
|
+
# takes options available to MQ::Queue#subscribe
|
118
185
|
def react(callback=nil,opts=nil,&block)
|
119
186
|
if block
|
120
187
|
opts = callback
|
121
188
|
callback = block
|
122
189
|
end
|
123
190
|
opts = {} if opts.nil?
|
124
|
-
|
125
|
-
@
|
191
|
+
|
192
|
+
@callback = build_callback(callback)
|
126
193
|
@ack = opts[:ack]
|
194
|
+
# ensure queue is set
|
195
|
+
self.queue unless @queue
|
127
196
|
@queue.subscribe(opts) do |info,payload|
|
128
197
|
payload = ::Marshal.load(payload)
|
129
|
-
callback
|
198
|
+
obj = prepare_callback(@callback,info,payload)
|
199
|
+
obj.send(payload[:method],payload[:data])
|
130
200
|
info.ack if @ack
|
131
201
|
end
|
132
202
|
self
|
133
203
|
end
|
134
|
-
|
204
|
+
|
205
|
+
# note that we can redirect the result to some
|
206
|
+
# place else by setting :key and :reply_to
|
135
207
|
def call(method,data=nil,meta=nil,opts={})
|
136
208
|
# opts passed to publish
|
137
209
|
# if no routing key is given, use receiver's name as the routing key.
|
@@ -143,70 +215,199 @@ class Ass
|
|
143
215
|
:version => version
|
144
216
|
}
|
145
217
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
@server_exchange.publish Marshal.dump(payload), {
|
151
|
-
:key => (key ? key : @server_exchange.name),
|
152
|
-
:reply_to => @client_exchange.name
|
218
|
+
@server.exchange.publish Marshal.dump(payload), {
|
219
|
+
# opts[:routing_key] will override :key in MQ::Exchange#publish
|
220
|
+
:key => (@key ? @key : self.name),
|
221
|
+
:reply_to => self.name
|
153
222
|
}.merge(opts)
|
154
223
|
end
|
155
|
-
|
224
|
+
|
156
225
|
# for casting, just null the reply_to field, so server doesn't respond.
|
157
226
|
def cast(method,data=nil,meta=nil,opts={})
|
158
227
|
self.call(method,data,meta,opts.merge({:reply_to => nil}))
|
159
228
|
end
|
160
229
|
|
161
|
-
|
230
|
+
def inspect
|
231
|
+
"#<#{self.class} #{self.name}>"
|
232
|
+
end
|
162
233
|
end
|
163
234
|
|
164
|
-
|
165
|
-
|
235
|
+
# assumes server initializes it with an exclusive and auto_delete queue.
|
236
|
+
# TODO timeout
|
237
|
+
class RPC
|
238
|
+
require 'thread'
|
239
|
+
|
240
|
+
# i don't want deferrable. I want actual blockage when waiting.
|
241
|
+
## subscribe prolly run in a different thread.
|
242
|
+
# hmmm. I guess deferrable is a better idea.
|
243
|
+
class Future
|
244
|
+
attr_reader :message_id
|
245
|
+
attr_accessor :header, :data, :meta, :timeout
|
246
|
+
def initialize(rpc,message_id)
|
247
|
+
@message_id = message_id
|
248
|
+
@rpc = rpc
|
249
|
+
@timeout = false
|
250
|
+
@done = false
|
251
|
+
end
|
252
|
+
|
253
|
+
def wait(timeout=nil,&block)
|
254
|
+
# TODO timeout with eventmachine
|
255
|
+
@rpc.wait(self,timeout,&block) # synchronous call that will block
|
256
|
+
# EM.cancel_timer(ticket)
|
257
|
+
end
|
258
|
+
|
259
|
+
def done!
|
260
|
+
@done = true
|
261
|
+
end
|
166
262
|
|
167
|
-
|
168
|
-
|
169
|
-
|
263
|
+
def done?
|
264
|
+
@done
|
265
|
+
end
|
266
|
+
|
267
|
+
def timeout?
|
268
|
+
@timeout
|
269
|
+
end
|
270
|
+
|
271
|
+
def inspect
|
272
|
+
"#<#{self.class} #{message_id}>"
|
273
|
+
end
|
170
274
|
end
|
171
275
|
|
172
|
-
|
173
|
-
|
174
|
-
|
276
|
+
class Reactor
|
277
|
+
# want to minimize name conflicts here.
|
278
|
+
def initialize(rpc)
|
279
|
+
@rpc = rpc
|
280
|
+
end
|
281
|
+
|
282
|
+
def method_missing(_method,data)
|
283
|
+
@rpc.buffer << [header,data,meta]
|
284
|
+
end
|
175
285
|
end
|
176
286
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
287
|
+
attr_reader :buffer, :futures, :ready
|
288
|
+
def initialize(server,opts={})
|
289
|
+
@server = server
|
290
|
+
@seq = 0
|
291
|
+
# queue is used be used to synchronize RPC
|
292
|
+
# user thread and the AMQP eventmachine thread.
|
293
|
+
@buffer = Queue.new
|
294
|
+
@ready = {} # the ready results not yet waited
|
295
|
+
@futures = {} # all futures not yet waited for.
|
296
|
+
@reactor = Reactor.new(self)
|
297
|
+
# Creates an exclusive queue to serve the RPC client.
|
298
|
+
@client = @server.client(:key => "rpc.#{rand(999_999_999_999)}").
|
299
|
+
queue(:exclusive => true).react(@reactor,opts)
|
300
|
+
end
|
301
|
+
|
302
|
+
def call(method,data,meta=nil,opts={})
|
303
|
+
message_id = @seq.to_s # message gotta be unique for this RPC client.
|
304
|
+
@client.call method, data, meta, opts.merge(:message_id => message_id)
|
305
|
+
@seq += 1
|
306
|
+
@futures[message_id] = Future.new(self,message_id)
|
307
|
+
end
|
308
|
+
|
309
|
+
# the idea is to block on a synchronized queue
|
310
|
+
# until we get the future we want.
|
311
|
+
#
|
312
|
+
# WARNING: blocks forever if the thread
|
313
|
+
# calling wait is the same as the EventMachine
|
314
|
+
# thread.
|
315
|
+
def wait(future,timeout=nil)
|
316
|
+
return future.data if future.done? # future was waited before
|
317
|
+
timer = nil
|
318
|
+
if timeout
|
319
|
+
timer = EM.add_timer(timeout) {
|
320
|
+
@buffer << [:timeout,future.message_id,nil]
|
321
|
+
}
|
181
322
|
end
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
323
|
+
ready_future = nil
|
324
|
+
if @ready.has_key? future.message_id
|
325
|
+
@ready.delete future.message_id
|
326
|
+
ready_future = future
|
327
|
+
else
|
328
|
+
while true
|
329
|
+
header,data,meta = data = @buffer.pop # synchronize. like erlang's mailbox select.
|
330
|
+
if header == :timeout # timeout the future we are waiting for.
|
331
|
+
message_id = data
|
332
|
+
# if we got a timeout from previous wait. throw it away.
|
333
|
+
next if future.message_id != message_id
|
334
|
+
future.timeout = true
|
335
|
+
future.done!
|
336
|
+
@futures.delete future.message_id
|
337
|
+
return yield # return the value of timeout block
|
338
|
+
end
|
339
|
+
some_future = @futures[header.message_id]
|
340
|
+
# If we didn't find the future among the
|
341
|
+
# future, it must have timedout. Just
|
342
|
+
# throw result away and keep processing.
|
343
|
+
next unless some_future
|
344
|
+
some_future.header = header
|
345
|
+
some_future.data = data
|
346
|
+
some_future.meta = meta
|
347
|
+
if some_future == future
|
348
|
+
# The future we are waiting for
|
349
|
+
EM.cancel_timer(timer)
|
350
|
+
ready_future = future
|
351
|
+
break
|
352
|
+
else
|
353
|
+
# Ready, but we are not waiting for it. Save for later.
|
354
|
+
@ready[some_future.message_id] = some_future
|
355
|
+
end
|
199
356
|
end
|
200
|
-
MQ.direct(info.reply_to).publish(::Marshal.dump(payload2),:routing_key => key) if info.reply_to
|
201
|
-
info.ack if @ack
|
202
357
|
end
|
203
|
-
|
358
|
+
ready_future.done!
|
359
|
+
@futures.delete ready_future.message_id
|
360
|
+
return ready_future.data
|
361
|
+
end
|
362
|
+
|
363
|
+
def waitall
|
364
|
+
@futures.values.map { |k,v|
|
365
|
+
wait(v)
|
366
|
+
}
|
367
|
+
end
|
368
|
+
|
369
|
+
def inspect
|
370
|
+
"#<#{self.class} #{self.name}>"
|
204
371
|
end
|
205
372
|
end
|
206
373
|
|
374
|
+
# TODO should prolly have the option of using
|
375
|
+
# non auto-delete queues. This would be useful
|
376
|
+
# for logger. Maybe if a peeper name is given,
|
377
|
+
# then create queues with options.
|
207
378
|
class Peeper
|
208
|
-
|
209
|
-
|
379
|
+
include Callback
|
380
|
+
attr_reader :server_name
|
381
|
+
def initialize(server_name,callback)
|
382
|
+
@server_name = server_name
|
383
|
+
@clients = {}
|
384
|
+
@callback = build_callback(callback)
|
385
|
+
|
386
|
+
uid = "#{@server_name}.peeper.#{rand 999_999_999_999}"
|
387
|
+
q = MQ.queue uid, :auto_delete => true
|
388
|
+
q.bind(@server_name) # messages to the server would be duplicated here.
|
389
|
+
q.subscribe { |info,payload|
|
390
|
+
payload = ::Marshal.load(payload)
|
391
|
+
# sets context, but doesn't make the call
|
392
|
+
obj = prepare_callback(@callback,info,payload)
|
393
|
+
# there is a specific method we want to call.
|
394
|
+
obj.server(payload[:method],payload[:data])
|
395
|
+
|
396
|
+
# bind to peep client message queue if we've not seen it before.
|
397
|
+
unless @clients.has_key? info.routing_key
|
398
|
+
@clients[info.routing_key] = true
|
399
|
+
client_q = MQ.queue "#{uid}--#{info.routing_key}",
|
400
|
+
:auto_delete => true
|
401
|
+
# messages to the client would be duplicated here.
|
402
|
+
client_q.bind("#{server_name}--", :routing_key => info.routing_key)
|
403
|
+
client_q.subscribe { |info,payload|
|
404
|
+
payload = ::Marshal.load(payload)
|
405
|
+
obj = prepare_callback(@callback,info,payload)
|
406
|
+
obj.client(payload[:method],payload[:data])
|
407
|
+
}
|
408
|
+
end
|
409
|
+
}
|
210
410
|
end
|
211
411
|
end
|
412
|
+
|
212
413
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hayeah-ASS
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Howard Yeh
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-08-
|
12
|
+
date: 2009-08-17 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|