symphony 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,98 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'set'
5
+ require 'symphony/task_group' unless defined?( Symphony::TaskGroup )
6
+ require 'symphony/statistics'
7
+
8
+
9
+ # A task group for the 'longlived' work model.
10
+ class Symphony::TaskGroup::LongLived < Symphony::TaskGroup
11
+ include Symphony::Statistics
12
+
13
+
14
+ ### Create a LongLived task group for the specified +task_class+ that will
15
+ ### run a maximum of +max_workers+.
16
+ def initialize( task_class, max_workers )
17
+ super
18
+
19
+ @queue = nil
20
+ @pids = Set.new
21
+ @started_one_worker = false
22
+ end
23
+
24
+
25
+ ######
26
+ public
27
+ ######
28
+
29
+ # The PIDs of the child this task group manages
30
+ attr_reader :pids
31
+
32
+
33
+ ### Return +true+ if the task group should scale up by one.
34
+ def needs_a_worker?
35
+ return true unless self.started_one_worker?
36
+ return false unless @queue
37
+ return false if @queue.consumer_count >= self.max_workers
38
+ return self.sample_values_increasing?
39
+ end
40
+
41
+
42
+ ### Returns +true+ if the group has started at least one worker. Used to avoid
43
+ ### racing to start workers when one worker has started, but we haven't yet connected
44
+ ### to AMQP to get consumer count yet.
45
+ def started_one_worker?
46
+ return @started_one_worker
47
+ end
48
+
49
+
50
+ ### If the number of workers is not at the maximum, start some.
51
+ def adjust_workers
52
+ self.sample_queue_status
53
+
54
+ return nil if self.throttled?
55
+
56
+ if self.needs_a_worker?
57
+ self.log.info "Too few workers for (%s); spinning one up." % [ self.task_class.name ]
58
+ pid = self.start_worker( @started_one_worker )
59
+ self.pids.add( pid )
60
+ return [ pid ]
61
+ end
62
+
63
+ return nil
64
+ end
65
+
66
+
67
+ ### Add the current number of workers to the samples.
68
+ def sample_queue_status
69
+ return unless @queue
70
+ self.add_sample( @queue.message_count )
71
+ end
72
+
73
+
74
+ ### Overridden to grab a Bunny::Queue for monitoring when the first
75
+ ### worker starts.
76
+ def start_worker( exit_on_idle=false )
77
+ @started_one_worker = true
78
+
79
+ pid = super
80
+ self.log.info "Start a new worker at pid %d" % [ pid ]
81
+
82
+ unless @queue
83
+ begin
84
+ channel = Symphony::Queue.amqp_channel
85
+ @queue = channel.queue( self.task_class.queue_name, passive: true, prefetch: 0 )
86
+ self.log.debug " got the 0-prefetch queue"
87
+ rescue Bunny::NotFound => err
88
+ self.log.info "Child hasn't created the queue yet; deferring"
89
+ Symphony::Queue.reset
90
+ end
91
+ end
92
+
93
+ return pid
94
+ end
95
+
96
+ end # class Symphony::TaskGroup::LongLived
97
+
98
+
@@ -0,0 +1,25 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'symphony/task_group' unless defined?( Symphony::TaskGroup )
5
+
6
+
7
+ # A task group for the 'oneshot' work model.
8
+ class Symphony::TaskGroup::Oneshot < Symphony::TaskGroup
9
+
10
+ ### If the number of workers is not at the maximum, start some.
11
+ def adjust_workers
12
+ return nil if self.throttled?
13
+
14
+ missing_workers = []
15
+ missing_count = self.max_workers - self.workers.size
16
+ missing_count.times do
17
+ missing_workers << self.start_worker
18
+ end
19
+
20
+ return missing_workers.empty? ? nil : missing_workers
21
+ end
22
+
23
+ end # class Symphony::TaskGroup::Oneshot
24
+
25
+
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'tmpdir'
5
+ require 'symphony/task' unless defined?( Symphony::Task )
6
+ require 'symphony/metrics'
7
+
8
+ # A spike to test out various task execution outcomes.
9
+ class OneshotSimulator < Symphony::Task
10
+ prepend Symphony::Metrics
11
+
12
+ # Simulate processing all events
13
+ subscribe_to '#'
14
+
15
+ # Fetch 100 events at a time
16
+ prefetch 100
17
+
18
+ # Only allow 2 seconds for work to complete before rejecting or retrying.
19
+ timeout 2.0, action: :retry
20
+
21
+ # Run once per job
22
+ work_model :oneshot
23
+
24
+
25
+
26
+ ######
27
+ public
28
+ ######
29
+
30
+ #
31
+ # Task API
32
+ #
33
+
34
+ # Do the ping.
35
+ def work( payload, metadata )
36
+ if metadata[:properties][:headers] &&
37
+ metadata[:properties][:headers]['x-death']
38
+ puts "Deaths! %p" % [ metadata[:properties][:headers]['x-death'] ]
39
+ end
40
+
41
+ val = Random.rand
42
+ case
43
+ when val < 0.33
44
+ $stderr.puts "Simulating an error in the task (reject)."
45
+ raise "OOOOOPS!"
46
+ when val < 0.66
47
+ $stderr.puts "Simulating a soft failure in the task (reject+requeue)."
48
+ return false
49
+ when val < 0.88
50
+ $stderr.puts "Simulating a timeout case"
51
+ sleep( self.class.timeout + 1 )
52
+ else
53
+ $stderr.puts "Simulating a successful task run (accept)"
54
+ puts( payload.inspect )
55
+ return true
56
+ end
57
+ end
58
+
59
+
60
+ end # class OneshotSimulator
61
+
@@ -13,10 +13,10 @@ class Simulator < Symphony::Task
13
13
  subscribe_to '#'
