jamesgolick-ASS 0.1.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.
@@ -0,0 +1,19 @@
1
+
2
+ # monkey patch to the amqp gem that adds :no_declare => true option for new
3
+ # Exchange objects. This allows us to send messeages to exchanges that are
4
+ # declared by the mappers and that we have no configuration priviledges on.
5
+ # temporary until we get this into amqp proper
6
+ MQ::Exchange.class_eval do
7
+ def initialize mq, type, name, opts = {}
8
+ @mq = mq
9
+ @type, @name, @opts = type, name, opts
10
+ @mq.exchanges[@name = name] ||= self
11
+ @key = opts[:key]
12
+
13
+ @mq.callback{
14
+ @mq.send AMQP::Protocol::Exchange::Declare.new({ :exchange => name,
15
+ :type => type,
16
+ :nowait => true }.merge(opts))
17
+ } unless name == "amq.#{type}" or name == '' or opts[:no_declare]
18
+ end
19
+ end
@@ -0,0 +1,95 @@
1
+ class ASS::CallbackFactory
2
+
3
+ module ServiceMethods
4
+ def resend
5
+ throw(:__ass_resend)
6
+ end
7
+
8
+ def discard
9
+ throw(:__ass_discard)
10
+ end
11
+
12
+ def header
13
+ @__header__
14
+ end
15
+
16
+ def payload
17
+ @__payload__
18
+ end
19
+
20
+ def method
21
+ @__method__
22
+ end
23
+
24
+ def data
25
+ @__data__
26
+ end
27
+
28
+ def meta
29
+ @__meta__
30
+ end
31
+
32
+ def version
33
+ @__version__
34
+ end
35
+
36
+ def call(name,method,data=nil,opts={},meta=nil)
37
+ @__service__.call(name,method,data,opts,meta)
38
+ end
39
+
40
+ def cast(name,method,data=nil,opts={},meta=nil)
41
+ @__service__.cast(name,method,data,opts,meta)
42
+ end
43
+ end
44
+
45
+ def initialize(callback)
46
+ @factory = build_factory(callback)
47
+ end
48
+
49
+ def callback_for(server,header,payload)
50
+ # method,data
51
+ if @factory.is_a? Class
52
+ if @factory.respond_to? :version
53
+ klass = @factory.get_version(payload[:version])
54
+ else
55
+ klass = @factory
56
+ end
57
+ obj = klass.new
58
+ else
59
+ obj = @factory
60
+ end
61
+ obj.instance_variable_set("@__service__",server)
62
+ obj.instance_variable_set("@__header__",header)
63
+ obj.instance_variable_set("@__payload__",payload)
64
+ obj.instance_variable_set("@__method__",payload[:method])
65
+ obj.instance_variable_set("@__data__",payload[:data])
66
+ obj.instance_variable_set("@__meta__",payload[:meta])
67
+ obj.instance_variable_set("@__version__",payload[:version])
68
+ obj
69
+ end
70
+
71
+ private
72
+
73
+ def build_factory(callback)
74
+ c = case callback
75
+ when Proc
76
+ Class.new &callback
77
+ when Class
78
+ callback
79
+ when Module
80
+ Class.new { include callback }
81
+ else
82
+ raise "can build factory from one of Proc, Class, Module"
83
+ end
84
+ case c
85
+ when Class
86
+ c.instance_eval { include ServiceMethods }
87
+ else
88
+ c.extend ServiceMethods
89
+ end
90
+ c
91
+ end
92
+
93
+
94
+
95
+ end
@@ -0,0 +1,19 @@
1
+ class ASS::Client
2
+ def initialize(opts={})
3
+ @rpc_opts = opts
4
+ end
5
+
6
+ def rpc
7
+ # should lazy start the RPC server
8
+ @rpc ||= ASS.rpc(@rpc_opts)
9
+ end
10
+
11
+ def cast(name,method,data,opts={},meta=nil)
12
+ ASS.cast(name,method,data,opts,meta)
13
+ end
14
+
15
+ # makes synchronized call through ASS::RPC
16
+ def call(name,method,data,opts={},meta=nil)
17
+ rpc.call(name,method,data,opts,meta)
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ # TODO should prolly have the option of using
2
+ # non auto-delete queues. This would be useful
3
+ # for logger. Maybe if a peeper name is given,
4
+ # then create queues with options.
5
+ class Peeper
6
+ include Callback
7
+ attr_reader :server_name
8
+ def initialize(server_name,callback)
9
+ @server_name = server_name
10
+ @clients = {}
11
+ @callback = build_callback(callback)
12
+
13
+ uid = "#{@server_name}.peeper.#{rand 999_999_999_999}"
14
+ q = MQ.queue uid, :auto_delete => true
15
+ q.bind(@server_name) # messages to the server would be duplicated here.
16
+ q.subscribe { |info,payload|
17
+ payload = ::Marshal.load(payload)
18
+ # sets context, but doesn't make the call
19
+ obj = prepare_callback(@callback,info,payload)
20
+ # there is a specific method we want to call.
21
+ obj.server(payload[:method],payload[:data])
22
+
23
+ # bind to peep client message queue if we've not seen it before.
24
+ unless @clients.has_key? info.routing_key
25
+ @clients[info.routing_key] = true
26
+ client_q = MQ.queue "#{uid}--#{info.routing_key}",
27
+ :auto_delete => true
28
+ # messages to the client would be duplicated here.
29
+ client_q.bind("#{server_name}--", :routing_key => info.routing_key)
30
+ client_q.subscribe { |info,payload|
31
+ payload = ::Marshal.load(payload)
32
+ obj = prepare_callback(@callback,info,payload)
33
+ obj.client(payload[:method],payload[:data])
34
+ }
35
+ end
36
+ }
37
+ end
38
+ end
@@ -0,0 +1,180 @@
1
+ # A RPC client is a transient entity that dies
2
+ # with the process that created it. Its purpose
3
+ # is only to provide a synchronized interface to
4
+ # the asynchronous services.
5
+ require 'thread'
6
+ require 'monitor'
7
+ class ASS::RPC
8
+ # stolen from nanite
9
+ def self.random_id
10
+ values = [
11
+ rand(0x0010000),
12
+ rand(0x0010000),
13
+ rand(0x0010000),
14
+ rand(0x0010000),
15
+ rand(0x0010000),
16
+ rand(0x1000000),
17
+ rand(0x1000000),
18
+ ]
19
+ "%04x%04x%04x%04x%04x%06x%06x" % values
20
+ end
21
+
22
+ class Future
23
+ # TODO set meta
24
+ attr_reader :message_id
25
+ attr_accessor :header, :data, :method, :meta
26
+ attr_accessor :timeout
27
+ def initialize(rpc,message_id)
28
+ @message_id = message_id
29
+ @rpc = rpc
30
+ @timeout = false
31
+ @done = false
32
+ end
33
+
34
+ def wait(timeout=nil,&block)
35
+ @rpc.wait(self,timeout,&block) # synchronous call that will block
36
+ end
37
+
38
+ def done!
39
+ @done = true
40
+ end
41
+
42
+ def done?
43
+ @done
44
+ end
45
+
46
+ def timeout?
47
+ @timeout
48
+ end
49
+
50
+ def inspect
51
+ "#<#{self.class} #{message_id}>"
52
+ end
53
+ end
54
+
55
+ attr_reader :name
56
+ attr_reader :buffer, :futures, :ready
57
+ def initialize(opts={})
58
+ raise "can't run rpc client in the same thread as eventmachine" if EM.reactor_thread?
59
+ self.extend(MonitorMixin)
60
+ @seq = 0
61
+ # queue is used be used to synchronize RPC
62
+ # user thread and the AMQP eventmachine thread.
63
+ @buffer = Queue.new
64
+ @ready = {} # the ready results not yet waited
65
+ @futures = {} # all futures not yet waited for.
66
+ # Creates an exclusive queue to serve the RPC client.
67
+ @rpc_id = ASS::RPC.random_id.to_s
68
+ buffer = @buffer # closure binding for reactor
69
+ exchange = ASS.mq.direct("__rpc__")
70
+ @name = "__rpc__#{@rpc_id}"
71
+ queue = ASS.mq.queue(@name,
72
+ :exclusive => true,
73
+ :auto_delete => true)
74
+ queue.bind("__rpc__",:routing_key => @rpc_id)
75
+ queue.subscribe { |header,payload|
76
+ payload = ::Marshal.load(payload)
77
+ buffer << [header,payload]
78
+ }
79
+ end
80
+
81
+ def call(server_name,method,data=nil,opts={},meta=nil)
82
+ self.synchronize do
83
+ message_id = @seq.to_s # message gotta be unique for this RPC client.
84
+ # by default route message to the exchange @name@, with routing key @name@
85
+ ASS.call(server_name,
86
+ method,
87
+ data,
88
+ # can't override these options
89
+ opts.merge(:message_id => message_id,
90
+ :reply_to => "__rpc__",
91
+ :key => @rpc_id),
92
+ meta)
93
+ @seq += 1
94
+ @futures[message_id] = Future.new(self,message_id)
95
+ end
96
+ end
97
+
98
+ # the idea is to block on a synchronized queue
99
+ # until we get the future we want.
100
+ #
101
+ # WARNING: blocks forever if the thread
102
+ # calling wait is the same as the EventMachine
103
+ # thread.
104
+ #
105
+ # It is safe (btw) to use the RPC client within
106
+ # an ASS server/actor, because the wait is in an
107
+ # EM worker thread, rather than the EM thread
108
+ # itself. The EM thread is still free to process
109
+ # the queue. CAVEAT: you could run out of EM
110
+ # worker threads.
111
+ def wait(future,timeout=nil)
112
+ return future.data if future.done? # future was waited before
113
+ # we can have more fine grained synchronization later.
114
+ ## easiest thing to do (later) is use threadsafe hash for @futures and @ready.
115
+ ### But it's actually trickier than
116
+ ### that. Before each @buffer.pop, a thread
117
+ ### has to check again if it sees the result
118
+ ### in @ready.
119
+ self.synchronize do
120
+ timer = nil
121
+ if timeout
122
+ timer = EM.add_timer(timeout) {
123
+ @buffer << [:timeout,future.message_id]
124
+ }
125
+ end
126
+ ready_future = nil
127
+ if @ready.has_key? future.message_id
128
+ @ready.delete future.message_id
129
+ ready_future = future
130
+ else
131
+ while true
132
+ header,payload = @buffer.pop # synchronize. like erlang's mailbox select.
133
+ if header == :timeout # timeout the future we are waiting for.
134
+ message_id = payload
135
+ # if we got a timeout from previous wait. throw it away.
136
+ next if future.message_id != message_id
137
+ future.timeout = true
138
+ future.done!
139
+ @futures.delete future.message_id
140
+ return yield # return the value of timeout block
141
+ end
142
+ data = payload[:data]
143
+ some_future = @futures[header.message_id]
144
+ # If we didn't find the future among the
145
+ # future, it must have timedout. Just
146
+ # throw result away and keep processing.
147
+ next unless some_future
148
+ some_future.timeout = false
149
+ some_future.header = header
150
+ some_future.data = data
151
+ some_future.method = payload[:method]
152
+ some_future.meta = payload[:meta]
153
+ if some_future == future
154
+ # The future we are waiting for
155
+ EM.cancel_timer(timer) if timer
156
+ ready_future = future
157
+ break
158
+ else
159
+ # Ready, but we are not waiting for it. Save for later.
160
+ @ready[some_future.message_id] = some_future
161
+ end
162
+ end
163
+ end
164
+ ready_future.done!
165
+ @futures.delete ready_future.message_id
166
+ return ready_future.data
167
+ end
168
+
169
+ end
170
+
171
+ def waitall
172
+ @futures.values.map { |k,v|
173
+ wait(v)
174
+ }
175
+ end
176
+
177
+ def inspect
178
+ "#<#{self.class} #{self.name}>"
179
+ end
180
+ end
@@ -0,0 +1,171 @@
1
+ class ASS::Server
2
+ attr_reader :name
3
+
4
+ def initialize(name,opts={})
5
+ @name = name
6
+ # the server is a fanout (ignores routing key)
7
+ @exchange = ASS.mq.fanout(name,opts)
8
+ end
9
+
10
+ def exchange
11
+ @exchange
12
+ end
13
+
14
+ def queue(opts={})
15
+ unless @queue
16
+ @queue ||= ASS.mq.queue(self.name,opts)
17
+ @queue.bind(self.exchange)
18
+ end
19
+ self
20
+ end
21
+
22
+ # takes options available to MQ::Queue# takes options available to MQ::Queue#subscribe
23
+ def react(_callback=nil,_opts=nil,&_block)
24
+ if _block
25
+ _opts = _callback
26
+ _callback = _block
27
+ end
28
+ _opts = {} if _opts.nil?
29
+
30
+ # second call would just swap out the callback.
31
+ @factory = ASS::CallbackFactory.new(_callback)
32
+
33
+ return(self) if @subscribed
34
+ @subscribed = true
35
+ @ack = _opts[:ack]
36
+ self.queue unless @queue
37
+
38
+ # yikes!! potential for scary bugs
39
+ @queue.subscribe(_opts) do |info,payload|
40
+ payload = ::Marshal.load(payload)
41
+ #p [info,payload]
42
+ callback_object = @factory.callback_for(self,info,payload)
43
+ proc { #|callback_object=prepare_callback(@callback,info,payload)|
44
+ operation = proc {
45
+ with_handlers do
46
+ callback_object.send(:on_call,payload[:data])
47
+ end
48
+ }
49
+ done = proc { |result|
50
+ # the client MUST exist, otherwise it's an error.
51
+ ## FIXME it's bad if the server dies b/c
52
+ ## the client isn't there. It's bad that
53
+ ## this can cause the server to fail.
54
+ ##
55
+ ## I am not sure what happens if message
56
+ ## is unroutable. I think it's just
57
+ ## silently dropped unless the mandatory
58
+ ## option is given.
59
+ case status = result[0]
60
+ when :ok
61
+ if info.reply_to
62
+ data = result[1]
63
+ # respond with cast (we don't want
64
+ # to get a response to our response,
65
+ # then respond to the response of
66
+ # this response, and so on.)
67
+ ASS.cast(info.reply_to,
68
+ payload[:method],
69
+ data, {
70
+ :routing_key => info.routing_key,
71
+ :message_id => info.message_id},
72
+ payload[:meta])
73
+ end
74
+ info.ack if @ack
75
+ when :resend
76
+ # resend the same message
77
+ ASS.call(self.name,
78
+ payload[:method],
79
+ payload[:data], {
80
+ :reply_to => info.reply_to, # this could be nil for cast
81
+ :routing_key => info.routing_key,
82
+ :message_id => info.message_id},
83
+ payload[:meta])
84
+ info.ack if @ack
85
+ when :discard
86
+ # no response back to client
87
+ info.ack if @ack
88
+ when :error
89
+ # programmatic error. don't ack
90
+ error = result[1]
91
+ if callback_object.respond_to?(:on_error)
92
+ begin
93
+ callback_object.on_error(error,payload[:data])
94
+ info.ack if @ack # successful error handling
95
+ rescue => more_error
96
+ $stderr.puts more_error
97
+ $stderr.puts more_error.backtrace
98
+ ASS.stop
99
+ end
100
+ else
101
+ # unhandled error
102
+ $stderr.puts error
103
+ $stderr.puts error.backtrace
104
+ ASS.stop
105
+ end
106
+ # don't ack.
107
+ end
108
+ }
109
+ EM.defer operation, done
110
+ }.call
111
+
112
+
113
+ end
114
+ self
115
+ end
116
+
117
+ def call(name,method,data,opts={},meta=nil)
118
+ reply_to = opts[:reply_to] || self.name
119
+ ASS.call(name,
120
+ method,
121
+ data,
122
+ opts.merge(:reply_to => reply_to),
123
+ meta)
124
+
125
+ end
126
+
127
+ def cast(name,method,data,opts={},meta=nil)
128
+ reply_to = nil # the remote server will not reply
129
+ ASS.call(name,
130
+ method,
131
+ data,
132
+ opts.merge(:reply_to => nil),
133
+ meta)
134
+ end
135
+
136
+ def inspect
137
+ "#<#{self.class} #{self.name}>"
138
+ end
139
+
140
+ private
141
+
142
+ def with_handlers
143
+ not_discarded = false
144
+ not_resent = false
145
+ not_raised = false
146
+ result = nil
147
+ error = nil
148
+ catch(:__ass_discard) do
149
+ catch(:__ass_resend) do
150
+ begin
151
+ result = yield
152
+ not_raised = true
153
+ rescue => e
154
+ error = e
155
+ end
156
+ not_resent = true
157
+ end
158
+ not_discarded = true
159
+ end
160
+
161
+ if not_discarded && not_resent && not_raised
162
+ [:ok,result]
163
+ elsif not_discarded == false
164
+ [:discard]
165
+ elsif not_resent == false
166
+ [:resend] # resend original payload
167
+ elsif not_raised == false
168
+ [:error,error]
169
+ end
170
+ end
171
+ end