symphony 0.3.0.pre20140327204419 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5a18ff1610ed82fd8b70b2101553dd540b0855e7
4
- data.tar.gz: 13658dcf21296bcb6fc7094cb68fddb263ace02f
3
+ metadata.gz: 2ba69bb4284905cfc71472c6b5629c3c063dd546
4
+ data.tar.gz: 2bf944cdfce3b98d5eaf91732a9ef62f093eef65
5
5
  SHA512:
6
- metadata.gz: dccd19e742e0817f2de1a8555490aa193f20abde32ea2ecc120b330d1edc5fed2ee511ecbd6097ecd692af1c55b2e1e5028dcd1b7c1040429f6509db875b2049
7
- data.tar.gz: 33c92a0f1a2a84b13982a25321af137b0c445d9ac2950372568066fcd4e8329e51af5be0ac5646408940cf32b047be7208b6124ca38604969eb916b63508e3da
6
+ metadata.gz: 3c656bb9f2ce03e943eda18baec1eac6e7d3c9aad80c771db522cc4c38984e9fdf214fe8ddb0379208775f050877badac843507c0a3cb575ac10c05b8ddbbda6
7
+ data.tar.gz: 2125f48a7132995aa9a60465e430b656cedf198f48c6e5688e7f317b911f63d0e45c1f2bc5bde4e8b69c7f057397536dcfe39acd60f65c31c2ebb232d16a2e4c
Binary file
data.tar.gz.sig CHANGED
Binary file
File without changes
@@ -1,6 +1,6 @@
1
- == v0.3.0 []
1
+ == v0.3.0 [2014-03-28] Michael Granger <ged@FaerieMUD.org>
2
2
 
3
- Rewritten for AMQP (and possibly other similar message buses)
3
+ Rewritten for AMQP and renamed to Symphony.
4
4
 
5
5
 
6
6
  == v0.1.0 [2012-09-19] Michael Granger <ged@FaerieMUD.org>
@@ -23,8 +23,9 @@ lib/symphony/tasks/pinger.rb
23
23
  lib/symphony/tasks/simulator.rb
24
24
  lib/symphony/tasks/ssh.rb
25
25
  lib/symphony/tasks/sshscript.rb
26
+ spec/helpers.rb
27
+ spec/symphony/daemon_spec.rb
26
28
  spec/symphony/mixins_spec.rb
27
29
  spec/symphony/queue_spec.rb
28
30
  spec/symphony/task_spec.rb
29
31
  spec/symphony_spec.rb
30
- spec/helpers.rb
data/Rakefile CHANGED
@@ -15,7 +15,6 @@ Hoe.plugin :deveiate
15
15
  Hoe.plugin :bundler
16
16
 
17
17
  Hoe.plugins.delete :rubyforge
18
- Hoe.plugins.delete :gemcutter
19
18
 
20
19
  hoespec = Hoe.spec 'symphony' do |spec|
21
20
  spec.readme_file = 'README.rdoc'
data/TODO.md CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
2
  [ ] Convert routing / metrics to 'plugins' syntax (inside/outside for prepend/include)
3
- [ ] Process management w/ Daemon
4
3
  [ ] Update pinger/ssh/sshscript
5
-
4
+ [ ] Per Task throttling for daemon
5
+ [ ] Task scaling (min/max settings), check queue backlog during received message
6
+ [ ] Make the block of routes declared with on() method accept various arities
data/USAGE.rdoc CHANGED
@@ -252,10 +252,7 @@ name of the task. You can override this per-task.
252
252
 
253
253
  = Plugins
254
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.
255
+ Plugins change or enhance the behavior of a Symphony::Task.
259
256
 
260
257
 
261
258
  == Metrics
@@ -265,8 +262,13 @@ of a task worker to the log. It shows processed message averages and
265
262
  resource consumption summaries at the "info" log level, and also changes
266
263
  the process name to display a total count and jobs per second rate.
267
264
 
268
- plugin :metrics
265
+ require 'symphony/metrics'
269
266
 
267
+ class Test < Symphony::Task
268
+ prepend Symphony::Metrics
269
+ # ...
270
+ end
271
+
270
272
 
271
273
  == Routing
272
274
 
@@ -275,10 +277,10 @@ in the #work method, and instead links individual units of work to
275
277
  separate #on declarations. This makes tasks that are designed to
276
278
  receive multiple message topics much easier to maintain and test.
277
279
 
280
+ require 'symphony/routing'
281
+
278
282
  class Test < Symphony::Task
279
-
280
- # Use the routing plugin
281
- plugin :routing
283
+ include Symphony::Routing
282
284
 
283
285
  on 'users.create', 'workstation.create' do |payload, metadata|
284
286
  puts "A user or workstation wants to be created!"
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'symphony'
4
- require 'symphony/worker_daemon'
4
+ require 'symphony/daemon'
5
5
 
6
6
  Encoding.default_internal = Encoding::UTF_8
7
7
  Symphony::Daemon.run( ARGV )
8
-
@@ -15,11 +15,11 @@ module Symphony
15
15
  VERSION = '0.3.0'
16
16
 
17
17
  # Version-control revision constant
18
- REVISION = %q$Revision$
18
+ REVISION = %q$Revision: cf61ced7d585 $
19
19
 
20
20
 
21
21
  # The name of the environment variable to check for config file overrides
22
- CONFIG_ENV = 'GROUNDCONTROL_CONFIG'
22
+ CONFIG_ENV = 'SYMPHONY_CONFIG'
23
23
 
24
24
  # The path to the default config file
25
25
  DEFAULT_CONFIG_FILE = 'etc/config.yml'
@@ -3,19 +3,17 @@
3
3
 
4
4
  require 'configurability'
5
5
  require 'loggability'
6
- require 'fcntl'
7
- require 'trollop'
8
6
 
9
7
  require 'symphony' unless defined?( Symphony )
10
- require 'symphony/worker'
11
8
  require 'symphony/task'
9
+ require 'symphony/signal_handling'
12
10
 
13
-
14
- # The Symphony worker daemon. Watches a Symphony job queue, and runs the tasks
15
- # contained in the jobs it fetches.
11
+ # A daemon which manages startup and shutdown of one or more Workers
12
+ # running Tasks as they are published from a queue.
16
13
  class Symphony::Daemon
17
14
  extend Loggability,
18
- Configurability
15
+ Configurability,
16
+ Symphony::MethodUtilities
19
17
 
20
18
  include Symphony::SignalHandling
21
19
 
@@ -24,27 +22,42 @@ class Symphony::Daemon
24
22
  log_to :symphony
25
23
 
26
24
  # Configurability API -- use the 'worker_daemon' section of the config
27
- config_key :worker_daemon
25
+ config_key :symphony
26
+
28
27
 
28
+ # Default configuration
29
+ CONFIG_DEFAULTS = {
30
+ throttle_max: 16,
31
+ throttle_factor: 1,
32
+ tasks: []
33
+ }
29
34
 
30
35
  # Signals we understand
31
36
  QUEUE_SIGS = [
32
- :QUIT, :INT, :TERM, :HUP,
37
+ :QUIT, :INT, :TERM, :HUP, :CHLD,
33
38
  # :TODO: :WINCH, :USR1, :USR2, :TTIN, :TTOU
34
39
  ]
35
40
 
36
- # The maximum throttle value caused by failing workers
37
- THROTTLE_MAX = 16
38
-
39
- # The factor which controls how much incrementing the throttle factor
40
- # affects the pause between workers being started.
41
- THROTTLE_FACTOR = 2
42
41
 
43
42
 
44
43
  #
45
44
  # Class methods
46
45
  #
47
46
 
47
+ ##
48
+ # The maximum throttle factor caused by failing workers
49
+ singleton_attr_accessor :throttle_max
50
+
51
+ ##
52
+ # The factor which controls how much incrementing the throttle factor
53
+ # affects the pause between workers being started.
54
+ singleton_attr_accessor :throttle_factor
55
+
56
+ ##
57
+ # The Array of Symphony::Task classes that are configured to run
58
+ singleton_attr_accessor :tasks
59
+
60
+
48
61
  ### Get the daemon's version as a String.
49
62
  def self::version_string( include_buildnum=false )
50
63
  vstring = "%s %s" % [ self.name, Symphony::VERSION ]
@@ -56,37 +69,41 @@ class Symphony::Daemon
56
69
  end
57
70
 
58
71
 
59
- ### Start the daemon.
60
- def self::run( argv )
61
- Loggability.format_with( :color ) if $stdout.tty?
72
+ ### Configurability API -- configure the daemon.
73
+ def self::configure( config=nil )
74
+ config = self.defaults.merge( config || {} )
62
75
 
63
- progname = File.basename( $0 )
64
- opts = Trollop.options do
65
- banner "Usage: #{progname} OPTIONS"
66
- version self.version_string( true )
76
+ self.throttle_max = config[:throttle_max]
77
+ self.throttle_factor = config[:throttle_factor]
78
+
79
+ self.tasks = self.load_configured_tasks( config[:tasks] )
80
+ end
67
81
 
68
- opt :config, "The config file to load instead of the default",
69
- :type => :string
70
- opt :crew_size, "Number of workers to maintain.", :default => DEFAULT_CREW_SIZE
71
- opt :queue, "The name of the queue to monitor.", :default => '_default_'
72
82
 
73
- opt :debug, "Turn on debugging output."
83
+ ### Load the tasks with the specified +task_names+ and return them
84
+ ### as an Array.
85
+ def self::load_configured_tasks( task_names )
86
+ return task_names.map do |task_name|
87
+ Symphony::Task.get_subclass( task_name )
74
88
  end
89
+ end
90
+
91
+
92
+ ### Start the daemon.
93
+ def self::run( args )
94
+ Loggability.format_with( :color ) if $stdout.tty?
75
95
 
76
96
  # Turn on debugging if it's enabled
77
- if opts.debug
78
- $DEBUG = true
79
- Loggability.level = :debug
80
- end
97
+ Loggability.level = :debug if $DEBUG
81
98
 
82
99
  # Now load the config file
83
- Symphony.load_config( opts.config )
100
+ Symphony.load_config( args.shift )
84
101
 
85
102
  # Re-enable debug-level logging if the config reset it
86
- Loggability.level = :debug if opts.debug
103
+ Loggability.level = :debug if $DEBUG
87
104
 
88
105
  # And start the daemon
89
- self.new( opts ).run
106
+ self.new.run
90
107
  end
91
108
 
92
109
 
@@ -95,13 +112,11 @@ class Symphony::Daemon
95
112
  #
96
113
 
97
114
  ### Create a new Daemon instance.
98
- def initialize( options )
99
- @options = options
100
- @queue = Symphony::Queue.new( options.queue )
101
-
115
+ def initialize
102
116
  # Process control
103
- @crew_size = options.crew_size
104
- @crew_workers = []
117
+ @tasks = self.class.tasks
118
+
119
+ @running_tasks = {}
105
120
  @running = false
106
121
  @shutting_down = false
107
122
  @throttle = 0
@@ -115,11 +130,8 @@ class Symphony::Daemon
115
130
  public
116
131
  ######
117
132
 
118
- # The Array of PIDs of currently-running workers
119
- attr_reader :crew_workers
120
-
121
- # The maximum number of children to have running at any given time
122
- attr_reader :crew_size
133
+ # The Hash of PIDs to task class
134
+ attr_reader :running_tasks
123
135
 
124
136
  # A self-pipe for deferred signal-handling
125
137
  attr_reader :selfpipe
@@ -145,19 +157,13 @@ class Symphony::Daemon
145
157
 
146
158
  ### Set up the daemon and start running.
147
159
  def run
148
- self.log.info "Starting worker supervisor"
149
-
150
- # Become session leader if we can
151
- if Process.euid.zero?
152
- sid = Process.setsid
153
- self.log.debug " became session leader of new session: %d" % [ sid ]
154
- end
160
+ self.log.info "Starting task daemon"
155
161
 
156
162
  # Set up traps for common signals
157
163
  self.set_signal_traps( *QUEUE_SIGS )
158
164
 
159
165
  # Listen for new jobs and handle them as they come in
160
- self.start_handling_jobs
166
+ self.run_tasks
161
167
 
162
168
  # Restore the default signal handlers
163
169
  self.reset_signal_traps( *QUEUE_SIGS )
@@ -168,26 +174,22 @@ class Symphony::Daemon
168
174
 
169
175
  ### The main loop of the daemon -- wait for signals, children dying, or jobs, and
170
176
  ### take appropriate action.
171
- def start_handling_jobs
177
+ def run_tasks
172
178
  @running = true
173
179
 
174
180
  self.log.debug "Starting supervisor loop..."
175
181
  while self.running?
176
182
  self.start_missing_children unless self.shutting_down?
177
-
178
- timeout = self.throttle_seconds
179
- timeout = nil if timeout.zero?
180
-
181
183
  self.wait_for_signals
182
184
  self.reap_children
183
185
  end
184
- self.log.info "Supervisor job loop done."
185
186
 
186
187
  rescue => err
187
188
  self.log.fatal "%p in job-handler loop: %s" % [ err.class, err.message ]
188
189
  self.log.debug { ' ' + err.backtrace.join("\n ") }
189
190
 
190
191
  ensure
192
+ self.log.info "Done running tasks."
191
193
  @running = false
192
194
  self.stop
193
195
  end
@@ -202,18 +204,18 @@ class Symphony::Daemon
202
204
 
203
205
  self.log.warn "Stopping children."
204
206
  3.times do |i|
205
- self.reap_children( *self.crew_workers )
207
+ self.reap_children
206
208
  sleep( 1 )
207
209
  self.kill_children
208
210
  sleep( 1 )
209
- break if self.crew_workers.empty?
211
+ break if self.running_tasks.empty?
210
212
  sleep( 1 )
211
- end unless self.crew_workers.empty?
213
+ end unless self.running_tasks.empty?
212
214
 
213
215
  # Give up on our remaining children.
214
216
  Signal.trap( :CHLD, :IGNORE )
215
- if !self.crew_workers.empty?
216
- self.log.warn " %d workers remain: sending KILL" % [ self.crew_workers.length ]
217
+ if !self.running_tasks.empty?
218
+ self.log.warn " %d workers remain: sending KILL" % [ self.running_tasks.length ]
217
219
  self.kill_children( :KILL )
218
220
  end
219
221
  end
@@ -232,11 +234,11 @@ class Symphony::Daemon
232
234
 
233
235
  ### Handle signals.
234
236
  def handle_signal( sig )
235
- self.log.debug "Handling signal %s" % [ sig ]
237
+ self.log.debug "Handling signal %s in PID %d" % [ sig, Process.pid ]
236
238
  case sig
237
239
  when :INT, :TERM
238
240
  if @running
239
- self.log.warn "%s signal: immediate shutdown" % [ sig ]
241
+ self.log.warn "%s signal: graceful shutdown" % [ sig ]
240
242
  @running = false
241
243
  else
242
244
  self.ignore_signals
@@ -250,6 +252,7 @@ class Symphony::Daemon
250
252
  self.reload_config
251
253
 
252
254
  when :CHLD
255
+ self.log.info "Got SIGCHLD."
253
256
  # Just need to wake up, nothing else necessary
254
257
 
255
258
  else
@@ -259,10 +262,10 @@ class Symphony::Daemon
259
262
  end
260
263
 
261
264
 
262
- ### Fill out the work crew with new children if necessary
265
+ ### Start any tasks which aren't already running
263
266
  def start_missing_children
264
- missing_count = self.crew_size - self.crew_workers.length
265
- return unless missing_count > 0
267
+ missing_tasks = self.find_missing_tasks
268
+ return if missing_tasks.empty?
266
269
 
267
270
  # Return unless the throttle period has lapsed
268
271
  unless self.throttle_seconds < (Time.now - @last_child_started)
@@ -271,21 +274,37 @@ class Symphony::Daemon
271
274
  return
272
275
  end
273
276
 
274
- self.log.debug "Starting %d workers for a crew of %d" % [ missing_count, self.crew_size ]
275
- missing_count.times do |i|
276
- pid = self.start_worker
277
- self.log.debug " started worker %d" % [ pid ]
278
- self.crew_workers << pid
277
+ self.log.debug "Starting %d tasks out of %d" % [ missing_tasks.size, self.class.tasks.size ]
278
+ missing_tasks.each do |task_class|
279
+ pid = self.start_worker( task_class )
280
+ self.log.debug " started task %p at pid %d" % [ task_class, pid ]
281
+ self.running_tasks[ pid ] = task_class
279
282
  end
280
283
 
281
284
  @last_child_started = Time.now
282
285
  end
283
286
 
284
287
 
288
+ ### Examine the running tasks and return any that are missing.
289
+ def find_missing_tasks
290
+ missing_tasks = []
291
+
292
+ self.class.tasks.uniq.each do |task_type|
293
+ count = self.class.tasks.count( task_type )
294
+ missing = count - self.running_tasks.values.count( task_type )
295
+ missing.times do
296
+ missing_tasks << task_type
297
+ end
298
+ end
299
+
300
+ return missing_tasks
301
+ end
302
+
303
+
285
304
  ### Return the number of seconds between child startup times.
286
305
  def throttle_seconds
287
306
  return 0 unless @throttle.nonzero?
288
- return Math.log( @throttle ) * THROTTLE_FACTOR
307
+ return Math.log( @throttle ) * self.class.throttle_factor
289
308
  end
290
309
 
291
310
 
@@ -295,18 +314,18 @@ class Symphony::Daemon
295
314
  self.log.debug "Adjusting worker throttle by %d" % [ adjustment ]
296
315
  @throttle += adjustment
297
316
  @throttle = 0 if @throttle < 0
298
- @throttle = THROTTLE_MAX if @throttle > THROTTLE_MAX
317
+ @throttle = self.class.throttle_max if @throttle > self.class.throttle_max
299
318
  end
300
319
 
301
320
 
302
321
  ### Kill all current children with the specified +signal+. Returns +true+ if the signal was
303
322
  ### sent to one or more children.
304
323
  def kill_children( signal=:TERM )
305
- return false if self.crew_workers.empty?
324
+ return false if self.running_tasks.empty?
306
325
 
307
- self.log.info "Sending %s signal to %d workers: %p." %
308
- [ signal, self.crew_workers.length, self.crew_workers ]
309
- Process.kill( signal, *self.crew_workers )
326
+ self.log.info "Sending %s signal to %d task pids: %p." %
327
+ [ signal, self.running_tasks.length, self.running_tasks.keys ]
328
+ Process.kill( signal, *self.running_tasks.keys )
310
329
 
311
330
  return true
312
331
  rescue Errno::ESRCH
@@ -314,11 +333,18 @@ class Symphony::Daemon
314
333
  end
315
334
 
316
335
 
317
- ### Start a new Symphony::Worker and return its PID.
318
- def start_worker
336
+ ### Start a new Symphony::Task and return its PID.
337
+ def start_worker( task_class )
319
338
  return if self.shutting_down?
320
- self.log.debug "Starting a worker."
321
- return Symphony::Worker.start( self.queue )
339
+
340
+ self.log.debug "Starting a %p." % [ task_class ]
341
+ pid = Process.fork do
342
+ task_class.after_fork
343
+ task_class.run
344
+ end
345
+ Process.setpgid( pid, 0 )
346
+
347
+ return pid
322
348
  end
