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,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class Collective::Utilities::StorageBase
4
+
5
+ module Resolver
6
+ # resolution is as so:
7
+ # nil, :mock => :mock
8
+ # :redis => redis://127.0.0.1:6379/1
9
+ # string => CLASS.new
10
+ # CLASS => CLASS.new
11
+ # PROC => yield (recursive)
12
+ # [ ARRAY ] => first, *args (recursive)
13
+ def resolve( storage, *args )
14
+ storage ||= :mock
15
+ case
16
+ when storage.respond_to?(:call)
17
+ resolve(storage.call(*args))
18
+ else
19
+ case storage
20
+ when :mock
21
+ resolve( Collective::Mocks::Storage, *args )
22
+ when :redis
23
+ resolve( Collective::Redis::Storage, *args )
24
+ when Class
25
+ storage.new(*args)
26
+ when String
27
+ resolve( Collective.resolve_class(storage), *args )
28
+ when Array
29
+ args = storage.dup + args
30
+ storage = args.shift
31
+ resolve( storage, *args )
32
+ else
33
+ return storage
34
+ end
35
+ end
36
+ end # resolve
37
+ end # Resolver
38
+
39
+ extend Resolver
40
+
41
+ end # Collective::Utilities::StorageBase
@@ -0,0 +1,3 @@
1
+ module Collective
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,161 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ =begin
4
+
5
+ A Worker is a forked process which runs jobs.
6
+ Jobs are short lived and run repeatedly.
7
+
8
+ =end
9
+
10
+ class Collective::Worker
11
+
12
+ include Collective::Utilities::Observeable
13
+
14
+ # forks a new process
15
+ # creates a new instance of the job class
16
+ # runs a loop which calls the job
17
+ def self.spawn( prototype_job, options = {} )
18
+ policy = options[:policy] || Collective::Policy.resolve
19
+ name = options[:name] || policy.name || prototype_job.to_s
20
+ storage = policy.storage
21
+ registry = options[:registry] || Collective::Registry.new( name, storage )
22
+
23
+ foptions = { stdout: "/tmp/debug.log" }
24
+ Collective::Utilities::Process.fork_and_detach( foptions ) do
25
+ if after_forks = policy.after_forks then
26
+ after_forks.each { |af| af.call }
27
+ end
28
+ # $0 = "$0 #{name}"
29
+ worker = new( prototype_job, options )
30
+ trap("TERM") { worker.quit! }
31
+ worker.run
32
+ end
33
+ end
34
+
35
+ attr :job
36
+ attr :name
37
+ attr :policy
38
+ attr :registry
39
+ attr :state
40
+ attr :storage
41
+ attr :worker_expire
42
+ attr :worker_jobs
43
+
44
+ # @param options[:name] is optional
45
+ # @param options[:policy] is optional
46
+ # @param options[:registry] is optional
47
+ def initialize( prototype_job, options = {} )
48
+ @policy = options[:policy] || Collective::Policy.resolve
49
+ @name = options[:name] || policy.name || prototype_job.to_s
50
+ @storage = policy.storage
51
+ @registry = options[:registry] || Collective::Registry.new( name, storage )
52
+ @job = Collective::Idler.new( resolve_job( prototype_job ), min_sleep: policy.worker_idle_min_sleep, max_sleep: policy.worker_idle_max_sleep )
53
+
54
+ # type checks
55
+ policy.pool_min_workers
56
+ registry.workers
57
+
58
+ # post-fork processing
59
+ storage.reconnect_after_fork
60
+ registry.reconnect_after_fork
61
+
62
+ # set up observers
63
+ policy.observers.each do |observer|
64
+ o = Collective::Utilities::ObserverBase.resolve(observer)
65
+ add_observer(o)
66
+ end
67
+
68
+ # manage the registry via an observer
69
+ add_observer( Collective::LifecycleObserver.new( key, registry ) )
70
+ end
71
+
72
+ def run()
73
+ @state = :running
74
+ @worker_jobs = 0
75
+ @worker_expire = Time.now + policy.worker_max_lifetime
76
+
77
+ context = { worker: self }
78
+ with_start_and_stop do
79
+ while running? do
80
+ with_quitting_checks do
81
+ with_heartbeat do
82
+ job.call(context)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def quit!()
90
+ @state = :quitting
91
+ end
92
+
93
+ def running?
94
+ state == :running
95
+ end
96
+
97
+ def to_s
98
+ %Q[Worker(#{key})]
99
+ end
100
+
101
+ # the key is a constant string which uniquely identifies this worker
102
+ # WARNING this would be invalidated if we forked or set this before forking
103
+ def key
104
+ @key ||= Collective::Key.new( name, Process.pid )
105
+ end
106
+
107
+ def mq
108
+ @mq ||= Collective::Messager.new( storage, my_address: key )
109
+ end
110
+
111
+ # ----------------------------------------------------------------------------
112
+ protected
113
+ # ----------------------------------------------------------------------------
114
+
115
+ def with_start_and_stop(&block)
116
+ notify :worker_started
117
+ begin
118
+ yield
119
+ ensure
120
+ notify :worker_stopped
121
+ end
122
+ end
123
+
124
+ def with_quitting_checks(&block)
125
+ yield
126
+ ensure
127
+ @worker_jobs += 1
128
+ quit! if policy.worker_max_jobs <= worker_jobs
129
+ quit! if worker_expire <= Time.now
130
+ end
131
+
132
+ def with_heartbeat(&block)
133
+ begin
134
+ yield
135
+ rescue => x
136
+ notify :job_error, x
137
+ ensure
138
+ notify :worker_heartbeat
139
+ end
140
+ end
141
+
142
+ def resolve_job( job_factory )
143
+ raise Collective::ConfigurationError if ! job_factory
144
+
145
+ case
146
+ when job_factory.respond_to?(:call)
147
+ job_factory
148
+ when job_factory.respond_to?(:new)
149
+ context = { worker: self }
150
+ resolve_job(job_factory.new(context))
151
+ else
152
+ case job_factory
153
+ when String, Symbol
154
+ resolve_job(Collective.resolve_class(job_factory.to_s))
155
+ else
156
+ raise Collective::ConfigurationError, "Unknown kind of job #{job_factory.inspect}"
157
+ end
158
+ end
159
+ end
160
+
161
+ end # Collective::Worker
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+ require "collective/checker"
5
+
6
+ describe Checker do
7
+
8
+ it "does something" do
9
+ activity_count = 192
10
+ last_time = Time.now - 3600
11
+ c = Checker.new activity_count, last_time
12
+
13
+ c.check?.should_not be_false
14
+ c.estimated_delay.should be >= 1.0
15
+ c.estimated_delay.should be < 8.0
16
+
17
+ c.checked 12
18
+ end
19
+
20
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Configuration do
6
+
7
+ it "should parse command-line switches" do
8
+ c = Collective::Configuration.parse %w(--dry-run --env the_env --name a_name --chdir .)
9
+ c.env.should eq("the_env")
10
+ c.name.should eq("a_name")
11
+ end
12
+
13
+ it "should parse the DSL" do
14
+ script = <<-EOT.gsub(/^ +/,'')
15
+ set_env "the_env"
16
+ set_name "a_name"
17
+ chdir "."
18
+ EOT
19
+ c = Collective::Configuration.parse ["--dry-run", "--script", script]
20
+ c.env.should eq("the_env")
21
+ c.name.should eq("a_name")
22
+ end
23
+
24
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ =begin
4
+ @see https://www.relishapp.com/rspec/rspec-core/docs/hooks/around-hooks
5
+ @see https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/be-matchers
6
+ @see https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/equality-matchers
7
+ @see https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/operator-matchers
8
+ @see https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers/predicate-matchers
9
+ =end
10
+
11
+ require "bundler/setup" # set up gem paths
12
+ require "ruby-debug" # because sometimes you need it
13
+
14
+ #require "simplecov" # code coverage
15
+ #SimpleCov.start # must be loaded before our own code
16
+
17
+ require "collective" # load this gem
18
+ require "support/jobs" # simple helpers for testing
19
+ require "support/redis" # simple helpers for testing
20
+ require "support/timing" # simple helpers for testing
21
+
22
+ RSpec.configure do |spec|
23
+ # @see https://www.relishapp.com/rspec/rspec-core/docs/helper-methods/define-helper-methods-in-a-module
24
+ spec.include RedisClient, redis: true
25
+ spec.include Timing, time: true
26
+ spec.include Collective::Idler::Utilities
27
+
28
+ # nuke the Redis database around each run
29
+ # @see https://www.relishapp.com/rspec/rspec-core/docs/hooks/around-hooks
30
+ spec.around( :each, redis: true ) do |example|
31
+ with_clean_redis { example.run }
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Idler do
6
+
7
+ it "should accept a proc" do
8
+ idler = Collective::Idler.new() { true }
9
+ expect { idler.call }.to_not raise_error
10
+ end
11
+
12
+ it "should accept a lambda" do
13
+ job = ->() { true }
14
+ idler = Collective::Idler.new(job)
15
+ expect { idler.call }.to_not raise_error
16
+ end
17
+
18
+ it "should accept an object with an interface" do
19
+ idler = Collective::Idler.new( TrueJob.new )
20
+ expect { idler.call }.to_not raise_error
21
+ end
22
+
23
+ it "should refuse non-callable jobs" do
24
+ fake_job = Object.new
25
+ expect { Collective::Idler.new(fake_job) }.to raise_error
26
+ end
27
+
28
+ describe "when dealing with sleep times", time: true do
29
+
30
+ it "should not run idle tasks too much" do
31
+ count = 0
32
+ Collective::Idler.wait_until { count += 1; false }
33
+ count.should be <= 10
34
+ end
35
+
36
+ it "should sleep after a failure" do
37
+ job = ->() { false }
38
+ idler = Collective::Idler.new( job, min_sleep: 0.125 )
39
+ time { idler.call }.should be >= 0.125
40
+ end
41
+
42
+ it "should not sleep after a success" do
43
+ result = false
44
+ job = ->() { result }
45
+ idler = Collective::Idler.new( job, min_sleep: 0.125 )
46
+ idler.call
47
+ time { result = true; idler.call }.should be < 0.1
48
+ end
49
+
50
+ it "should not sleep too long" do
51
+ job = ->() { false }
52
+ idler = Collective::Idler.new( job, min_sleep: 0.125 )
53
+ time { idler.call }.should be < 0.25
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Key do
6
+
7
+ it "can make keys" do
8
+ name = "processor"
9
+ pid = 1234
10
+ host = "foo.example.com"
11
+ Collective::Key.new( name, pid, host ).to_s.should eq("processor-1234@foo.example.com")
12
+ end
13
+
14
+ it "can parse keys" do
15
+ Collective::Key.parse("processor-1234@foo.example.com").should eq(Collective::Key.new( "processor", "1234", "foo.example.com" ))
16
+ Collective::Key.parse("test-job-1234@foo.example.com").should eq(Collective::Key.new( "test-job", "1234", "foo.example.com" ))
17
+ end
18
+
19
+ it "can compare to another key" do
20
+ key1 = Collective::Key.new "Alpha", 1234, "example.com"
21
+ key2 = Collective::Key.new "Alpha", 1234, "example.com"
22
+ key1.should eq(key2)
23
+
24
+ key1 = Collective::Key.new "Alpha", 1234, "example.com"
25
+ key2 = Collective::Key.new "Alpha", "1234", "example.com"
26
+ key1.should eq(key2)
27
+
28
+ key1 = Collective::Key.new "Alpha", 1234, "example.com"
29
+ key2 = Collective::Key.new "Beta", 1234, "example.com"
30
+ key1.should_not eq(key2)
31
+
32
+ key1 = Collective::Key.new "Alpha", 1234, "example.com"
33
+ key2 = Collective::Key.new "Alpha", 2345, "example.com"
34
+ key1.should_not eq(key2)
35
+
36
+ key1 = Collective::Key.new "Alpha", 1234, "example.com"
37
+ key2 = Collective::Key.new "Alpha", 1234, "example.org"
38
+ key1.should_not eq(key2)
39
+ end
40
+
41
+ end
@@ -0,0 +1,131 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Messager, redis: true do
6
+
7
+ before do
8
+ @a = "me@example.com"
9
+ @b = "you@example.com"
10
+ end
11
+
12
+ it "can take to_address initially or per message" do
13
+ storage = Collective::Mocks::Storage.new
14
+
15
+ a = Collective::Messager.new( storage, my_address: @a )
16
+ expect { a.send "Hello" }.to raise_exception
17
+ expect { a.send "Hello", to: @b }.to_not raise_exception
18
+
19
+ b = Collective::Messager.new( storage, my_address: @a, to_address: @b )
20
+ expect { b.send "Hello" }.to_not raise_exception
21
+ end
22
+
23
+ it "the message id varies with the source, content and timestamp" do
24
+ storage = Collective::Mocks::Storage.new
25
+ a = Collective::Messager.new( storage, my_address: @a, to_address: @b )
26
+ b = Collective::Messager.new( storage, my_address: @b, to_address: @a )
27
+ now = 1234567890
28
+
29
+ id1 = a.send "Hello", at: now
30
+ id3 = a.send "Hello", at: now+1
31
+ id4 = a.send "Goodbye", at: now
32
+ id5 = b.send "Hello", at: now
33
+
34
+ id3.should_not eq(id1)
35
+ id4.should_not eq(id1)
36
+ id5.should_not eq(id1)
37
+ end
38
+
39
+ describe "sending and receiving messages" do
40
+
41
+ it "can match against a string" do
42
+ storage = Collective::Mocks::Storage.new
43
+ a = Collective::Messager.new( storage, my_address: @a )
44
+ b = Collective::Messager.new( storage, my_address: @b )
45
+ b.expect("Hello") { |message| false }
46
+ a.send "Hello", to: @b
47
+ expect { b.receive }.to_not raise_exception
48
+ end
49
+
50
+ it "can match against a regexp" do
51
+ storage = Collective::Mocks::Storage.new
52
+ a = Collective::Messager.new( storage, my_address: @a )
53
+ b = Collective::Messager.new( storage, my_address: @b )
54
+ b.expect(/ello/) { |message| false }
55
+ a.send "Hello", to: @b
56
+ expect { b.receive }.to_not raise_exception
57
+ end
58
+
59
+ it "returns true if it got a message" do
60
+ storage = Collective::Mocks::Storage.new
61
+ a = Collective::Messager.new( storage, my_address: @a )
62
+ b = Collective::Messager.new( storage, my_address: @b )
63
+ b.expect(//) { |message| false }
64
+ a.receive.should eq(false)
65
+ a.send "Hello", to: @b
66
+ b.receive.should eq(true)
67
+ b.receive.should eq(false)
68
+ end
69
+
70
+ it "can send and receive messages" do
71
+ storage = Collective::Mocks::Storage.new
72
+ a = Collective::Messager.new( storage, my_address: @a )
73
+ b = Collective::Messager.new( storage, my_address: @b )
74
+
75
+ callback = double("callback")
76
+ callback.should_receive(:call).with(anything)
77
+
78
+ reply_to_id = nil
79
+
80
+ b.expect("Hello") { |message|
81
+ message.body.should eq("Hello")
82
+ message.from.should eq(@a)
83
+ reply_to_id = message.id
84
+ b.reply( "Goodbye", to: message )
85
+ }
86
+
87
+ a.expect("Goodbye") { |message|
88
+ message.body.should eq("Goodbye")
89
+ message.from.should eq(@b)
90
+ message.id.should_not be_nil
91
+ message.reply_to_id.should eq(reply_to_id)
92
+ callback.call(message)
93
+ }
94
+
95
+ a.send "Hello", to: @b
96
+ b.receive
97
+ a.receive
98
+ end
99
+
100
+ end
101
+
102
+ describe "when working with multiple processes", redis: true do
103
+
104
+ it "can send a message between processes" do
105
+ storage = Collective::Redis::Storage.new(redis)
106
+ me = Collective::Messager.new( storage, my_address: @a )
107
+
108
+ ok = false
109
+ me.expect("Goodbye") do |message|
110
+ ok = true
111
+ end
112
+
113
+ Collective::Utilities::Process.fork_and_detach do
114
+ redis.client.disconnect
115
+ me = Collective::Messager.new( storage, my_address: @b )
116
+ ok = false
117
+ me.expect("Hello") do |message|
118
+ me.reply "Goodbye", to: message
119
+ ok = true
120
+ end
121
+ wait_until { me.receive; ok }
122
+ end
123
+
124
+ me.send "Hello", to: @b
125
+ wait_until { me.receive }
126
+ ok.should be_true
127
+ end
128
+
129
+ end
130
+
131
+ end