collective 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.autotest +13 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +16 -0
  5. data/Guardfile +6 -0
  6. data/README +7 -0
  7. data/Rakefile +11 -0
  8. data/bin/collective +37 -0
  9. data/collective.gemspec +23 -0
  10. data/demo/demo +36 -0
  11. data/demo/demo.rb +30 -0
  12. data/demo/demo3 +36 -0
  13. data/demo/job1.rb +31 -0
  14. data/demo/job2.rb +42 -0
  15. data/demo/job3.rb +44 -0
  16. data/demo/populate.rb +22 -0
  17. data/lib/collective.rb +52 -0
  18. data/lib/collective/checker.rb +51 -0
  19. data/lib/collective/configuration.rb +219 -0
  20. data/lib/collective/idler.rb +81 -0
  21. data/lib/collective/key.rb +48 -0
  22. data/lib/collective/lifecycle_observer.rb +25 -0
  23. data/lib/collective/log.rb +29 -0
  24. data/lib/collective/messager.rb +218 -0
  25. data/lib/collective/mocks/storage.rb +108 -0
  26. data/lib/collective/monitor.rb +58 -0
  27. data/lib/collective/policy.rb +60 -0
  28. data/lib/collective/pool.rb +180 -0
  29. data/lib/collective/redis/storage.rb +142 -0
  30. data/lib/collective/registry.rb +123 -0
  31. data/lib/collective/squiggly.rb +20 -0
  32. data/lib/collective/utilities/airbrake_observer.rb +26 -0
  33. data/lib/collective/utilities/hoptoad_observer.rb +26 -0
  34. data/lib/collective/utilities/log_observer.rb +40 -0
  35. data/lib/collective/utilities/observeable.rb +18 -0
  36. data/lib/collective/utilities/observer_base.rb +59 -0
  37. data/lib/collective/utilities/process.rb +82 -0
  38. data/lib/collective/utilities/signal_hook.rb +47 -0
  39. data/lib/collective/utilities/storage_base.rb +41 -0
  40. data/lib/collective/version.rb +3 -0
  41. data/lib/collective/worker.rb +161 -0
  42. data/spec/checker_spec.rb +20 -0
  43. data/spec/configuration_spec.rb +24 -0
  44. data/spec/helper.rb +33 -0
  45. data/spec/idler_spec.rb +58 -0
  46. data/spec/key_spec.rb +41 -0
  47. data/spec/messager_spec.rb +131 -0
  48. data/spec/mocks/storage_spec.rb +108 -0
  49. data/spec/monitor_spec.rb +15 -0
  50. data/spec/policy_spec.rb +43 -0
  51. data/spec/pool_spec.rb +119 -0
  52. data/spec/redis/storage_spec.rb +133 -0
  53. data/spec/registry_spec.rb +52 -0
  54. data/spec/support/jobs.rb +58 -0
  55. data/spec/support/redis.rb +22 -0
  56. data/spec/support/timing.rb +32 -0
  57. data/spec/utilities/observer_base_spec.rb +50 -0
  58. data/spec/utilities/process_spec.rb +17 -0
  59. data/spec/worker_spec.rb +121 -0
  60. data/unused/times.rb +45 -0
  61. 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