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.
Files changed (5) hide show
  1. data/ASS.gemspec +2 -2
  2. data/README.textile +162 -0
  3. data/VERSION.yml +1 -1
  4. data/lib/ass.rb +338 -137
  5. 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.1"
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-13}
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
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :major: 0
3
3
  :minor: 0
4
- :patch: 1
4
+ :patch: 2
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
- def self.declare(server_exchange)
7
- self.new([server_exchange,{:passive => true}])
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
- def client(client_exchange,*args)
16
- @client_exchange ||= get_exchange(client_exchange)
17
- q = get_queue(@client_exchange,*args)
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 server(*args)
22
- Server.new(@server_exchange,get_queue(@server_exchange,*args))
11
+ def self.new(name,opts={})
12
+ ASS::Server.new(name)
23
13
  end
24
14
 
25
- def get_exchange(arg)
26
- case arg
27
- when Array
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
- def build_callback_klass(callback)
65
- case callback
66
- when Proc
67
- Class.new &callback
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
- callback
70
- when Module
71
- Class.new { include callback }
53
+ c.instance_eval { include MagicMethods }
54
+ else
55
+ c.extend MagicMethods
72
56
  end
57
+ c
73
58
  end
74
-
75
- def callback(info,payload)
59
+
60
+ # called for each request
61
+ def prepare_callback(callback,info,payload)
76
62
  # method,data,meta
77
- if @callback_klass.respond_to? :version
78
- klass = @callback_klass.get_version(payload[:version])
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
- klass = @callback_klass
71
+ obj = callback
81
72
  end
82
- obj = klass.new
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
- class << obj
88
- def header
89
- @__header__
90
- end
76
+ #p [:call,payload]
77
+ obj
78
+ end
79
+ end
91
80
 
92
- def meta
93
- @__meta__
94
- end
95
-
96
- def service
97
- @__service__
98
- end
81
+ class Server
82
+ include Callback
99
83
 
100
- def call(method,data=nil,meta=nil,opts={})
101
- @__service__.call(method,data,meta,opts)
102
- end
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
- #p [:call,payload]
105
- obj.send(payload[:method],
106
- payload[:data])
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
- def initialize(server_exchange,client_exchange,queue)
113
- @server_exchange = server_exchange
114
- @client_exchange = client_exchange
115
- @queue = queue
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
- @callback_klass = build_callback_klass(callback)
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(info,payload)
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
- # set it up s.t. server would respond to
147
- # private queue if key is given, otherwise
148
- # the server would respond to public queue.
149
- key = opts.delete(:key)
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
- class Server
165
- include Callback
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
- def initialize(server_exchange,q)
168
- @queue = q
169
- @server_exchange = server_exchange
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
- attr_reader :queue
173
- def exchange
174
- @server_exchange
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
- def react(callback=nil,opts=nil,&block)
178
- if block
179
- opts = callback
180
- callback = block
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
- opts = {} if opts.nil?
183
-
184
- @callback_klass = build_callback_klass(callback)
185
- @ack = opts[:ack]
186
- @queue.subscribe(opts) do |info,payload|
187
- payload = ::Marshal.load(payload)
188
- #p [info,info.reply_to,payload]
189
- data2 = callback(info,payload)
190
- payload2 = payload.merge :data => data2
191
- if info.routing_key == @server_exchange.name
192
- # addressed to the server's public
193
- # queue, respond to the routing_key of
194
- # the client's public queue.
195
- key = info.reply_to
196
- else
197
- # addressed to the private queue
198
- key = info.routing_key
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
- self
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
- def initialize(exchange,callback)
209
- # create a temporary queue that binds to an exchange
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.1
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-13 00:00:00 -07:00
12
+ date: 2009-08-17 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency