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.
- 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
|
|