adamhenry-vincent 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/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
+