14
14
 
15
15
  # Fetch 100 events at a time
16
- prefetch 100
16
+ prefetch 10
17
17
 
18
18
  # Only allow 2 seconds for work to complete before rejecting or retrying.
19
- timeout 2.0, action: :retry
19
+ # timeout 2.0, action: :retry
20
20
 
21
21
 
22
22
  ######
@@ -34,22 +34,26 @@ class Simulator < Symphony::Task
34
34
  puts "Deaths! %p" % [ metadata[:properties][:headers]['x-death'] ]
35
35
  end
36
36
 
37
- val = Random.rand
38
- case
39
- when val < 0.33
40
- $stderr.puts "Simulating an error in the task (reject)."
41
- raise "OOOOOPS!"
42
- when val < 0.66
43
- $stderr.puts "Simulating a soft failure in the task (reject+requeue)."
44
- return false
45
- when val < 0.88
46
- $stderr.puts "Simulating a timeout case"
47
- sleep( self.class.timeout + 1 )
48
- else
49
- $stderr.puts "Simulating a successful task run (accept)"
50
- puts( payload.inspect )
51
- return true
52
- end
37
+ sleep rand( 0.0 .. 2.0 )
38
+
39
+ # val = Random.rand
40
+ # case
41
+ # when val < 0.33
42
+ # $stderr.puts "Simulating an error in the task (reject)."
43
+ # raise "OOOOOPS!"
44
+ # when val < 0.66
45
+ # $stderr.puts "Simulating a soft failure in the task (reject+requeue)."
46
+ # return false
47
+ # when val < 0.88
48
+ # $stderr.puts "Simulating a timeout case"
49
+ # sleep( self.class.timeout + 1 )
50
+ # else
51
+ # $stderr.puts "Simulating a successful task run (accept)"
52
+ # puts( payload.inspect )
53
+ # return true
54
+ # end
55
+
56
+ true
53
57
  end
54
58
 
55
59
 
@@ -8,14 +8,79 @@ require 'loggability'
8
8
  require 'loggability/spechelpers'
9
9
  require 'configurability'
10
10
  require 'configurability/behavior'
11
+ require 'timecop'
11
12
 
