symphony 0.8.0 → 0.9.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.
@@ -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