marconi 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.
- 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
|
+
|