323
349
 
324
350
 
@@ -340,28 +366,30 @@ class Symphony::Daemon
340
366
 
341
367
 
342
368
  ### Reap any children that have died within the caller's process group
343
- ### and remove them from the work crew.
369
+ ### and remove them from the Hash of running tasks.
344
370
  def reap_any_child
345
371
  self.log.debug " no pids; waiting on any child in this process group"
346
372
 
347
- pid, status = Process.waitpid2( -1, Process::WNOHANG )
373
+ pid, status = Process.waitpid2( -1, Process::WNOHANG|Process::WUNTRACED )
374
+ self.log.debug " waitpid2 returned: [ %p, %p ]" % [ pid, status ]
348
375
  while pid
349
376
  self.adjust_throttle( status.success? ? -1 : 1 )
350
377
  self.log.debug "Child %d exited: %p." % [ pid, status ]
351
- self.crew_workers.delete( pid )
378
+ self.running_tasks.delete( pid )
352
379
 
353
- pid, status = Process.waitpid2( -1, Process::WNOHANG )
380
+ pid, status = Process.waitpid2( -1, Process::WNOHANG|Process::WUNTRACED )
381
+ self.log.debug " waitpid2 returned: [ %p, %p ]" % [ pid, status ]
354
382
  end
355
383
  end
356
384
 
357
385
 
358
386
  ### Wait on the child associated with the given +pid+, deleting it from the
359
- ### crew workers if successful.
387
+ ### running tasks Hash if successful.
360
388
  def reap_specific_child( pid )
361
389
  spid, status = Process.waitpid2( pid )
362
390
  if spid
363
391
  self.log.debug "Child %d exited: %p." % [ spid, status ]
364
- self.crew_workers.delete( spid )
392
+ self.running_tasks.delete( spid )
365
393
  self.adjust_throttle( status.success? ? -1 : 1 )
366
394
  else
367
395
  self.log.debug "Child %d no reapy." % [ pid ]
@@ -28,7 +28,8 @@ module Symphony::Metrics
28
28
 
