symphony 0.3.0.pre20140327204419
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.simplecov +9 -0
- data/ChangeLog +508 -0
- data/History.rdoc +15 -0
- data/Manifest.txt +30 -0
- data/README.rdoc +89 -0
- data/Rakefile +77 -0
- data/TODO.md +5 -0
- data/USAGE.rdoc +381 -0
- data/bin/symphony +8 -0
- data/bin/symphony-task +10 -0
- data/etc/config.yml.example +9 -0
- data/lib/symphony/daemon.rb +372 -0
- data/lib/symphony/metrics.rb +84 -0
- data/lib/symphony/mixins.rb +75 -0
- data/lib/symphony/queue.rb +313 -0
- data/lib/symphony/routing.rb +98 -0
- data/lib/symphony/signal_handling.rb +107 -0
- data/lib/symphony/task.rb +407 -0
- data/lib/symphony/tasks/auditor.rb +51 -0
- data/lib/symphony/tasks/failure_logger.rb +106 -0
- data/lib/symphony/tasks/pinger.rb +64 -0
- data/lib/symphony/tasks/simulator.rb +57 -0
- data/lib/symphony/tasks/ssh.rb +126 -0
- data/lib/symphony/tasks/sshscript.rb +168 -0
- data/lib/symphony.rb +56 -0
- data/spec/helpers.rb +36 -0
- data/spec/symphony/mixins_spec.rb +78 -0
- data/spec/symphony/queue_spec.rb +368 -0
- data/spec/symphony/task_spec.rb +147 -0
- data/spec/symphony_spec.rb +14 -0
- data.tar.gz.sig +0 -0
- metadata +332 -0
- metadata.gz.sig +0 -0
data/Rakefile
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'hoe'
|
5
|
+
rescue LoadError
|
6
|
+
abort "This Rakefile requires hoe (gem install hoe)"
|
7
|
+
end
|
8
|
+
|
9
|
+
GEMSPEC = 'symphony.gemspec'
|
10
|
+
|
11
|
+
|
12
|
+
Hoe.plugin :mercurial
|
13
|
+
Hoe.plugin :signing
|
14
|
+
Hoe.plugin :deveiate
|
15
|
+
Hoe.plugin :bundler
|
16
|
+
|
17
|
+
Hoe.plugins.delete :rubyforge
|
18
|
+
Hoe.plugins.delete :gemcutter
|
19
|
+
|
20
|
+
hoespec = Hoe.spec 'symphony' do |spec|
|
21
|
+
spec.readme_file = 'README.rdoc'
|
22
|
+
spec.history_file = 'History.rdoc'
|
23
|
+
spec.extra_rdoc_files = FileList[ '*.rdoc' ]
|
24
|
+
spec.spec_extras[:rdoc_options] = ['-f', 'fivefish', '-t', 'Symphony']
|
25
|
+
spec.spec_extras[:required_rubygems_version] = '>= 2.0.3'
|
26
|
+
spec.license 'BSD'
|
27
|
+
|
28
|
+
spec.developer 'Michael Granger', 'ged@FaerieMUD.org'
|
29
|
+
spec.developer 'Mahlon E. Smith', 'mahlon@martini.nu'
|
30
|
+
|
31
|
+
spec.dependency 'pluggability', '~> 0.4'
|
32
|
+
spec.dependency 'bunny', '~> 1.1'
|
33
|
+
spec.dependency 'sysexits', '~> 1.1'
|
34
|
+
spec.dependency 'yajl-ruby', '~> 1.2'
|
35
|
+
spec.dependency 'msgpack', '~> 0.5'
|
36
|
+
spec.dependency 'metriks', '~> 0.9'
|
37
|
+
spec.dependency 'rusage', '~> 0.2'
|
38
|
+
|
39
|
+
spec.dependency 'rspec', '~> 3.0', :developer
|
40
|
+
spec.dependency 'net-ssh', '~> 2.8', :developer
|
41
|
+
spec.dependency 'net-sftp', '~> 2.1', :developer
|
42
|
+
spec.dependency 'simplecov', '~> 0.8', :developer
|
43
|
+
|
44
|
+
spec.require_ruby_version( '>=2.0.0' )
|
45
|
+
spec.hg_sign_tags = true if spec.respond_to?( :hg_sign_tags= )
|
46
|
+
|
47
|
+
self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
|
48
|
+
end
|
49
|
+
|
50
|
+
ENV['VERSION'] ||= hoespec.spec.version.to_s
|
51
|
+
|
52
|
+
# Run the tests before checking in
|
53
|
+
task 'hg:precheckin' => [ :check_history, :check_manifest, :spec ]
|
54
|
+
|
55
|
+
# Rebuild the ChangeLog immediately before release
|
56
|
+
task :prerelease => 'ChangeLog'
|
57
|
+
CLOBBER.include( 'ChangeLog' )
|
58
|
+
|
59
|
+
desc "Build a coverage report"
|
60
|
+
task :coverage do
|
61
|
+
ENV["COVERAGE"] = 'yes'
|
62
|
+
Rake::Task[:spec].invoke
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
task :gemspec => GEMSPEC
|
67
|
+
file GEMSPEC => __FILE__ do |task|
|
68
|
+
spec = $hoespec.spec
|
69
|
+
spec.files.delete( '.gemtest' )
|
70
|
+
spec.version = "#{spec.version}.pre#{Time.now.strftime("%Y%m%d%H%M%S")}"
|
71
|
+
File.open( task.name, 'w' ) do |fh|
|
72
|
+
fh.write( spec.to_ruby )
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
task :default => :gemspec
|
77
|
+
|
data/TODO.md
ADDED
data/USAGE.rdoc
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
|
2
|
+
= Initial Setup
|
3
|
+
|
4
|
+
Symphony uses the RabbitMQ[http://www.rabbitmq.com/] server
|
5
|
+
for speed, redundancy, and high availability. Installing
|
6
|
+
RabbitMQ is outside the scope of this document, please see their
|
7
|
+
installation[http://www.rabbitmq.com/download.html] instructions.
|
8
|
+
|
9
|
+
Once installed and running, lets move on!
|
10
|
+
|
11
|
+
== Nomenclature
|
12
|
+
|
13
|
+
Here's a quick review on RabbitMQ vocabulary. It's helpful to have some
|
14
|
+
fundamental understanding of AMQP concepts when using Symphony, as
|
15
|
+
a lot of advanced messaging options are available only via server-side
|
16
|
+
configuration.
|
17
|
+
|
18
|
+
virtual host::
|
19
|
+
A logical container for exchanges and queues. You can have as many of
|
20
|
+
these as you like for a RabbitMQ cluster, they are used to partition
|
21
|
+
objects for security purposes.
|
22
|
+
|
23
|
+
exchange::
|
24
|
+
An exchange receives incoming messages, and routes them to queues via
|
25
|
+
rules defined in bindings.
|
26
|
+
|
27
|
+
queue::
|
28
|
+
Consumers receive messages from a queue, if their binding rules match.
|
29
|
+
|
30
|
+
binding::
|
31
|
+
A rule that links/routes messages from an exchange to a queue.
|
32
|
+
|
33
|
+
routing key::
|
34
|
+
A period separated hierarchal string that specifies a category for
|
35
|
+
a message. This is matched against existing bindings for incoming
|
36
|
+
messages.
|
37
|
+
|
38
|
+
dead-letter-exchange::
|
39
|
+
An exchange that receives messages that fail for any reason. Configuring
|
40
|
+
a dead letter exchange is critical for robust error reporting.
|
41
|
+
|
42
|
+
|
43
|
+
= Starting Out
|
44
|
+
|
45
|
+
Symphony uses
|
46
|
+
Configurability[https://rubygems.org/gems/configurability] to determine
|
47
|
+
connection criteria to the RabbitMQ server, which is stored in a
|
48
|
+
YAML[http://www.yaml.org/] file.
|
49
|
+
|
50
|
+
An example configuration file for Symphony looks as such, if you
|
51
|
+
were connecting to a virtual host called '/test', wanted to log all
|
52
|
+
messages to STDERR at a level of 'debug', and had an exchange called
|
53
|
+
'symphony' (the default):
|
54
|
+
|
55
|
+
amqp:
|
56
|
+
broker_uri: amqp://USERNAME:PASSWORD@example.com:5672/%2Ftest
|
57
|
+
exchange: symphony
|
58
|
+
|
59
|
+
logging:
|
60
|
+
symphony: debug STDERR (color)
|
61
|
+
|
62
|
+
|
63
|
+
Symphony won't create the exchange for you, it is expected to
|
64
|
+
already exist under the specified virtual host. (There are a lot of
|
65
|
+
server-side exchange configuration options, and we don't make any
|
66
|
+
assumptions on your behalf.) The only requirement is that it is a
|
67
|
+
'topic' exchange. If it is of a different kind ('fanout', 'direct',
|
68
|
+
etc, it won't work!) It's also recommended to mark it 'durable', so you
|
69
|
+
won't need to re-create it after any RabbitMQ service restarts.
|
70
|
+
|
71
|
+
|
72
|
+
= Building a Task
|
73
|
+
|
74
|
+
Tasks inherit from the Symphony::Task class. You can start an
|
75
|
+
individual task directly via the 'symphony-task' binary, or as a
|
76
|
+
pool of managed processes via the 'symphony' daemon, or by simply
|
77
|
+
calling #run on the class itself.
|
78
|
+
|
79
|
+
== The Simplest Task
|
80
|
+
|
81
|
+
#!/usr/bin/env ruby
|
82
|
+
|
83
|
+
require 'symphony'
|
84
|
+
|
85
|
+
class Test < Symphony::Task
|
86
|
+
|
87
|
+
# Process all events
|
88
|
+
subscribe_to '#'
|
89
|
+
|
90
|
+
def work( payload, metadata )
|
91
|
+
puts "I received an event: %p with a payload of %p" % [
|
92
|
+
metadata[ :delivery_info ][ :routing_key ],
|
93
|
+
payload
|
94
|
+
]
|
95
|
+
return true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
Symphony.load_config( 'etc/config.yml' )
|
100
|
+
Test.run
|
101
|
+
|
102
|
+
|
103
|
+
The only requirement is the 'subscribe_to' clause, which indicates what
|
104
|
+
topics the queue will receive from the exchange. Subscribing to '#'
|
105
|
+
means this task will receive anything and everything that is published.
|
106
|
+
In practice, you'll likely want to be more discerning. You can specify
|
107
|
+
as many comma separated topics as you like for a given task, as well as
|
108
|
+
use AMQP wildcard characters.
|
109
|
+
|
110
|
+
# Subscribe to hypothetical creation events for all types,
|
111
|
+
# and deletion events for users.
|
112
|
+
#
|
113
|
+
subscribe_to '#.create, 'users.delete'
|
114
|
+
|
115
|
+
In this fashion, you can decide to have many separate (and optionally
|
116
|
+
distributed) tasks that only respond to a small subset of possible
|
117
|
+
events, or more monolithic tasks that can respond to a variety of event
|
118
|
+
topics.
|
119
|
+
|
120
|
+
Because AMQP manages the messages that the bound consumers receive,
|
121
|
+
starting up multiple copies of this Test task (across any number of
|
122
|
+
machines) will automatically cause published events to be received in
|
123
|
+
a round-robin fashion without any additional effort. All running task
|
124
|
+
workers will receive roughly equal numbers of matching events.
|
125
|
+
|
126
|
+
|
127
|
+
== An Aside on Queues and Binding Behavior
|
128
|
+
|
129
|
+
A task will automatically create an auto-delete queue for itself with a
|
130
|
+
sane (but configurable) name, along with subscription bindings, ONLY if
|
131
|
+
the queue didn't previously exist. The auto-delete flag ensures that
|
132
|
+
when the last worker disconnects, the queue is automatically removed
|
133
|
+
from the AMQP broker, setting everything back to the state it was before
|
134
|
+
a Symphony worker ever connected.
|
135
|
+
|
136
|
+
Along the same logic as the initial exchange creation, if a matching
|
137
|
+
queue exists already for a task name (an automatically generated or
|
138
|
+
manually specified name with the 'queue_name' option), then a binding is
|
139
|
+
NOT created. Symphony is 'hands off' for anything server side that
|
140
|
+
already exists, so you can enforce specific behaviors, and know that
|
141
|
+
Symphony won't fiddle with them.
|
142
|
+
|
143
|
+
This can be confusing if you've created a queue manually for a task,
|
144
|
+
and didn't also manually create a binding for the topic key. There are
|
145
|
+
some advanced routing cases where you'll want to set up queues yourself,
|
146
|
+
rather than let Symphony automatically create and remove them,
|
147
|
+
we'll talk more on that later.
|
148
|
+
|
149
|
+
|
150
|
+
== Return Values
|
151
|
+
|
152
|
+
By default, a Task is configured to tell AMQP when it has completed its
|
153
|
+
job. It does this by returning +true+ from the #work method. When AMQP
|
154
|
+
sees the job was completed, the message is considered "delivered", and
|
155
|
+
its lifetime is at an end. If instead, the task returns an explicit
|
156
|
+
+false+, the message is retried, potentially on a different task worker.
|
157
|
+
|
158
|
+
If something within the task raises an exception, the default behavior
|
159
|
+
is to permanently abort the task. If you need different behavior,
|
160
|
+
you'll need to catch the exception. As the task author, you're in the
|
161
|
+
best position to know if a failure state requires an immediate retry,
|
162
|
+
or a permanent rejection. If you'll be allowing message retries, you
|
163
|
+
might also want to consider publishing messages with a maximum TTL, to
|
164
|
+
avoid any situations that could cause jobs to loop infinitely. (Default
|
165
|
+
Max-TTL is an example of something that is settable when creating queues
|
166
|
+
on the RabbitMQ server manually.)
|
167
|
+
|
168
|
+
|
169
|
+
= Task Options
|
170
|
+
|
171
|
+
== Message acknowledgement
|
172
|
+
|
173
|
+
If you don't require retry or reject behaviors on task error states, you
|
174
|
+
can set 'acknowledge' to +false+. With this setting, the AMQP server
|
175
|
+
considers a message as "delivered" the moment a consumer receives it.
|
176
|
+
This can be useful for additional speed when processing non-important
|
177
|
+
events.
|
178
|
+
|
179
|
+
acknowledge false # (default: true)
|
180
|
+
|
181
|
+
|
182
|
+
== Worker Model
|
183
|
+
|
184
|
+
By default, a task signals that is ready for more messages as soon as
|
185
|
+
it finishes processing one. This isn't always the optimal environment
|
186
|
+
for long running processes. The work you want to accomplish might
|
187
|
+
require heavy computing resources, and you might want to do things
|
188
|
+
like relinquish memory in between messages, or disconnect from network
|
189
|
+
resources (databases, etc.)
|
190
|
+
|
191
|
+
Settings your work_model to 'oneshot' causes the task to exit
|
192
|
+
immediately after performing its work. Clearly, this only makes sense
|
193
|
+
if you're running managed processes under the Symphony daemon, so a
|
194
|
+
fresh process can take its place. You can do all of your expensive work
|
195
|
+
within the #work method, leaving the main "waiting for messages" loop as
|
196
|
+
low-resource as possible.
|
197
|
+
|
198
|
+
work_model :oneshot # (default: :longlived)
|
199
|
+
|
200
|
+
|
201
|
+
== Message Prefetch
|
202
|
+
|
203
|
+
Prefetch is another tuning for speed on extremely busy exchanges. If
|
204
|
+
a task worker sees there are additional pending messages, it can
|
205
|
+
concurrently retrieve and store them locally in memory while performing
|
206
|
+
current work. This can reduce consumer/server network traffic, but the
|
207
|
+
trade off is that all prefetched message payloads are stored in memory
|
208
|
+
until they are processed. It's a good idea to keep this low if your
|
209
|
+
payloads are large.
|
210
|
+
|
211
|
+
prefetch 100 # (default: 10)
|
212
|
+
|
213
|
+
Message prefetch is ignored (automatically set to 1) if the task's
|
214
|
+
work_model is set to oneshot.
|
215
|
+
|
216
|
+
|
217
|
+
== Timeout
|
218
|
+
|
219
|
+
The timeout option specifies the maximum length of time (in seconds)
|
220
|
+
a task can be within its #work method, and what action to take if it
|
221
|
+
exceeds that timeframe. Please note, this is different from the Max-TTL
|
222
|
+
setting for AMQP, which dictates the maximum timeframe a message can
|
223
|
+
exist in a queue.
|
224
|
+
|
225
|
+
AMQP can't know if a task is in ruby loop or otherwise unfortunate
|
226
|
+
state, so this can ensure workers won't ever get permanently stuck.
|
227
|
+
There is no default timeout. If one is set, the default action is to
|
228
|
+
act as an exception, which is a permanent rejection. You can choose to
|
229
|
+
retry instead:
|
230
|
+
|
231
|
+
# perform work for maximum 20 seconds, then stop and try
|
232
|
+
# again on another worker
|
233
|
+
#
|
234
|
+
timeout 20.0, :action => :retry
|
235
|
+
|
236
|
+
== Queue Name
|
237
|
+
|
238
|
+
By default, Symphony will try and create a queue based on the Class
|
239
|
+
name of the task. You can override this per-task.
|
240
|
+
|
241
|
+
class Test < Symphony::Task
|
242
|
+
|
243
|
+
# Process all events
|
244
|
+
subscribe_to '#'
|
245
|
+
|
246
|
+
# I don't want to connect to a 'test' queue, lets make it interesting:
|
247
|
+
queue_name 'shazzzaaaam'
|
248
|
+
|
249
|
+
def work( payload, metadata )
|
250
|
+
...
|
251
|
+
|
252
|
+
|
253
|
+
= Plugins
|
254
|
+
|
255
|
+
Plugins change or enhance the behavior of a Symphony::Task. They
|
256
|
+
are enabled with the 'plugin' option, with the name of the plugin (or
|
257
|
+
comma separated plugins) as a symbol argument. Symphony currently
|
258
|
+
ships with two.
|
259
|
+
|
260
|
+
|
261
|
+
== Metrics
|
262
|
+
|
263
|
+
The metrics plugin provides periodic information about the performance
|
264
|
+
of a task worker to the log. It shows processed message averages and
|
265
|
+
resource consumption summaries at the "info" log level, and also changes
|
266
|
+
the process name to display a total count and jobs per second rate.
|
267
|
+
|
268
|
+
plugin :metrics
|
269
|
+
|
270
|
+
|
271
|
+
== Routing
|
272
|
+
|
273
|
+
The routing plugin removes the requirement to perform all processing
|
274
|
+
in the #work method, and instead links individual units of work to
|
275
|
+
separate #on declarations. This makes tasks that are designed to
|
276
|
+
receive multiple message topics much easier to maintain and test.
|
277
|
+
|
278
|
+
class Test < Symphony::Task
|
279
|
+
|
280
|
+
# Use the routing plugin
|
281
|
+
plugin :routing
|
282
|
+
|
283
|
+
on 'users.create', 'workstation.create' do |payload, metadata|
|
284
|
+
puts "A user or workstation wants to be created!"
|
285
|
+
# ...
|
286
|
+
return true
|
287
|
+
end
|
288
|
+
|
289
|
+
on 'users.delete' do |payload, metadata|
|
290
|
+
puts "A user wants to be deleted!"
|
291
|
+
return true
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
The #on method accepts the same syntax as the 'subscribe_to' option. In
|
296
|
+
fact, if you're using the routing plugin, it removes the requirement of
|
297
|
+
'subscribe_to' altogether, making it entirely redundant. #on blocks that
|
298
|
+
match multiple times for a message will be executed in top down order.
|
299
|
+
It will only return true (signalling success) if all blocks return true.
|
300
|
+
|
301
|
+
|
302
|
+
= Publishing messages
|
303
|
+
|
304
|
+
Because AMQP is language agnostic and a message can have many unique
|
305
|
+
flags attached to it, this is another area where Symphony remains
|
306
|
+
neutral, and imposes nothing.
|
307
|
+
|
308
|
+
If you want to publish or republish from within Symphony, you can
|
309
|
+
get a reference to the exchange object Symphony is binding to via
|
310
|
+
the Symphony::Queue class:
|
311
|
+
|
312
|
+
exchange = Symphony::Queue.amqp_exchange
|
313
|
+
|
314
|
+
This is a Bunny object that you can interact with directly. See the
|
315
|
+
Bunny documentation[http://rubybunny.info/articles/exchanges.html] for
|
316
|
+
options.
|
317
|
+
|
318
|
+
|
319
|
+
= How Do I...
|
320
|
+
|
321
|
+
== Avoid Infinite Retry and/or Failure Loops?
|
322
|
+
|
323
|
+
When publishing, include a message expiration. (Again, this has a
|
324
|
+
different goal than the 'timeout' option for the Symphony::Task
|
325
|
+
class, which can dislodge a potentially stuck worker.)
|
326
|
+
|
327
|
+
With an expiration, a task that retries will eventually meet it's
|
328
|
+
maximum lifetime, and AMQP will stop delivering it to consumers. If
|
329
|
+
there is a dead letter queue configured, it will place the message there
|
330
|
+
for later inspection.
|
331
|
+
|
332
|
+
|
333
|
+
== Report on Errors?
|
334
|
+
|
335
|
+
Because messaging is designed to be asynchronous, you won't receive
|
336
|
+
instantaneous results when pushing an event into the void. RabbitMQ has
|
337
|
+
the concept of a 'dead letter queue', which receives events that have
|
338
|
+
failed due to expiration or rejection. They contain the original route
|
339
|
+
information and the original payload.
|
340
|
+
|
341
|
+
RabbitMQ also permits setting 'policies', which apply default settings
|
342
|
+
to any created queue under a virtual host. We've been creating an
|
343
|
+
automatic policy that links any queue that doesn't start with '_' to a
|
344
|
+
dead letter queue called '_failures', so all errors filter there without
|
345
|
+
any further configuration. New queues have the same policy applied
|
346
|
+
automatically.
|
347
|
+
|
348
|
+
% rabbitmqctl list_policies
|
349
|
+
Listing policies ...
|
350
|
+
/ DLX queues ^[^_].* {"dead-letter-exchange":"_failures"} 0
|
351
|
+
|
352
|
+
There is an example task (symphony/lib/tasks/failure_logger.rb)
|
353
|
+
that you're welcome to use as a foundation for your error reporting.
|
354
|
+
What you do with the errors is up to you -- the failure_logger example
|
355
|
+
assumes it is binding to a dead letter queue, and it simply logs to
|
356
|
+
screen.
|
357
|
+
|
358
|
+
|
359
|
+
== Ensure Resiliency for Very Important Messages?
|
360
|
+
|
361
|
+
Like good error reporting, this requires server-side configuration to
|
362
|
+
get rolling.
|
363
|
+
|
364
|
+
If messages are published when no queues are bound and available to
|
365
|
+
receive them, AMQP drops the message immediately. (If the publisher
|
366
|
+
sets the 'mandatory' flag, they'll receive an error and can act
|
367
|
+
accordingly.)
|
368
|
+
|
369
|
+
Instead of setting 'mandatory', you may want to have AMQP accept the
|
370
|
+
message, and save it for task worker to consume at a later time. To do
|
371
|
+
this, you just need to manually create the queue, and the bindings to
|
372
|
+
it from the exchange (you'll probably want these marked as 'durable' as
|
373
|
+
well, to survive RabbitMQ service restarts.)
|
374
|
+
|
375
|
+
With a binding in place, messages will be retained on the RabbitMQ
|
376
|
+
server until a consumer drains them. If the messages are published with
|
377
|
+
the 'persistent' flag, they'll also survive RabbitMQ server restarts.
|
378
|
+
|
379
|
+
With this setup, Symphony won't create or modify any queues. It
|
380
|
+
will just attach, start receiving events, and get to work.
|
381
|
+
|
data/bin/symphony
ADDED
data/bin/symphony-task
ADDED