marconi 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/marconi/broadcaster.rb +63 -0
- data/lib/marconi/config.rb +33 -0
- data/lib/marconi/envelope.rb +79 -0
- data/lib/marconi/exchange.rb +179 -0
- data/lib/marconi/guid_generator.rb +16 -0
- data/lib/marconi/receiver.rb +84 -0
- data/lib/marconi/smart_xml.rb +40 -0
- data/lib/marconi.rb +89 -0
- metadata +106 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
|
4
|
+
module Marconi
|
5
|
+
module Broadcaster
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.setup
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def setup
|
14
|
+
@master_model_name = self.name.underscore
|
15
|
+
|
16
|
+
# Callbacks
|
17
|
+
after_update :publish_update
|
18
|
+
after_create :publish_create
|
19
|
+
after_destroy :publish_destroy
|
20
|
+
end
|
21
|
+
|
22
|
+
def master_model_name
|
23
|
+
@master_model_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def publish_update
|
28
|
+
publish('update')
|
29
|
+
end
|
30
|
+
|
31
|
+
def publish_create
|
32
|
+
publish('create')
|
33
|
+
end
|
34
|
+
|
35
|
+
def publish_destroy
|
36
|
+
publish('destroy')
|
37
|
+
end
|
38
|
+
|
39
|
+
def publish(operation)
|
40
|
+
|
41
|
+
# This is set in Receiver if it's included. Intent is to
|
42
|
+
# prevent sending messages in response to incoming messages and thus
|
43
|
+
# creating infinite loops.
|
44
|
+
return if self.class.respond_to?(:broadcasts_suppressed?) &&
|
45
|
+
self.class.broadcasts_suppressed?
|
46
|
+
|
47
|
+
fmt = "%28s %9s %s Published" % [guid,operation,Time.now.to_s]
|
48
|
+
logger.debug fmt
|
49
|
+
Marconi.log(fmt)
|
50
|
+
|
51
|
+
e = Envelope.new { |e| e.send(operation, self) }
|
52
|
+
topic = "#{Marconi.application_name}.#{self.class.master_model_name}.#{operation}"
|
53
|
+
exchange.publish(e.to_s, :topic => topic)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def exchange
|
59
|
+
Marconi.inbound
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
module Marconi
|
3
|
+
class Config
|
4
|
+
attr_accessor :name, :short_name, :keepalive, :bunny_params, :listeners
|
5
|
+
def initialize(config_file = nil, env = 'test')
|
6
|
+
|
7
|
+
if Object.const_defined?(:Rails)
|
8
|
+
config_file ||= "#{Rails.root}/config/queues.yml"
|
9
|
+
env = Rails.env
|
10
|
+
end
|
11
|
+
|
12
|
+
unless File.exists?(config_file)
|
13
|
+
raise "Could not find #{config_file}"
|
14
|
+
end
|
15
|
+
|
16
|
+
params = YAML.load_file(config_file)
|
17
|
+
|
18
|
+
self.name = params['name'] || raise("Wait... Who am I?")
|
19
|
+
self.short_name = params['short_name'] || raise("w8... hu m i?")
|
20
|
+
self.keepalive = !!params['keepalive']
|
21
|
+
self.listeners = params['listeners'] || []
|
22
|
+
self.bunny_params = params['bunny'] && params['bunny'][env]
|
23
|
+
|
24
|
+
if bunny_params
|
25
|
+
self.bunny_params.symbolize_keys!
|
26
|
+
else
|
27
|
+
puts "Warning: No config specified for #{env}"
|
28
|
+
self.bunny_params = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
require 'active_support/core_ext/hash/conversions'
|
4
|
+
require 'marconi/smart_xml'
|
5
|
+
|
6
|
+
module Marconi
|
7
|
+
class Envelope
|
8
|
+
|
9
|
+
attr_accessor :topic
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
def from_xml(xml)
|
14
|
+
return nil unless xml
|
15
|
+
Envelope.new(:xml => xml)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(options = {}, &block)
|
21
|
+
if xml = options[:xml]
|
22
|
+
hash = SmartXML.parse(xml)
|
23
|
+
@topic = hash[:headers][:topic]
|
24
|
+
hash[:payload].each do |hsh|
|
25
|
+
messages << { :meta => hsh[:meta], :data => HashWithIndifferentAccess.new(hsh[:data]) }
|
26
|
+
end
|
27
|
+
else
|
28
|
+
@timestamp = options[:timestamp] || Time.now
|
29
|
+
@topic = options[:topic]
|
30
|
+
block.call(self) if block_given?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def messages
|
35
|
+
@messages ||= []
|
36
|
+
end
|
37
|
+
|
38
|
+
def contents
|
39
|
+
{ :headers => headers, :payload => messages }
|
40
|
+
end
|
41
|
+
|
42
|
+
def create(model)
|
43
|
+
add_message(model, :create)
|
44
|
+
end
|
45
|
+
|
46
|
+
def update(model)
|
47
|
+
add_message(model, :update)
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy(model)
|
51
|
+
add_message(model, :destroy)
|
52
|
+
end
|
53
|
+
|
54
|
+
def override(old_guid, model)
|
55
|
+
add_message(model, :override, old_guid)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
contents.to_xml
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def headers
|
65
|
+
{ :topic => @topic }
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_message(model, operation, guid = model.guid)
|
69
|
+
meta = {
|
70
|
+
:operation => operation.to_s,
|
71
|
+
:guid => guid,
|
72
|
+
:version => model.version,
|
73
|
+
:timestamp => @timestamp
|
74
|
+
}
|
75
|
+
messages << { :meta => meta, :data => HashWithIndifferentAccess.new(model.attributes) }
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'bunny'
|
2
|
+
|
3
|
+
module Marconi
|
4
|
+
class Exchange
|
5
|
+
|
6
|
+
def initialize(exchange_name)
|
7
|
+
@exchange_name = exchange_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def exchange_name
|
11
|
+
@exchange_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
Marconi.config.name
|
16
|
+
end
|
17
|
+
|
18
|
+
def keepalive
|
19
|
+
Marconi.config.keepalive
|
20
|
+
end
|
21
|
+
|
22
|
+
def bunny_params
|
23
|
+
Marconi.config.bunny_params
|
24
|
+
end
|
25
|
+
|
26
|
+
DEFAULT_PUBLISH_OPTIONS = {
|
27
|
+
# Tells the server how to react if the message cannot be routed to a queue.
|
28
|
+
# If set to true, the server will return an unroutable message with a Return method.
|
29
|
+
# If set to false, the server silently drops the message.
|
30
|
+
:mandatory => true,
|
31
|
+
|
32
|
+
# Tells the server how to react if the message cannot be routed to a
|
33
|
+
# queue consumer immediately. If set to true, the server will return an
|
34
|
+
# undeliverable message with a Return method. If set to false, the
|
35
|
+
# server will queue the message, but with no guarantee that it will ever
|
36
|
+
# be consumed.
|
37
|
+
:immediate => false,
|
38
|
+
|
39
|
+
# Tells the server whether to persist the message. If set to true, the
|
40
|
+
# message will be persisted to disk and not lost if the server restarts.
|
41
|
+
# Setting to true incurs a performance penalty as there is an extra cost
|
42
|
+
# associated with disk access.
|
43
|
+
:persistent => true
|
44
|
+
}
|
45
|
+
|
46
|
+
# Example: Marconi.inbound.publish("Howdy!", :topic => 'deals.member.create')
|
47
|
+
def publish(msg, options = {})
|
48
|
+
topic = ensure_valid_publish_topic(options)
|
49
|
+
begin
|
50
|
+
connect
|
51
|
+
@exchange.publish(msg, DEFAULT_PUBLISH_OPTIONS.merge(:key => topic))
|
52
|
+
retmsg = @bunny.returned_message
|
53
|
+
raise "Invalid return payload: #{retmsg[:payload]}" unless retmsg[:payload] == :no_return
|
54
|
+
true
|
55
|
+
rescue Exception => e
|
56
|
+
Marconi.log(e)
|
57
|
+
if Marconi.backup_queue_class && !options[:recovering]
|
58
|
+
Marconi.backup_queue_class.create!(:exchange_name => exchange_name,
|
59
|
+
:topic => topic,
|
60
|
+
:body => msg)
|
61
|
+
end
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Example: Marconi.inbound.subscribe('foo_q', :key => 'deals.member.*') { |msg| puts msg[:payload] }
|
67
|
+
def subscribe(q_name, options = {}, &block)
|
68
|
+
q, key = get_q(q_name, options)
|
69
|
+
q.subscribe(options, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Example: Marconi.inbound.pop('foo_q', :key => 'deals.member.*')
|
73
|
+
def pop(q_name, options = {})
|
74
|
+
q, key = get_q(q_name, options)
|
75
|
+
msg = q.pop[:payload]
|
76
|
+
msg == :queue_empty ? nil : msg
|
77
|
+
end
|
78
|
+
|
79
|
+
# Example: Marconi.inbound.purge_q('foo_q')
|
80
|
+
# Use judiciously - this tosses all messages in the Q!
|
81
|
+
def purge_q(q_name)
|
82
|
+
connect
|
83
|
+
unless q_name.blank?
|
84
|
+
if o = exists?(:queue, q_name)
|
85
|
+
o.purge
|
86
|
+
end
|
87
|
+
end
|
88
|
+
rescue Bunny::ForcedChannelCloseError
|
89
|
+
connect(true) # Connection is fucked after this error, so it must be refreshed
|
90
|
+
raise
|
91
|
+
end
|
92
|
+
|
93
|
+
# Example: Marconi.inbound.nuke_q('foo_q')
|
94
|
+
# Use judiciously - this tosses all messages in the Q *and* nukes it
|
95
|
+
def nuke_q(q_name)
|
96
|
+
generic_nuke(:queue, q_name)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def connected?
|
102
|
+
@bunny && @bunny.status == :connected && @exchange
|
103
|
+
end
|
104
|
+
|
105
|
+
def connect(reconnect = false)
|
106
|
+
reconnect = true unless keepalive
|
107
|
+
@bunny.stop if reconnect && connected? rescue nil
|
108
|
+
if reconnect || !connected?
|
109
|
+
@bunny = Bunny.new(bunny_params)
|
110
|
+
result = @bunny.start
|
111
|
+
if result == :connected
|
112
|
+
@exchange =
|
113
|
+
@bunny.exchange(
|
114
|
+
exchange_name,
|
115
|
+
|
116
|
+
# Topic queues are broadcast queues that allow wildcard subscriptions
|
117
|
+
:type => :topic,
|
118
|
+
|
119
|
+
# Durable exchanges remain active when a server restarts.
|
120
|
+
# Non-durable exchanges (transient exchanges) are purged if/when
|
121
|
+
# a server restarts.
|
122
|
+
:durable => true
|
123
|
+
)
|
124
|
+
result
|
125
|
+
else
|
126
|
+
raise "Unable to connect to RabbitMQ: #{result}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
rescue Bunny::ProtocolError # raised by @bunny.start
|
130
|
+
@bunny = nil
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
|
134
|
+
def ensure_valid_publish_topic(options)
|
135
|
+
raise "You must specify a topic to publish to!" unless topic = options[:topic]
|
136
|
+
raise "You may not publish to a topic name with a wildcard" if topic =~ /[*#]/
|
137
|
+
topic
|
138
|
+
end
|
139
|
+
|
140
|
+
DEFAULT_Q_OPTIONS = {
|
141
|
+
:durable => true,
|
142
|
+
:exclusive => false,
|
143
|
+
:auto_delete => false
|
144
|
+
}
|
145
|
+
|
146
|
+
def get_q(q_name, options)
|
147
|
+
connect
|
148
|
+
raise "You must specify a topic to subscribe to!" unless key = options[:key]
|
149
|
+
raise "You must specify a Queue Name" if q_name.blank?
|
150
|
+
q = @bunny.queue(q_name, DEFAULT_Q_OPTIONS)
|
151
|
+
q.bind(@exchange, :key => key)
|
152
|
+
[q, key]
|
153
|
+
rescue Bunny::ForcedChannelCloseError, Bunny::ProtocolError
|
154
|
+
connect(true) # Connection is fucked after this error, so it must be refreshed
|
155
|
+
raise
|
156
|
+
end
|
157
|
+
|
158
|
+
def exists?(type, name)
|
159
|
+
@bunny.send(type, name, :passive => true)
|
160
|
+
# Annoyingly, this error is thrown if the q or exchange can't be found ... yet :passive isn't
|
161
|
+
# supposed to err!! Bah.
|
162
|
+
rescue Bunny::ForcedChannelCloseError
|
163
|
+
connect(true) # Connection is fucked after this error, so it must be refreshed
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
def generic_nuke(type, name)
|
168
|
+
connect
|
169
|
+
unless name.blank?
|
170
|
+
if o = exists?(type, name)
|
171
|
+
o.delete
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue Bunny::ForcedChannelCloseError
|
175
|
+
connect(true) # Connection is fucked after this error, so it must be refreshed
|
176
|
+
raise
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
require 'uuidtools'
|
3
|
+
|
4
|
+
module Marconi
|
5
|
+
module GUIDGenerator
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def next_guid
|
10
|
+
shortname = Marconi.short_application_name[0,2]
|
11
|
+
uid = UUIDTools::UUID.random_create.to_i.to_s(36).rjust(25,'0')
|
12
|
+
"#{shortname}-#{uid}"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
|
4
|
+
module Marconi
|
5
|
+
module Receiver
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.setup
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@master_model_name = self.name.underscore
|
16
|
+
end
|
17
|
+
|
18
|
+
def master_model_name
|
19
|
+
@master_model_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def register(operation, &block)
|
23
|
+
self.handlers[operation] ||= []
|
24
|
+
self.handlers[operation] << block
|
25
|
+
end
|
26
|
+
|
27
|
+
def handlers
|
28
|
+
@handlers ||= HashWithIndifferentAccess.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def log_message_action(amqp_mesg, info_mesg)
|
32
|
+
fmt = "%28s %9s %s %s" %
|
33
|
+
[amqp_mesg[:meta][:guid],
|
34
|
+
amqp_mesg[:meta][:operation],
|
35
|
+
Time.now().to_s, info_mesg]
|
36
|
+
logger.debug(fmt)
|
37
|
+
Marconi.log(fmt)
|
38
|
+
end
|
39
|
+
|
40
|
+
def listen(max = nil)
|
41
|
+
q_name = "#{Marconi.application_name}.#{self.master_model_name}"
|
42
|
+
topic = "#.#{self.master_model_name}.#"
|
43
|
+
exchange.subscribe(q_name, :key => topic, :message_max => max) do |amqp_msg|
|
44
|
+
e = Envelope.from_xml(amqp_msg[:payload])
|
45
|
+
suppress_broadcasts do
|
46
|
+
e.messages.each do |message|
|
47
|
+
log_message_action(message, "Receiving")
|
48
|
+
operation = message[:meta][:operation]
|
49
|
+
next unless self.handlers[operation]
|
50
|
+
self.handlers[operation].each do |h|
|
51
|
+
h.call(message)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def suppress_broadcasts
|
59
|
+
@suppress_broadcasts = true
|
60
|
+
yield
|
61
|
+
ensure
|
62
|
+
@suppress_broadcasts = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def broadcasts_suppressed?
|
66
|
+
@suppress_broadcasts
|
67
|
+
end
|
68
|
+
|
69
|
+
def purge_handlers
|
70
|
+
@handlers = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def exchange
|
74
|
+
Marconi.outbound
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def log_message_action(amqp_mesg, info_mesg)
|
79
|
+
self.class.log_message_action(amqp_mesg, info_mesg)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
|
4
|
+
module Marconi
|
5
|
+
module SmartXML
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def parse(xml)
|
9
|
+
return nil unless xml
|
10
|
+
hash = Hash.from_xml(xml)
|
11
|
+
return nil unless hash
|
12
|
+
|
13
|
+
# If the top-level object is a hash, rails's deserializer puts it
|
14
|
+
# under the key "hash". If it's an array, it uses "object". Simple
|
15
|
+
# values like numbers and strings can't be XML docs all by themselves
|
16
|
+
# so we don't need to consider any other cases.
|
17
|
+
|
18
|
+
indifferentize(hash['hash'] || hash['objects'])
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Convert all hashes to indifferent hashes, because otherwise we get
|
24
|
+
# subtle bugs.
|
25
|
+
def indifferentize(hash)
|
26
|
+
case hash
|
27
|
+
when Hash
|
28
|
+
new_hash = HashWithIndifferentAccess.new
|
29
|
+
hash.each do |key, value|
|
30
|
+
new_hash[key] = indifferentize(value)
|
31
|
+
end
|
32
|
+
new_hash
|
33
|
+
else
|
34
|
+
hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/marconi.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
|
2
|
+
module Marconi
|
3
|
+
|
4
|
+
extend self
|
5
|
+
|
6
|
+
attr_accessor :backup_queue_class_name, :logger
|
7
|
+
|
8
|
+
def init(config_file, env)
|
9
|
+
@config = Config.new(config_file, env)
|
10
|
+
end
|
11
|
+
|
12
|
+
# this uses eval to get the class from its name because
|
13
|
+
# otherwise rails class reloading in dev will screw everything up
|
14
|
+
def backup_queue_class
|
15
|
+
eval(backup_queue_class_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def inbound
|
19
|
+
@inbound ||= exchange('marconi.events.inbound')
|
20
|
+
end
|
21
|
+
|
22
|
+
def outbound
|
23
|
+
@outbound ||= exchange('marconi.events.outbound')
|
24
|
+
end
|
25
|
+
|
26
|
+
def error
|
27
|
+
@error ||= exchange('marconi.events.error')
|
28
|
+
end
|
29
|
+
|
30
|
+
def config
|
31
|
+
@config ||= Config.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def application_name
|
35
|
+
config.name
|
36
|
+
end
|
37
|
+
|
38
|
+
def short_application_name
|
39
|
+
config.short_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def listen
|
43
|
+
config.listeners.each do |class_name|
|
44
|
+
fork do
|
45
|
+
|
46
|
+
# this is to ditch any exit handlers. Minitest in ruby 1.9
|
47
|
+
# for some reason sets itself to run an empty suite at exit.
|
48
|
+
at_exit { exit! }
|
49
|
+
|
50
|
+
# Change the process name as it appears in ps/top so the proc
|
51
|
+
# is easy to identify
|
52
|
+
$0 = "ruby #{$0} Marconi.listen [#{class_name}]"
|
53
|
+
|
54
|
+
class_name.constantize.listen
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Process.waitall
|
59
|
+
rescue Interrupt
|
60
|
+
Process.kill("-INT",0) # interrupt the whole process group
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_recovery_loop
|
64
|
+
if backup_queue_class
|
65
|
+
backup_queue_class.find_each do |bc|
|
66
|
+
if exchange(bc.exchange_name).publish(bc.body, :topic => bc.topic, :recovering => true)
|
67
|
+
bc.destroy
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def exchange(name)
|
74
|
+
@registry ||= {}
|
75
|
+
@registry[name] ||= Exchange.new(name)
|
76
|
+
end
|
77
|
+
|
78
|
+
def log(message)
|
79
|
+
logger.info(message) if logger
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
require 'marconi/guid_generator'
|
85
|
+
require 'marconi/exchange'
|
86
|
+
require 'marconi/config'
|
87
|
+
require 'marconi/envelope'
|
88
|
+
require 'marconi/receiver'
|
89
|
+
require 'marconi/broadcaster'
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: marconi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ken Miller
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-02 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: bunny
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - "="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.6.0
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: mocha
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: shoulda
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
description: Implements asychronous, distributed state broadcasting for ActiveModel-like objects.
|
61
|
+
email: ken.miller@gmail.com
|
62
|
+
executables: []
|
63
|
+
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files: []
|
67
|
+
|
68
|
+
files:
|
69
|
+
- lib/marconi/broadcaster.rb
|
70
|
+
- lib/marconi/config.rb
|
71
|
+
- lib/marconi/envelope.rb
|
72
|
+
- lib/marconi/exchange.rb
|
73
|
+
- lib/marconi/guid_generator.rb
|
74
|
+
- lib/marconi/receiver.rb
|
75
|
+
- lib/marconi/smart_xml.rb
|
76
|
+
- lib/marconi.rb
|
77
|
+
has_rdoc: true
|
78
|
+
homepage: https://github.com/kemiller/marconi
|
79
|
+
licenses: []
|
80
|
+
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: "0"
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: "0"
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project: nowarning
|
101
|
+
rubygems_version: 1.6.2
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Message-based distributed state updates for Active Record
|
105
|
+
test_files: []
|
106
|
+
|