adamhenry-vincent 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,153 @@
1
+
2
+ ======= Vincent =======
3
+
4
+ This a PROOF for a micro-framework for handling AMQP traffic between multiple
5
+ daemons. It was written with Fibers on Ruby 1.9 to allow for synchronous action
6
+ inside event machine and MAY work in 1.8 if patched with fibers but is untested.
7
+ The is PRE-ALPHA and it needs a full design and implementation phase as well as
8
+ full tests and examples. If anyone wants to contribute I am totally open.
9
+
10
+ === Background:
11
+
12
+ Vincent gets you many of the AMQP benefits out of the box that most would miss
13
+ if they do not know the protocol very well. For instance it hoes a clean
14
+ shutdown on a SIGTERM and transparently handles message acknowledgements for
15
+ you so that you never lose an AMQP message. Like-wise it includes a TTL option
16
+ on messages that lets you kill off messages that might have stuck around beyond
17
+ their usefulness.
18
+
19
+ === Model:
20
+
21
+ Http App Server (thin/mongrel) are not the place to be processing long running
22
+ work (100ms max). These app servers should be doing only simple database
23
+ operations and displaying cached data. When work needs to be done an
24
+ asynchronous message should be sent to worker processes. Vincent is broken
25
+ into several libraries that allow you to enforce that your app servers are
26
+ only sending data, while the workers can both send an receive.
27
+
28
+ Vincent needs to be run inside an Event Machine reactor. Thin uses EventMachine.
29
+ If you are using an app server that does not use EM, you will need to start the
30
+ reactor manually.
31
+
32
+ === Client Example:
33
+
34
+ I use the 'cast' and the 'call' idioms from erlang to specify if you are
35
+ sending a message (cast) or sending a message and awaiting a reply (call). It
36
+ is important to not confuse these two.
37
+
38
+ require 'vincent'
39
+
40
+ Vincent::Client.cast("file.encrypt", { :key_id => 1234, :file => "/gfs1/file.dat", :enc => 'blowfish' })
41
+
42
+ ## Vincent.file_encrypt( { ... } ) does the same thing with method_missing?
43
+
44
+ Cast takes a hash and will encode the that via JSON before publishing the AMQP
45
+ message and will decode the hash on the other side.
46
+
47
+ === In line Server Example:
48
+
49
+ An in line server can be setup as such:
50
+
51
+ require 'vincent/server'
52
+
53
+ Vincent::Server.start do |vincent|
54
+ vincent.cast("server.up", { :host => ENV['HOSTNAME'], :pid => Process.pid } )
55
+
56
+ vincent.listen4("file.encrypt", :queue => 'file.encrypt.queue') do |b|
57
+ file = get_file(b['file'])
58
+ key = get_key(b['enc'],b['key_id'])
59
+ file.lock(key)
60
+ b.cast("file.encrypted", { :file => file.name })
61
+ end
62
+ end
63
+
64
+ The code above sends a "server.up" message when it comes online and encrypts
65
+ files when told to. All such processes listen on the same named queue so they
66
+ will do the work round-robin. After finishing the file it sends a
67
+ file.encrypted message.
68
+
69
+ === Call:
70
+
71
+ Now imagine the files are stored on a file server and an AMQP message needs to
72
+ be sent to fetch it. This is no easy task since AMQP is asynchronous and in
73
+ eventmachine. Fibers come to the rescue.
74
+
75
+ Vincent::Server.start do |vincent|
76
+ vincent.listen4("file.encrypt", :queue => 'file.encrypt.queue') do |b|
77
+ file = b.call("file.get", { :file => b['file'] })
78
+ key = get_key(b['enc'],b['key_id'])
79
+ file.lock(key)
80
+ b.cast("file.encrypted", { :file => file.name })
81
+ end
82
+ end
83
+
84
+ Here is the file server code - Vincent reads the file and sends its bytes
85
+ back over the wire
86
+
87
+ Vincent::Server.start do |vincent|
88
+ vincent.listen4("file.get") do |b|
89
+ File.read(b['file'])
90
+ end
91
+ end
92
+
93
+ Note: if the call block were to throw an exception that exception would
94
+ propagate up to the callers block.
95
+
96
+ This process is good, but it breaks down as your app size grows. Code is hard
97
+ to reuse and spec, and the size of the server block will grow out of control.
98
+ That's where Vincent::Base comes in.
99
+
100
+ === The Vincent Object Model:
101
+
102
+ require 'vincent/base'
103
+
104
+ class FilePut < Vincent::Base
105
+ bind_to "file.put", :route_to => :one, :active => :disk_not_full
106
+
107
+ ## only one worker will receive the message
108
+
109
+ ## this class accepts requests to store files locally if and
110
+ ## only if the disk is not full
111
+
112
+ def disk_not_full
113
+ not disk_full?
114
+ end
115
+
116
+ def handle_cast
117
+ File.write(params['file_name'],"w") do |f|
118
+ f.write(params['file_bytes'])
119
+ end
120
+ end
121
+ end
122
+
123
+ class FileGet < Vincent::Base
124
+ bind_to "file.get", :route_to => :one
125
+
126
+ ## this decrypts the file but rejects requests for a file that does
127
+ ## not exist, since it probably is on a different server
128
+
129
+ def handle_call
130
+ reject unless File.exists?(params['file'])
131
+ File.read(params['file'])
132
+ end
133
+ end
134
+
135
+
136
+ === What's Next?
137
+
138
+ There is a lot of work to be done:
139
+
140
+ * finalize the interface
141
+ * get it working on 1.8.7+fibers
142
+ * get full test coverage
143
+ * test the fiber code more - make sure it works in all cases
144
+ * make sure it's wire compatible with current AMQP network
145
+ * write examples
146
+ * write docs
147
+ * do a proper gem release
148
+
149
+ === Why call it Vincent?
150
+
151
+ He was that scrappy little robot from the movie The Black Hole. =)
152
+
153
+
@@ -0,0 +1,198 @@
1
+
2
+ require 'vincent'
3
+ require 'fiber'
4
+
5
+ ### TODO
6
+
7
+ ### symbolize keys
8
+ ### confirm works with droids
9
+ ### headers
10
+ ### cache
11
+ ### autodelete queues?
12
+
13
+ module Vincent
14
+
15
+ class RejectMessage < RuntimeError ; end
16
+ class MissingCallHandler < RuntimeError ; end
17
+ class MissingCastHandler < RuntimeError ; end
18
+
19
+ module Core
20
+ def call(key, args = {})
21
+ args['reply_to'] = "reply_to.#{rand 99_999_999}"
22
+ reply = receive(args['reply_to']) do
23
+ cast(key, args)
24
+ end
25
+ raise unpack(reply["exception"]) if reply["exception"]
26
+ return unpack(reply["results"]) if reply["results"]
27
+ return reply
28
+ end
29
+
30
+ def receive(q)
31
+ f = Fiber.current
32
+ subscribe(q) do |result|
33
+ ## maybe we can destroy the queue here - unsubscribe - autodelete
34
+ f.resume(result)
35
+ end
36
+ yield
37
+ Fiber.yield
38
+ end
39
+
40
+ def listen4(key, options = {}, &block)
41
+ q = options[:queue]
42
+ q ||= "q.#{ENV['HOSTNAME']}"
43
+
44
+ bind(q, key)
45
+ subscribe(q) do |args|
46
+ block.call(Vincent::Base.new(args, args))
47
+ end
48
+ end
49
+
50
+ def bind(q, key)
51
+ MQ.queue(q).bind(exchange, :key => key)
52
+ end
53
+
54
+ def unsubscribe(q)
55
+ MQ.queue(q).unsubscribe
56
+ end
57
+
58
+ def subscribe(q, &block)
59
+ MQ.queue(q).subscribe(:ack => true) do |info, data|
60
+ next if AMQP.closing?
61
+
62
+ args = decode(data)
63
+
64
+ if args['kill_by'] and args['kill_by'] < Time.now.to_i
65
+ info.ack
66
+ return
67
+ end
68
+
69
+ begin
70
+ results = block.call(args)
71
+ rescue RejectMessage
72
+ info.reject
73
+ next
74
+ rescue Object => e
75
+ puts "got exception #{e} - packing it for return"
76
+ ## just display the exception if there's not reply_to
77
+ results = { :exception => pack(e) }
78
+ end
79
+
80
+ results = { :results => pack(results) } unless results.is_a?(Hash)
81
+ MQ.queue(args['reply_to']).publish(encode(results)) if args['reply_to']
82
+ info.ack
83
+ end
84
+ nil
85
+ end
86
+
87
+ def pack(obj)
88
+ [Marshal.dump(obj)].pack("m")
89
+ end
90
+
91
+ def unpack(s)
92
+ Marshal.load(s.unpack("m")[0])
93
+ end
94
+ end
95
+
96
+ class Binding
97
+ attr_accessor :key, :klass, :q, :subscribed, :active
98
+ def initialize(key, klass, q, active)
99
+ @key = key
100
+ @klass = klass
101
+ @q = q
102
+ @subscribed = false
103
+ @active = active
104
+ end
105
+
106
+ def needs_to_subscribe?
107
+ handler = klass.new({}, {})
108
+ subscribed == false and handler.send(active) == true
109
+ end
110
+
111
+ def needs_to_unsubscribe?
112
+ handler = klass.new({}, {})
113
+ subscribed == true and handler.send(active) == false
114
+ end
115
+ end
116
+
117
+ module Routes
118
+ def bind_to(key, options = {})
119
+ key = key.to_s
120
+ q = "q.#{key}" if options[:route_to] == :one
121
+ q = "q.#{ENV['HOSTNAME']}" if options[:route_to] == :host
122
+ q = "q.#{rand 9_999_999}" if options[:route_to] == :all
123
+ raise "route_to => :one, :all or :host" unless q
124
+
125
+ active = options[:active]
126
+ active ||= :active
127
+
128
+ Routes.bindings[key] = Binding.new(key, self, q, active)
129
+ end
130
+
131
+ def self.check
132
+ bindings.each do |key, b|
133
+ if b.needs_to_subscribe?
134
+ Server.subscribe(b.q) do |params|
135
+ handler = b.klass.new(params, params)
136
+ handler.handle
137
+ end
138
+ b.subscribed = true
139
+ elsif b.needs_to_unsubscribe?
140
+ Server.unsubscribe(b.q)
141
+ b.subscribed = true
142
+ end
143
+ end
144
+ end
145
+
146
+ def self.bind
147
+ bindings.each do |key, b|
148
+ Server.bind(b.q, b.key)
149
+ end
150
+ check
151
+ end
152
+
153
+ def self.bindings
154
+ @bindings ||= {}
155
+ end
156
+ end
157
+
158
+ class Base < Vincent::Client
159
+ extend Routes
160
+
161
+ attr_accessor :headers, :params
162
+
163
+ def initialize(headers, params)
164
+ @headers = headers
165
+ @params = params
166
+ end
167
+
168
+ def [](index)
169
+ params[index]
170
+ end
171
+
172
+ def active
173
+ true
174
+ end
175
+
176
+ def reject
177
+ raise RejectMessage
178
+ end
179
+
180
+ def handle
181
+ if params['reply_to']
182
+ handle_call
183
+ else
184
+ handle_cast
185
+ end
186
+ end
187
+
188
+ def handle_call
189
+ raise MissingCallHandler
190
+ end
191
+
192
+ def handle_cast
193
+ raise MissingCastHandler
194
+ end
195
+
196
+ end
197
+ end
198
+
@@ -0,0 +1,21 @@
1
+ require 'vincent/base'
2
+
3
+ module Vincent
4
+ class Server
5
+ extend Core
6
+
7
+ def self.start(&block)
8
+ EM.run {
9
+ Signal.trap('INT') { AMQP.stop{ EM.stop } }
10
+ Signal.trap('TERM'){ AMQP.stop{ EM.stop } }
11
+
12
+ Fiber.new {
13
+ Vincent::Routes.bind
14
+ EM.add_periodic_timer(60) { Vincent::Routes.check }
15
+ block.call(Vincent::Server) if block
16
+ }.resume
17
+ }
18
+ end
19
+ end
20
+ end
21
+
data/lib/vincent.rb ADDED
@@ -0,0 +1,50 @@
1
+
2
+ require 'mq'
3
+ require 'uri'
4
+ require 'json'
5
+
6
+ module Vincent
7
+
8
+ def self.method_missing(method,data = {})
9
+ key = method.to_s.gsub(/_/,".")
10
+ Vincent::Client.cast(key,data)
11
+ end
12
+
13
+ module Core
14
+ def exchange
15
+ @exchange ||= MQ.topic
16
+ end
17
+
18
+ def cast(key,args = {})
19
+ args['kill_by'] = Time.now.to_i + args['ttl'] if args['ttl'].to_i > 0
20
+ exchange.publish(encode(args), :routing_key => key)
21
+ nil
22
+ end
23
+
24
+ def encode(data)
25
+ data.to_json
26
+ end
27
+
28
+ def decode(data)
29
+ begin
30
+ JSON.parse(data)
31
+ rescue
32
+ # log error
33
+ {}
34
+ end
35
+ end
36
+ end
37
+
38
+ class Client
39
+ extend Core
40
+ end
41
+ end
42
+
43
+ config = URI.parse(ENV['AMQP_URI'] || 'amqp://guest:guest@localhost/')
44
+ AMQP.settings.merge!(
45
+ :host => config.host,
46
+ :user => config.user,
47
+ :pass => config.password,
48
+ :vhost => config.path
49
+ )
50
+
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adamhenry-vincent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Adam Henry
8
+ - Orion Henry
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-03-20 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description: An AMQP microp-framwork with best-practaces
18
+ email: a.david.henry@gmail.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - lib/vincent.rb
27
+ - lib/vincent/server.rb
28
+ - README
29
+ - lib/vincent/base.rb
30
+ has_rdoc: false
31
+ homepage: http://www.vincent.com/
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: An AMQP micro-framwork
56
+ test_files: []
57
+