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 +153 -0
- data/lib/vincent/base.rb +198 -0
- data/lib/vincent/server.rb +21 -0
- data/lib/vincent.rb +50 -0
- metadata +57 -0
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
|
+
|
data/lib/vincent/base.rb
ADDED
@@ -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
|
+
|