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