symphony 0.3.0.pre20140327204419

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+
2
+ [ ] Convert routing / metrics to 'plugins' syntax (inside/outside for prepend/include)
3
+ [ ] Process management w/ Daemon
4
+ [ ] Update pinger/ssh/sshscript
5
+
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'symphony'
4
+ require 'symphony/worker_daemon'
5
+
6
+ Encoding.default_internal = Encoding::UTF_8
7
+ Symphony::Daemon.run( ARGV )
8
+
data/bin/symphony-task ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'symphony'
4
+ require 'symphony/task'
5
+
6
+ taskname = ARGV.shift or raise "no task name given"
7
+
8
+ Symphony.load_config( 'etc/config.yml' )
9
+ Symphony::Task.get_subclass( taskname ).run
10
+
@@ -0,0 +1,9 @@
1
+ ---
2
+ amqp:
3
+ broker_uri: amqp://user:pass@amqp.local:5672/%2Fasync%2Fjobs
4
+ exchange: events
5
+
6
+ logging:
7
+ __default__: debug STDERR (color)
8
+
9
+