collective 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +13 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Guardfile +6 -0
- data/README +7 -0
- data/Rakefile +11 -0
- data/bin/collective +37 -0
- data/collective.gemspec +23 -0
- data/demo/demo +36 -0
- data/demo/demo.rb +30 -0
- data/demo/demo3 +36 -0
- data/demo/job1.rb +31 -0
- data/demo/job2.rb +42 -0
- data/demo/job3.rb +44 -0
- data/demo/populate.rb +22 -0
- data/lib/collective.rb +52 -0
- data/lib/collective/checker.rb +51 -0
- data/lib/collective/configuration.rb +219 -0
- data/lib/collective/idler.rb +81 -0
- data/lib/collective/key.rb +48 -0
- data/lib/collective/lifecycle_observer.rb +25 -0
- data/lib/collective/log.rb +29 -0
- data/lib/collective/messager.rb +218 -0
- data/lib/collective/mocks/storage.rb +108 -0
- data/lib/collective/monitor.rb +58 -0
- data/lib/collective/policy.rb +60 -0
- data/lib/collective/pool.rb +180 -0
- data/lib/collective/redis/storage.rb +142 -0
- data/lib/collective/registry.rb +123 -0
- data/lib/collective/squiggly.rb +20 -0
- data/lib/collective/utilities/airbrake_observer.rb +26 -0
- data/lib/collective/utilities/hoptoad_observer.rb +26 -0
- data/lib/collective/utilities/log_observer.rb +40 -0
- data/lib/collective/utilities/observeable.rb +18 -0
- data/lib/collective/utilities/observer_base.rb +59 -0
- data/lib/collective/utilities/process.rb +82 -0
- data/lib/collective/utilities/signal_hook.rb +47 -0
- data/lib/collective/utilities/storage_base.rb +41 -0
- data/lib/collective/version.rb +3 -0
- data/lib/collective/worker.rb +161 -0
- data/spec/checker_spec.rb +20 -0
- data/spec/configuration_spec.rb +24 -0
- data/spec/helper.rb +33 -0
- data/spec/idler_spec.rb +58 -0
- data/spec/key_spec.rb +41 -0
- data/spec/messager_spec.rb +131 -0
- data/spec/mocks/storage_spec.rb +108 -0
- data/spec/monitor_spec.rb +15 -0
- data/spec/policy_spec.rb +43 -0
- data/spec/pool_spec.rb +119 -0
- data/spec/redis/storage_spec.rb +133 -0
- data/spec/registry_spec.rb +52 -0
- data/spec/support/jobs.rb +58 -0
- data/spec/support/redis.rb +22 -0
- data/spec/support/timing.rb +32 -0
- data/spec/utilities/observer_base_spec.rb +50 -0
- data/spec/utilities/process_spec.rb +17 -0
- data/spec/worker_spec.rb +121 -0
- data/unused/times.rb +45 -0
- metadata +148 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
=begin
|
6
|
+
|
7
|
+
A registry knows how to lookup up and register workers.
|
8
|
+
|
9
|
+
=end
|
10
|
+
|
11
|
+
class Collective::Registry
|
12
|
+
|
13
|
+
attr :name
|
14
|
+
attr :storage
|
15
|
+
|
16
|
+
def initialize( name, storage )
|
17
|
+
@name = name or raise
|
18
|
+
@storage = storage or raise
|
19
|
+
|
20
|
+
# type checking
|
21
|
+
name.encoding
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def reconnect_after_fork
|
26
|
+
@storage.reconnect_after_fork
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def register( key )
|
31
|
+
key = key.to_s
|
32
|
+
storage.set_add( workers_key, key )
|
33
|
+
storage.put( status_key(key), Time.now.to_i )
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def update( key )
|
38
|
+
key = key.to_s
|
39
|
+
storage.set_add( workers_key, key ) if ! storage.set_member?( workers_key, key )
|
40
|
+
storage.put( status_key(key), Time.now.to_i )
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def unregister( key )
|
45
|
+
key = key.to_s
|
46
|
+
storage.del( status_key(key) )
|
47
|
+
storage.set_remove( workers_key, key )
|
48
|
+
end
|
49
|
+
|
50
|
+
# ----------------------------------------------------------------------------
|
51
|
+
# Query API
|
52
|
+
# ----------------------------------------------------------------------------
|
53
|
+
|
54
|
+
# @returns an array of key strings
|
55
|
+
# NOTICE this will include keys for workers on all hosts
|
56
|
+
def workers
|
57
|
+
all = storage.set_get_all( workers_key )
|
58
|
+
raise "Not a Set: #{workers_key} (#{all.class})" unless all.kind_of?(Array)
|
59
|
+
all.map { |key_string| Collective::Key.parse(key_string) }
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def checked_workers( policy )
|
64
|
+
groups = { live: [], late: [], hung: [], dead: [] }
|
65
|
+
check_workers(policy) do |key, status|
|
66
|
+
groups[status] << key
|
67
|
+
end
|
68
|
+
OpenStruct.new(groups)
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# This method can be slow so it takes a block for incremental processing.
|
73
|
+
# @param block takes entry, status in [ :live, :late, :hung, :dead ]
|
74
|
+
# @param options[:all] = true to get keys across all hosts
|
75
|
+
def check_workers( policy, options = nil, &block )
|
76
|
+
workers.each do |key|
|
77
|
+
heartbeat = storage.get( status_key(key.to_s) ).to_i
|
78
|
+
status = heartbeat_status( policy, heartbeat )
|
79
|
+
yield( key, status )
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# ----------------------------------------------------------------------------
|
84
|
+
protected
|
85
|
+
# ----------------------------------------------------------------------------
|
86
|
+
|
87
|
+
def heartbeat_status( policy, heartbeat )
|
88
|
+
if heartbeat > 0 then
|
89
|
+
age = now - heartbeat.to_i
|
90
|
+
if age >= policy.worker_hung then
|
91
|
+
:hung
|
92
|
+
elsif age >= policy.worker_late
|
93
|
+
:late
|
94
|
+
else
|
95
|
+
:live
|
96
|
+
end
|
97
|
+
else
|
98
|
+
:dead
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# easier to test if we can stub Time.now
|
104
|
+
def now
|
105
|
+
Time.now.to_i
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def policy
|
110
|
+
@policy ||= Collective::Policy.resolve
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def workers_key
|
115
|
+
@workers_key ||= "collective:#{name}:workers"
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
def status_key( key )
|
120
|
+
"collective:#{name}:worker:#{key}"
|
121
|
+
end
|
122
|
+
|
123
|
+
end # Collective::Registry
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Squiggly
|
4
|
+
|
5
|
+
ADJECTIVES = ["able", "afraid", "bad", "bitter", "brave", "bright", "busy", "careful", "cheap", "clean", "clear", "clever", "close", "cloudy", "cold", "comfortable", "cool", "cute", "dangerous", "dapper", "dark", "dead", "deep", "difficult", "dirty", "dry", "early", "empty", "exciting", "expensive", "fair", "famous", "far", "fast", "fat", "fine", "flat", "free", "free", "free", "fresh", "full", "funny", "good", "great", "happy", "hard", "healthy", "heavy", "high", "hungry", "important", "interesting", "kind", "large", "late", "lazy", "light", "long", "loud", "low", "lucky", "narrow", "near", "noisy", "old", "polite", "proud", "quick", "quiet", "rich", "sad", "safe", "salty", "scared", "short", "slimey", "slow", "small", "soft", "sour", "strong", "strong", "sweet", "thick", "thirsty", "tidy", "useful", "warm", "weak", "weak", "whole", "windy"]
|
6
|
+
ANIMALS = ["aardvarks", "ants", "badgers", "bats", "bears", "bees", "butterflies", "canaries", "cattle", "chickens", "chihuahuas", "clams", "cockles", "crabs", "crows", "deer", "dogs", "donkeys", "doves", "dragonflies", "ducks", "ferrets", "flies", "foxes", "frogs", "geese", "gerbils", "goats", "guinea pigs", "hamsters", "hares", "hawks", "hedgehogs", "herons", "horses", "hummingbirds", "kingfishers", "lobsters", "mice", "moles", "moths", "mussles", "newts", "otters", "owls", "oysters", "parrots", "peafowl", "pheasants", "pigeons", "pigs", "pikes", "platypuses", "rabbits", "rats", "robins", "rooks", "salmons", "sheep", "snails", "snakes", "sparrows", "spiders", "squid", "squirrels", "starlings", "stoats", "swans", "toads", "trouts", "wasps", "weasels"]
|
7
|
+
VERBS = ["cried", "cuddled", "danced", "drove", "engaged", "felt", "floundered", "fought", "hid", "hopped", "hugged", "jumped", "kissed", "knitted", "listened", "loitered", "married", "played", "played", "ran", "sang", "sat", "snuggled", "talked", "walked", "went", "whimpered", "whimpered", "whispered"]
|
8
|
+
ADVERBS = ["cordially", "easily", "jovially", "merrily", "quickly"]
|
9
|
+
|
10
|
+
def sentence
|
11
|
+
count = rand(8) + 2
|
12
|
+
[ count, ADJECTIVES.sample, ANIMALS.sample, VERBS.sample, ADVERBS.sample ].join(" ")
|
13
|
+
end
|
14
|
+
|
15
|
+
def subject
|
16
|
+
[ ADJECTIVES.sample, ANIMALS.sample ].join(" ")
|
17
|
+
end
|
18
|
+
|
19
|
+
extend Squiggly
|
20
|
+
end # Squiggly
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Wrapps a callable and issues feedback.
|
6
|
+
- Error
|
7
|
+
|
8
|
+
=end
|
9
|
+
|
10
|
+
require "airbrake"
|
11
|
+
|
12
|
+
class Collective::Utilities::AirbrakeObserver < Collective::Utilities::ObserverBase
|
13
|
+
|
14
|
+
attr :it # job
|
15
|
+
attr :me # worker
|
16
|
+
|
17
|
+
def initialize( observed, callable = nil, &callable_block )
|
18
|
+
@it = callable || callable_block
|
19
|
+
@me = observed
|
20
|
+
end
|
21
|
+
|
22
|
+
def job_error(x)
|
23
|
+
Airbrake.notify(x)
|
24
|
+
end
|
25
|
+
|
26
|
+
end # Collective::Utilities::AirbrakeObserver
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Wrapps a callable and issues feedback.
|
6
|
+
- Error
|
7
|
+
|
8
|
+
=end
|
9
|
+
|
10
|
+
require "hoptoad_notifier"
|
11
|
+
|
12
|
+
class Collective::Utilities::HoptoadObserver < Collective::Utilities::ObserverBase
|
13
|
+
|
14
|
+
attr :it # job
|
15
|
+
attr :me # worker
|
16
|
+
|
17
|
+
def initialize( observed, callable = nil, &callable_block )
|
18
|
+
@it = callable || callable_block
|
19
|
+
@me = observed
|
20
|
+
end
|
21
|
+
|
22
|
+
def job_error(x)
|
23
|
+
HoptoadNotifier.notify(x)
|
24
|
+
end
|
25
|
+
|
26
|
+
end # Collective::Utilities::HoptoadObserver
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Wrapps a callable and issues feedback.
|
6
|
+
- Started
|
7
|
+
- Hearbeat
|
8
|
+
- Error
|
9
|
+
- Stopped
|
10
|
+
|
11
|
+
=end
|
12
|
+
|
13
|
+
class Collective::Utilities::LogObserver < Collective::Utilities::ObserverBase
|
14
|
+
|
15
|
+
include Collective::Log
|
16
|
+
|
17
|
+
def initialize( filename = nil )
|
18
|
+
if filename then
|
19
|
+
self.logger = File.open(filename,"a") # append or create, write only
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def worker_started( *args )
|
24
|
+
log "#{subject} has started"
|
25
|
+
end
|
26
|
+
|
27
|
+
def worker_heartbeat( *args )
|
28
|
+
log "#{subject} is still alive"
|
29
|
+
end
|
30
|
+
|
31
|
+
def job_error(x)
|
32
|
+
log "Warning: #{subject} experienced a job failure due to an error:#{x.inspect}"
|
33
|
+
log x.backtrace
|
34
|
+
end
|
35
|
+
|
36
|
+
def worker_stopped( *args )
|
37
|
+
log "#{subject} has stopped"
|
38
|
+
end
|
39
|
+
|
40
|
+
end # Collective::Utilities::LogObserver
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Collective::Utilities::Observeable
|
4
|
+
|
5
|
+
def add_observer(o)
|
6
|
+
o.focus(self)
|
7
|
+
(@observers ||= []).push(o)
|
8
|
+
end
|
9
|
+
|
10
|
+
def notify( *details )
|
11
|
+
if @observers then
|
12
|
+
@observers.each do |observer|
|
13
|
+
observer.notify( self, *details )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
class Collective::Utilities::ObserverBase
|
4
|
+
|
5
|
+
attr :subject
|
6
|
+
|
7
|
+
# can implement notify( subject, *details )
|
8
|
+
# or can use this implementation
|
9
|
+
|
10
|
+
# It is possible but unlikely that I would want an observer to observe multiple subjects.
|
11
|
+
def focus( subject )
|
12
|
+
@subject = subject
|
13
|
+
end
|
14
|
+
|
15
|
+
def notify( subject, *details )
|
16
|
+
if self.respond_to?(details.first) then
|
17
|
+
details = details.dup
|
18
|
+
self.send( details.shift, *details )
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# factory_or_observer can be something which responds to #notify
|
23
|
+
# or a block which responds to #call and can return a factory_or_observer
|
24
|
+
# or a class which can be instantiated
|
25
|
+
# or a string which can be resolved to a class
|
26
|
+
# or an array which can be resolved to a class with parameters
|
27
|
+
def self.resolve( factory_or_observer, *args )
|
28
|
+
case
|
29
|
+
when factory_or_observer.respond_to?(:notify)
|
30
|
+
factory_or_observer
|
31
|
+
when factory_or_observer.respond_to?(:call)
|
32
|
+
resolve(factory_or_observer.call(*args))
|
33
|
+
else
|
34
|
+
case factory_or_observer
|
35
|
+
when :airbrake
|
36
|
+
resolve(Collective::Utilities::AirbrakeObserver,*args)
|
37
|
+
when :hoptoad
|
38
|
+
resolve(Collective::Utilities::HoptoadObserver,*args)
|
39
|
+
when :log
|
40
|
+
resolve(Collective::Utilities::LogObserver,*args)
|
41
|
+
when Class
|
42
|
+
factory_or_observer.new(*args)
|
43
|
+
when String
|
44
|
+
resolve(Collective.resolve_class(factory_or_observer.to_s),*args)
|
45
|
+
when Array
|
46
|
+
args = factory_or_observer.dup
|
47
|
+
fobs = args.shift
|
48
|
+
factory = resolve( fobs, *args )
|
49
|
+
else
|
50
|
+
return factory_or_observer # assume it supports the notifications natively
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.camelize(s)
|
56
|
+
s.to_s.gsub(/(?:^|_|\s)(.)/) { $1.upcase }
|
57
|
+
end
|
58
|
+
|
59
|
+
end # Collective::Utilities::ObserverBase
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module Collective::Utilities::Process
|
4
|
+
|
5
|
+
def wait_until_deadline( pid, deadline )
|
6
|
+
status = nil
|
7
|
+
interval = 0.125
|
8
|
+
begin # execute at least once to get status
|
9
|
+
dummy, status = wait2_now(pid)
|
10
|
+
break if status
|
11
|
+
#log "Waiting for #{pid}", "Sleeping #{interval}" if false
|
12
|
+
sleep(interval)
|
13
|
+
interval *= 2 if interval < 1.0
|
14
|
+
end while Time.now.to_f < deadline
|
15
|
+
status
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def wait_and_terminate( pid, options = {} )
|
20
|
+
#log "Monitoring job #{pid}"
|
21
|
+
timeout = options[:timeout] || 1024
|
22
|
+
signal = options[:signal] || "TERM"
|
23
|
+
|
24
|
+
status = wait_until_deadline( pid, Time.now.to_f + timeout )
|
25
|
+
return status if status
|
26
|
+
|
27
|
+
#log "Job #{pid} is overdue, killing"
|
28
|
+
|
29
|
+
::Process.kill( signal, pid )
|
30
|
+
status = wait_until_deadline( pid, Time.now.to_f + 1 )
|
31
|
+
return status if status
|
32
|
+
|
33
|
+
::Process.kill( "KILL", pid ) if ! status
|
34
|
+
dummy, status = wait2_now(pid)
|
35
|
+
status
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def fork_and_detach( options = {}, &action)
|
40
|
+
# Fork twice. First child doesn't matter. The second is our favorite.
|
41
|
+
pid1 = fork do
|
42
|
+
::Process.setsid
|
43
|
+
exit if fork
|
44
|
+
redirect_stdio( options[:stdout] )
|
45
|
+
action.call
|
46
|
+
end
|
47
|
+
|
48
|
+
# We must call waitpid on the first child to keep it from turning into a zombie
|
49
|
+
::Process.waitpid(pid1)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def wait2_now( pid )
|
54
|
+
::Process.wait2( pid, ::Process::WNOHANG )
|
55
|
+
rescue Errno::ECHILD # No child processes
|
56
|
+
return [ nil, 0 ]
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def redirect_stdio( stdout )
|
61
|
+
STDIN.reopen "/dev/null"
|
62
|
+
if stdout then
|
63
|
+
mask = File.umask(0000)
|
64
|
+
file = File.new( stdout, "a" ) # append or create, write only
|
65
|
+
File.umask( mask )
|
66
|
+
STDOUT.reopen( file )
|
67
|
+
else
|
68
|
+
STDOUT.reopen "/dev/null"
|
69
|
+
end
|
70
|
+
STDERR.reopen(STDOUT)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def alive?( pid )
|
75
|
+
!! ::Process.kill( 0, pid )
|
76
|
+
rescue Errno::ESRCH
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
extend Collective::Utilities::Process
|
81
|
+
|
82
|
+
end # Collective::Utilities::Process
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
=begin
|
4
|
+
Allows you to add hooks without worrying about restoring the chain when you are done.
|
5
|
+
Hooks can be nested.
|
6
|
+
Only the most deeply nested handler will be called.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
hook = SignalHook.trap("TERM") { ... my term handler }
|
10
|
+
hook.attempt do
|
11
|
+
... my long action
|
12
|
+
end
|
13
|
+
# at this point, term handler has been removed from chain
|
14
|
+
|
15
|
+
or
|
16
|
+
SignalHook.trap("QUIT") { foo.quit! }.attempt { foo.run }
|
17
|
+
=end
|
18
|
+
|
19
|
+
class Collective::SignalHook
|
20
|
+
|
21
|
+
attr_writer :local, :chain
|
22
|
+
|
23
|
+
def trigger
|
24
|
+
if @local then
|
25
|
+
@local.call
|
26
|
+
elsif @chain
|
27
|
+
@chain.call
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def attempt( &block )
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
@local = false
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
def trap( signal, &block )
|
39
|
+
hook = new
|
40
|
+
hook.local = block
|
41
|
+
previous = Signal.trap( signal ) { hook.trigger }
|
42
|
+
hook.chain = previous if previous && previous.kind_of?(Proc)
|
43
|
+
hook
|
44
|
+
end # trap
|
45
|
+
end
|
46
|
+
|
47
|
+
end # Collective::SignalHook
|