symphony 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/ChangeLog +91 -3
- data/History.rdoc +13 -0
- data/Manifest.txt +10 -1
- data/README.rdoc +1 -1
- data/Rakefile +5 -5
- data/UPGRADING.md +38 -0
- data/USAGE.rdoc +1 -1
- data/lib/symphony.rb +86 -5
- data/lib/symphony/daemon.rb +94 -147
- data/lib/symphony/queue.rb +11 -4
- data/lib/symphony/routing.rb +5 -4
- data/lib/symphony/signal_handling.rb +1 -2
- data/lib/symphony/statistics.rb +96 -0
- data/lib/symphony/task.rb +71 -11
- data/lib/symphony/task_group.rb +165 -0
- data/lib/symphony/task_group/longlived.rb +98 -0
- data/lib/symphony/task_group/oneshot.rb +25 -0
- data/lib/symphony/tasks/oneshot_simulator.rb +61 -0
- data/lib/symphony/tasks/simulator.rb +22 -18
- data/spec/helpers.rb +67 -1
- data/spec/symphony/daemon_spec.rb +83 -31
- data/spec/symphony/queue_spec.rb +2 -2
- data/spec/symphony/routing_spec.rb +297 -0
- data/spec/symphony/statistics_spec.rb +71 -0
- data/spec/symphony/task_group/longlived_spec.rb +219 -0
- data/spec/symphony/task_group/oneshot_spec.rb +56 -0
- data/spec/symphony/task_group_spec.rb +93 -0
- data/spec/symphony_spec.rb +6 -0
- metadata +41 -18
- metadata.gz.sig +0 -0
- data/TODO.md +0 -3
@@ -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
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
|
data/spec/helpers.rb
CHANGED
@@ -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
|
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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|