13
+ require 'symphony'
14
+ require 'symphony/task'
12
15
  require 'rspec'
13
16
 
14
17
  Loggability.format_with( :color ) if $stdout.tty?
15
18
 
16
19
 
17
20
  ### RSpec helper functions.
18
- module Loggability::SpecHelpers
21
+ module Symphony::SpecHelpers
22
+
23
+ class TestTask < Symphony::Task
24
+
25
+ # Don't ever really try to handle messages.
26
+ def start_handling_messages
27
+ end
28
+ end
29
+
30
+
31
+ class DummySession
32
+
33
+ class Queue
34
+ def initialize( channel )
35
+ @channel = channel
36
+ end
37
+ attr_reader :channel
38
+ def name
39
+ return 'dummy_queue_name'
40
+ end
41
+ def subscribe_with( * )
42
+ end
43
+ def message_count
44
+ return 1
45
+ end
46
+ end
47
+ class Channel
48
+ def initialize
49
+ @queue = nil
50
+ @exchange = nil
51
+ end
52
+ def queue( name, opts={} )
53
+ return @queue ||= DummySession::Queue.new( self )
54
+ end
55
+ def topic( * )
56
+ return @exchange ||= DummySession::Exchange.new
57
+ end
58
+ def prefetch( * )
59
+ end
60
+ def number
61
+ return 1
62
+ end
63
+ def close; end
64
+ end
65
+
66
+ class Exchange
67
+ end
68
+
69
+ def initialize
70
+ @channel = nil
71
+ end
72
+
73
+ def start
74
+ return true
75
+ end
76
+
77
+ def create_channel
78
+ return @channel ||= DummySession::Channel.new
79
+ end
80
+
81
+ def close; end
82
+ end
83
+
19
84
  end
20
85
 
21
86
 
@@ -30,6 +95,7 @@ RSpec.configure do |config|
30
95
  end
31
96
 
32
97
  config.include( Loggability::SpecHelpers )
98
+ config.include( Symphony::SpecHelpers )
33
99
  end
34
100
 
35
101
  # vim: set nosta noet ts=4 sw=4:
@@ -1,29 +1,42 @@
1
1
  # -*- ruby -*-
2
2
  #encoding: utf-8
3
+ # vim: set noet nosta sw=4 ts=4 :
4
+
3
5
 
4
6
  require_relative '../helpers'
5
7
 
6
8
  require 'symphony/daemon'
9
+ require 'symphony/task'
7
10
 
8
- class TestTask < Symphony::Task
9
-
10
- # Don't ever really try to handle messages.
11
- def start_handling_messages
12
- end
13
-
11
+ class Test1Task < Symphony::Task
12
+ subscribe_to '#'
13
+ end
14
+ class Test2Task < Symphony::Task
15
+ subscribe_to '#'
16
+ end
17
+ class Test3Task < Symphony::Task
18
+ subscribe_to '#'
14
19
  end
15
20
 
16
21
 
17
22
  describe Symphony::Daemon do
18
23
 
24
+ before( :all ) do
25
+ @pids = ( 200..65534 ).cycle
26
+ end
27
+
19
28
  before( :each ) do
20
- allow( Process ).to receive( :fork ).and_yield
29
+ allow( Process ).to receive( :fork ).and_yield.and_return( @pids.next )
21
30
  allow( Process ).to receive( :setpgid )
22
- Symphony::Daemon.configure( tasks: ['test', 'test'] )
31
+ allow( Process ).to receive( :kill )
32
+ Symphony.configure( tasks: ['test', 'test'] )
33
+
34
+ dummy_session = Symphony::SpecHelpers::DummySession.new
35
+ allow( Bunny::Session ).to receive( :new ).and_return( dummy_session )
23
36
  end
24
37
 
25
38
 
26
- let( :daemon ) { described_class.new }
39
+ let!( :daemon ) { described_class.new }
27
40
 
28
41
 
29
42
  it "can report what version it is" do
@@ -38,28 +51,8 @@ describe Symphony::Daemon do
38
51
  ).to match( /\(build \p{Xdigit}+\)/i )