29
29
  @log_reporter = Metriks::Reporter::Logger.new(
30
30
  logger: Loggability[ Symphony ],
31
- registry: @metriks_registry )
31
+ registry: @metriks_registry,
32
+ prefix: self.class.name )
32
33
  @proc_reporter = Metriks::Reporter::ProcTitle.new(
33
34
  prefix: self.class.name,
34
35
  registry: @metriks_registry,
@@ -209,7 +209,8 @@ class Symphony::Queue
209
209
  tag = self.consumer_tag
210
210
 
211
211
  # Last argument is *no_ack*, so need to invert the logic
212
- self.log.debug "Creating Bunny::Consumer for %p with tag: %s" % [ amqp_queue, tag ]
212
+ self.log.debug "Creating consumer for the '%s' queue with tag: %s" %
213
+ [ amqp_queue.name, tag ]
213
214
  cons = Bunny::Consumer.new( amqp_queue.channel, amqp_queue, tag, !ackmode )
214
215
 
215
216
  cons.on_delivery do |delivery_info, properties, payload|
@@ -309,5 +310,20 @@ class Symphony::Queue
309
310
  self.consumer.channel.close
310
311
  end
311
312
 
313
+
314
+ ### Return a human-readable representation of the Queue in a form suitable for debugging.
315
+ def inspect
316
+ return "#<%p:%#0x %s (%s) ack: %s, routing: %p, prefetch: %d>" % [
317
+ self.class,
318
+ self.object_id * 2,
319
+ self.name,
320
+ self.consumer_tag,
321
+ self.acknowledge ? "yes" : "no",
322
+ self.routing_keys,
323
+ self.prefetch,
324
+ ]
325
+ end
326
+
327
+
312
328
  end # class Symphony::Queue
313
329
 
@@ -89,6 +89,7 @@ module Symphony::SignalHandling
89
89
  def ignore_signals( *signals )
90
90
  self.log.debug "Ignoring signals."
91
91
  signals.each do |sig|
92
+ next if sig == :CHLD
92
93
  Signal.trap( sig, :IGNORE )
93
94
  end
94
95
  end
@@ -51,6 +51,14 @@ class Symphony::Task
51
51
  end
52
52
 
53
53
 
54
+ ### Prepare the process after being forked from the Daemon.
55
+ def self::after_fork
56
+ self.log.debug "After fork [%d]: Threads: %p" % [ Process.pid, ThreadGroup::Default.list ]
57
+ Process.setpgrp
58
+ Symphony.config.install if Symphony.config
59
+ end
60
+
61
+
54
62
  ### Inheritance hook -- set some defaults on subclasses.
55
63
  def self::inherited( subclass )
56
64
  super
@@ -193,6 +201,7 @@ class Symphony::Task
193
201
  end
194
202
 
195
203
 
204
+
196
205
  ######
197
206
  public
198
207
  ######
@@ -0,0 +1,13 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require_relative '../helpers'
5
+
6
+ require 'symphony/daemon'
7
+
8
+ describe Symphony::Daemon do
9
+
10
+
11
+
12
+ end
13
+
@@ -150,7 +150,7 @@ describe Symphony::Queue do
150
150
 
151
151
 
152
152
  it "subscribes to the message queue with a configured consumer to wait for messages" do
153
- amqp_queue = double( "AMQP queue", channel: described_class.amqp_channel )
153
+ amqp_queue = double( "AMQP queue", name: 'a queue', channel: described_class.amqp_channel )
154
154
  consumer = double( "Bunny consumer", channel: described_class.amqp_channel )
155
155
 
156
156
  expect( described_class.amqp_channel ).to receive( :queue ).
@@ -202,7 +202,7 @@ describe Symphony::Queue do
202
202
 
203
203
 
204
204
  it "sets up the queue and consumer to only run once if waiting in one-shot mode" do
205
- amqp_queue = double( "AMQP queue", channel: described_class.amqp_channel )
205
+ amqp_queue = double( "AMQP queue", name: 'a queue', channel: described_class.amqp_channel )
206
206
  consumer = double( "Bunny consumer", channel: described_class.amqp_channel )
207
207
 
208
208
  expect( described_class.amqp_channel ).to receive( :queue ).
@@ -243,7 +243,7 @@ describe Symphony::Queue do
243
243
 
244
244
 
245
245
  it "shuts down the consumer if the queues it's consuming from is deleted on the server" do
246
- amqp_queue = double( "AMQP queue", channel: described_class.amqp_channel )
246
+ amqp_queue = double( "AMQP queue", name: 'a queue', channel: described_class.amqp_channel )
247
247
  consumer = double( "Bunny consumer", channel: described_class.amqp_channel )
248
248
 
249
249
  expect( described_class.amqp_channel ).to receive( :queue ).
@@ -277,7 +277,7 @@ describe Symphony::Queue do
277
277
 
278
278
  it "creates a consumer with acknowledgements enabled if it has acknowledgements enabled" do
279
279
  amqp_channel = double( "AMQP channel" )
280
- amqp_queue = double( "AMQP queue", channel: amqp_channel )
280
+ amqp_queue = double( "AMQP queue", name: 'a queue', channel: amqp_channel )
281
281
  consumer = double( "Bunny consumer" )
282
282
 
283
283
  # Ackmode argument is actually 'no_ack'
@@ -293,7 +293,7 @@ describe Symphony::Queue do
293
293
 
294
294
  it "creates a consumer with acknowledgements disabled if it has acknowledgements disabled" do
295
295
  amqp_channel = double( "AMQP channel" )
296
- amqp_queue = double( "AMQP queue", channel: amqp_channel )
296
+ amqp_queue = double( "AMQP queue", name: 'a queue', channel: amqp_channel )
297
297
  consumer = double( "Bunny consumer" )
298
298
 
299
299
  # Ackmode argument is actually 'no_ack'
@@ -64,84 +64,205 @@ describe Symphony::Task do
64
64
 
65
65
  context "a concrete subclass" do
66
66
 
67
- before( :each ) do
68
- @task_class = Class.new( described_class ) do
67
+ let( :task_class ) do
68
+ Class.new( described_class ) do
69
69
  def self::name; 'ACME::TestingTask'; end
70
70
  end
71
71
  end
72
+ let( :payload ) {{ "the" => "payload" }}
73
+ let( :serialized_payload ) { Yajl.dump(payload) }
74
+ let( :metadata ) {{ :content_type => 'application/json' }}
75
+ let( :queue ) do
76
+ obj = Symphony::Queue.for_task( task_class )
77
+ # Don't really talk to AMQP for messages
78
+ allow( obj ).to receive( :wait_for_message ) do |oneshot, &callback|
79
+ callback.call( serialized_payload, metadata )
80
+ end
81
+ obj
82
+ end
83
+
84
+
85
+ it "puts the process into its own process group after a fork" do
86
+ expect( Process ).to receive( :setpgrp ).with( no_args )
87
+ task_class.after_fork
88
+ end
72
89
 
73
90
 
74
91
  it "raises an exception if run without specifying any subscriptions" do
75
- expect { @task_class.run }.to raise_error( ScriptError, /no subscriptions/i )
92
+ expect { task_class.run }.to raise_error( ScriptError, /no subscriptions/i )
76
93
  end
77
94
 
78
95
 
79
96
  it "can set an explicit queue name" do
80
- @task_class.queue_name( 'happy.fun.queue' )
81
- expect( @task_class.queue_name ).to eq( 'happy.fun.queue' )
97
+ task_class.queue_name( 'happy.fun.queue' )
98
+ expect( task_class.queue_name ).to eq( 'happy.fun.queue' )
99
+ end
100
+
101
+
102
+ it "can set the number of messages to prefetch" do
103
+ task_class.prefetch( 10 )
104
+ expect( task_class.prefetch ).to eq( 10 )
82
105
  end
83
106
 
84
107
 
85
108
  it "can retry on timeout instead of rejecting" do
86
- @task_class.timeout_action( :retry )
87
- expect( @task_class.timeout_action ).to eq( :retry )
109
+ task_class.timeout_action( :retry )
110
+ expect( task_class.timeout_action ).to eq( :retry )
88
111
  end
89
112
 
90
113
 
91
114
  it "provides a default name for its queue based on its name" do
92
- expect( @task_class.queue_name ).to eq( 'acme.testingtask' )
115
+ expect( task_class.queue_name ).to eq( 'acme.testingtask' )
93
116
  end
94
117
 
95
118
 
96
119
  it "can declare a pattern to use when subscribing" do
97
- @task_class.subscribe_to( 'foo.test' )
98
- expect( @task_class.routing_keys ).to include( 'foo.test' )
120
+ task_class.subscribe_to( 'foo.test' )
121
+ expect( task_class.routing_keys ).to include( 'foo.test' )
99
122
  end
100
123
 
101
124
 
102
125
  it "has acknowledgements enabled by default" do
103
- expect( @task_class.acknowledge ).to eq( true )
126
+ expect( task_class.acknowledge ).to eq( true )
104
127
  end
105
128
 
106
129
 
107
130
  it "can enable acknowledgements" do
108
- @task_class.acknowledge( true )
109
- expect( @task_class.acknowledge ).to eq( true )
131
+ task_class.acknowledge( true )
132
+ expect( task_class.acknowledge ).to eq( true )
110
133
  end
111
134
 
112
135
 
113
136
  it "can disable acknowledgements" do
114
- @task_class.acknowledge( false )
115
- expect( @task_class.acknowledge ).to eq( false )
137
+ task_class.acknowledge( false )
138
+ expect( task_class.acknowledge ).to eq( false )
116
139
  end
117
140
 
118
141
 
119
142
  it "can set a timeout" do
120
- @task_class.timeout( 10 )
121
- expect( @task_class.timeout ).to eq( 10 )
143
+ task_class.timeout( 10 )
144
+ expect( task_class.timeout ).to eq( 10 )
122
145
  end
123
146
 
124
147
 
125
148
  it "can declare a one-shot work model" do
126
- @task_class.work_model( :oneshot )
127
- expect( @task_class.work_model ).to eq( :oneshot )
149
+ task_class.work_model( :oneshot )
150
+ expect( task_class.work_model ).to eq( :oneshot )
128
151
  end
129
152
 
130
153
 
131
154
  it "can declare a long-lived work model" do
132
- @task_class.work_model( :longlived )
133
- expect( @task_class.work_model ).to eq( :longlived )
155
+ task_class.work_model( :longlived )
156
+ expect( task_class.work_model ).to eq( :longlived )
134
157
  end
135
158
 
136
159
 
137
160
  it "raises an error if an invalid work model is declared " do
138
161
  expect {
139
- @task_class.work_model( :lazy )
162
+ task_class.work_model( :lazy )
140
163
  }.to raise_error( /unknown work_model/i )
141
164
  end
142
165
 
143
166
 
167
+ context "an instance" do
168
+
169
+ let( :task_class ) do
170
+ Class.new( described_class ) do
171
+ def initialize( * )
172
+ super
173
+ @received_messages = []
174
+ end
175
+ attr_reader :received_messages
176
+
177
+ def work( payload, metadata )
178
+ self.received_messages << [ payload, metadata ]
179
+ true
180
+ end
181
+ end
182
+ end
183
+
184
+ let( :task ) { task_class.new(queue) }
185
+
186
+
187
+ it "handles received messages by calling its work method" do
188
+ expect( queue ).to receive( :wait_for_message ) do |oneshot, &callback|
189
+ callback.call( serialized_payload, metadata )
190
+ end
191
+
192
+ task.start_handling_messages
193
+
194
+ expect( task.received_messages ).to eq([ [payload, metadata] ])
195
+ end
196
+
197
+
198
+ end
199
+
200
+
201
+ context "an instance with a timeout" do
202
+
203
+ let( :task_class ) do
204
+ Class.new( described_class ) do
205
+ timeout 0.2
206
+ def initialize( * )
207
+ super
208
+ @received_messages = []
209
+ @sleeptime = 0
210
+ end
211
+ attr_reader :received_messages
212
+ attr_accessor :sleeptime
213
+
214
+ def work( payload, metadata )
215
+ self.received_messages << [ payload, metadata ]
216
+ sleep( self.sleeptime )
217
+ true
218
+ end
219
+ end
220
+ end
221
+
222
+ let( :task ) { task_class.new(queue) }
223
+
224
+
225
+ it "returns true if the work completes before the timeout" do
226
+ task.sleeptime = 0
227
+ expect( task.start_handling_messages ).to be_truthy
228
+ end
229
+
230
+
231
+ it "raises a Timeout::Error if the work takes longer than the timeout" do
232
+ task.sleeptime = task_class.timeout + 2
233
+ expect {
234
+ task.start_handling_messages
235
+ }.to raise_error( Timeout::Error, /execution expired/ )
236
+ end
237
+
238
+
239
+ it "returns false if the work takes longer than the timeout and the timeout_action is set to :retry" do
240
+ task_class.timeout_action( :retry )
241
+ task.sleeptime = task_class.timeout + 2
242
+ expect( task.start_handling_messages ).to be_falsey
243
+ end
244
+
245
+ end
246
+
247
+
248
+
249
+ context "an instance with no #work method" do
250
+
251
+ let( :task ) { task_class.new(queue) }
252
+
253
+ it "raises an exception when told to do work" do
254
+ expect {
255
+ task.work( 'payload', {} )
256
+ }.to raise_error( NotImplementedError, /#work/ )
257
+ end
258
+
259
+ end
260
+
261
+
262
+
144
263
  end
145
264
 
265
+
266
+
146
267
  end
147
268
 
@@ -8,7 +8,62 @@ require 'symphony'
8
8
 
9
9
  describe Symphony do
10
10
 
11
+ before( :each ) do
12
+ ENV.delete( 'SYMPHONY_CONFIG' )
13
+ end
11
14
 
12
15
 
16
+ it "will load a default config file if none is specified" do
17
+ config_object = double( "Configurability::Config object" )
18
+ expect( Configurability ).to receive( :gather_defaults ).
19
+ and_return( {} )
20
+ expect( Configurability::Config ).to receive( :load ).
21
+ with( described_class::DEFAULT_CONFIG_FILE, {} ).
22
+ and_return( config_object )
23
+ expect( config_object ).to receive( :install )
24
+
25
+ described_class.load_config
26
+ end
27
+
28
+
29
+ it "will load a config file given in an environment variable if none is specified" do
30
+ ENV['SYMPHONY_CONFIG'] = '/usr/local/etc/config.yml'
31
+
32
+ config_object = double( "Configurability::Config object" )
33
+ expect( Configurability ).to receive( :gather_defaults ).
34
+ and_return( {} )
35
+ expect( Configurability::Config ).to receive( :load ).
36
+ with( '/usr/local/etc/config.yml', {} ).
37
+ and_return( config_object )
38
+ expect( config_object ).to receive( :install )
39
+
40
+ described_class.load_config
41
+ end
42
+
43
+
44
+ it "will load a config file and install it if one is given" do
45
+ config_object = double( "Configurability::Config object" )
46
+ expect( Configurability ).to receive( :gather_defaults ).
47
+ and_return( {} )
48
+ expect( Configurability::Config ).to receive( :load ).
49
+ with( 'a/configfile.yml', {} ).
50
+ and_return( config_object )
51
+ expect( config_object ).to receive( :install )
52
+
53
+ described_class.load_config( 'a/configfile.yml' )
54
+ end
55
+
56
+
57
+ it "will override default values when loading the config if they're given" do
58
+ config_object = double( "Configurability::Config object" )
59
+ expect( Configurability ).to_not receive( :gather_defaults )
60
+ expect( Configurability::Config ).to receive( :load ).
61
+ with( 'a/different/configfile.yml', {database: {dbname: 'test'}} ).
62
+ and_return( config_object )
63
+ expect( config_object ).to receive( :install )
64
+
65
+ described_class.load_config( 'a/different/configfile.yml', database: {dbname: 'test'} )
66
+ end
67
+
13
68
  end
14
69
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: symphony
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0.pre20140327204419
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Granger
@@ -271,6 +271,7 @@ extra_rdoc_files:
271
271
  - TODO.md
272
272
  - USAGE.rdoc
273
273
  files:
274
+ - .gemtest
274
275
  - .simplecov
275
276
  - ChangeLog
276
277
  - History.rdoc
@@ -297,6 +298,7 @@ files:
297
298
  - lib/symphony/tasks/ssh.rb
298
299
  - lib/symphony/tasks/sshscript.rb
299
300
  - spec/helpers.rb
301
+ - spec/symphony/daemon_spec.rb
300
302
  - spec/symphony/mixins_spec.rb
301
303
  - spec/symphony/queue_spec.rb
302
304
  - spec/symphony/task_spec.rb
@@ -320,9 +322,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
320
322
  version: 2.0.0
321
323
  required_rubygems_version: !ruby/object:Gem::Requirement
322
324
  requirements:
323
- - - '>'
325
+ - - '>='
324
326
  - !ruby/object:Gem::Version
325
- version: 1.3.1
327
+ version: 2.0.3
326
328
  requirements: []
327
329
  rubyforge_project: symphony
328
330
  rubygems_version: 2.2.2
metadata.gz.sig CHANGED
Binary file