39
52
  end
40
53
 
41
- it "loads a task class for each configured task" do
42
- expect( daemon.tasks.size ).to eq( 2 )
43
- expect( daemon.tasks ).to include( TestTask )
44
- end
45
-
46
- it "forks a child for each task" do
47
- expect( Process ).to receive( :fork ).twice.and_yield
48
- expect( TestTask ).to receive( :after_fork ).twice
49
- expect( TestTask ).to receive( :run ).and_return( 118, 119 ) # pids
50
-
51
- daemon.simulate_signal( :TERM )
52
-
53
- status = double( Process::Status, :success? => true )
54
- expect( Process ).to receive( :waitpid2 ).
55
- with( -1, Process::WNOHANG|Process::WUNTRACED ).
56
- and_return( [118, status], [119, status], nil )
57
-
58
- daemon.run_tasks
59
- end
60
-
61
54
  it "exits gracefully on one SIGINT" do
62
- daemon.tasks.clear
55
+ Symphony.tasks.clear
63
56
  thr = Thread.new { daemon.run_tasks }
64
57
  sleep 0.1 until daemon.running? || !thr.alive?
65
58
 
@@ -70,7 +63,7 @@ describe Symphony::Daemon do
70
63
  end
71
64
 
72
65
  it "exits gracefully on one SIGQUIT" do
73
- daemon.tasks.clear
66
+ Symphony.tasks.clear
74
67
  thr = Thread.new { daemon.run_tasks }
75
68
  sleep 0.1 until daemon.running? || !thr.alive?
76
69
 
@@ -80,5 +73,64 @@ describe Symphony::Daemon do
80
73
  }.to change { daemon.running? }.from( true ).to( false )
81
74
  end
82
75
 
76
+ it "re-reads its configuration on a SIGHUP" do
77
+ Symphony.tasks.clear
78
+ thr = Thread.new { daemon.run_tasks }
79
+ sleep 0.1 until daemon.running? || !thr.alive?
80
+
81
+ config = double( Configurability::Config )
82
+ expect( Symphony ).to receive( :config ).at_least( :once ).and_return( config )
83
+ expect( config ).to receive( :reload )
84
+
85
+ daemon.simulate_signal( :HUP )
86
+ daemon.stop
87
+ thr.join( 2 )
88
+ end
89
+
90
+ it "adjusts its tasks when its config is reloaded" do
91
+ config = Configurability.default_config
92
+ config.symphony.tasks = [ 'test1', 'test2' ]
93
+ # config.logging.__default__ = 'debug'
94
+ config.install
95
+
96
+ allow( Symphony::Task ).to receive( :exit )
97
+ allow( Process ).to receive( :kill ) do |sig, pid|
98
+ status = instance_double( Process::Status, :success? => true )
99
+ daemon.task_pids[ pid ].on_child_exit( pid, status )
100
+ daemon.task_pids.delete( pid )
101
+ end
102
+
103
+ begin
104
+ thr = Thread.new { daemon.run_tasks }
105
+ sleep 0.1 until daemon.running? || !thr.alive?
106
+
107
+ daemon.task_groups.each do |task_class, task_group|
108
+ case task_class
109
+ when Test1Task
110
+ expect( task_group ).to receive( :restart_workers ) do
111
+ daemon.task_pids.clear
112
+ end
113
+ when Test2Task
114
+ expect( task_group ).to receive( :stop_workers ) do
115
+ daemon.task_pids.clear
116
+ end
117
+ end
118
+ end
119
+
120
+ expect( config ).to receive( :reload ) do
121
+ config.symphony.tasks = [ 'test1', 'test3' ]
122
+ config.install
123
+ end
124
+
125
+ expect {
126
+ daemon.reload_config
127
+ }.to change { daemon.task_groups }
128
+
129
+ ensure
130
+ daemon.stop
131
+ thr.join( 2 )
132
+ end
133
+ end
134
+
83
135
  end
84
136