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,108 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Mocks::Storage do
6
+
7
+ before do
8
+ @it = Collective::Mocks::Storage.new
9
+ end
10
+
11
+ it "should be concrete" do
12
+ @it.should_not be_nil
13
+ end
14
+
15
+ describe "simple values" do
16
+
17
+ before do
18
+ @it.put("foo","bar")
19
+ end
20
+
21
+ it "should be settable" do
22
+ @it.get("foo").should eq("bar")
23
+ end
24
+
25
+ it "should be replaceable" do
26
+ @it.put("foo","goo")
27
+ @it.get("foo").should eq("goo")
28
+ end
29
+
30
+ it "should be deleteable" do
31
+ @it.get("foo").should_not be_nil
32
+ @it.del("foo")
33
+ @it.get("foo").should be_nil
34
+ end
35
+
36
+ end # simple values
37
+
38
+ describe "lists" do
39
+
40
+ before do
41
+ @it.set_add("foos","A")
42
+ end
43
+
44
+ it "should not add to a list twice" do
45
+ @it.set_add("foos","A")
46
+ @it.set_size("foos").should eq 1
47
+ end
48
+
49
+ it "should be able to add to, enumerate, and remove from a list" do
50
+ @it.set_add("foos","B")
51
+ @it.set_size("foos").should eq 2
52
+ @it.set_get_all("foos").should be_include("B")
53
+ @it.set_remove("foos","A")
54
+ @it.set_size("foos").should eq 1
55
+ end
56
+
57
+ it "can detect membership in a list" do
58
+ @it.set_add("foos","A")
59
+ @it.set_member?("foos","A").should be_true
60
+ end
61
+
62
+ end # lists
63
+
64
+ describe "maps" do
65
+
66
+ before do
67
+ @it.map_set("good","A","E")
68
+ @it.map_set("food","A","B")
69
+ @it.map_set("food","C","D")
70
+ end
71
+
72
+ it "should count a set in a key as one item" do
73
+ @it.map_size("food").should eq 2
74
+ @it.map_size("good").should eq 1
75
+ end
76
+
77
+ it "should retain the set" do
78
+ @it.map_get("food","A").should eq "B"
79
+ @it.map_get("food","C").should eq "D"
80
+ @it.map_get("good","A").should eq "E"
81
+ end
82
+
83
+ it "should add to the set" do
84
+ @it.map_get_all_keys("food").size.should eq 2
85
+ end
86
+
87
+ it "should delete the set" do
88
+ @it.del("food")
89
+ @it.map_size("food").should eq 0
90
+ end
91
+
92
+ end # maps
93
+
94
+ describe "priority queues" do
95
+
96
+ it "can add items and remove them in order" do
97
+ @it.queue_add "foo", "A", 1
98
+ @it.queue_add "foo", "C", 3
99
+ @it.queue_add "foo", "B", 2
100
+ @it.queue_pop("foo",0).should eq(nil)
101
+ @it.queue_pop("foo",9).should eq("A")
102
+ @it.queue_pop("foo",9).should eq("B")
103
+ @it.queue_pop("foo",9).should eq("C")
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Monitor do
6
+
7
+ it "can monitor all pools continuously"
8
+
9
+ it "can start all workers in all pools"
10
+
11
+ it "can stop all workers in all pools"
12
+
13
+ it "can restart all workers in all pools"
14
+
15
+ end
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Policy do
6
+
7
+ it "should have defaults" do
8
+ p = Collective::Policy.resolve
9
+ p.worker_max_jobs.should eq(100)
10
+ end
11
+
12
+ it "should be changeable" do
13
+ p = Collective::Policy.resolve worker_max_jobs: 5
14
+ p.worker_max_jobs.should eq(5)
15
+ end
16
+
17
+ it "should work with symbols" do
18
+ p = Collective::Policy.resolve worker_max_jobs: 5
19
+ p.worker_max_jobs.should eq(5)
20
+ end
21
+
22
+ it "should support observers" do
23
+ o = Collective::Utilities::ObserverBase.new
24
+ p = Collective::Policy.resolve observers: [o]
25
+ p.observers.should eq([o])
26
+ end
27
+
28
+ it "is copied from another policy" do
29
+ p1 = Collective::Policy.resolve worker_max_jobs: 7, worker_max_lifetime: 999
30
+ p2 = Collective::Policy.resolve policy: p1, worker_max_jobs: 12
31
+
32
+ p2.worker_max_lifetime.should eq(p1.worker_max_lifetime)
33
+ p1.worker_max_jobs.should eq(7)
34
+ p2.worker_max_jobs.should eq(12)
35
+ end
36
+
37
+ it "can resolve storage" do
38
+ p = Collective::Policy.resolve storage: nil
39
+ s = p.storage
40
+ s.should be_instance_of(Collective::Mocks::Storage)
41
+ end
42
+
43
+ end
@@ -0,0 +1,119 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+ require "redis"
5
+
6
+ describe Collective::Pool do
7
+
8
+ before do
9
+ @name = "#{ described_class || 'Test' }::#{example.description}"
10
+ end
11
+
12
+ describe "name" do
13
+
14
+ it "needs to be specified for a proc" do
15
+ job = ->(context) { false }
16
+ expect { pool = Collective::Pool.new( job ) }.to raise_error
17
+ expect { pool = Collective::Pool.new( job, name: @name ) }.to_not raise_error
18
+ end
19
+
20
+ it "does not need to be specified for a class" do
21
+ job = TrueJob
22
+ expect { pool = Collective::Pool.new( job ) }.to_not raise_error
23
+ end
24
+
25
+ end
26
+
27
+ describe "when spawning proceses", redis: true do
28
+
29
+ def make_policy( options = {} )
30
+ options = { name: @name, worker_max_lifetime: 10, storage: :redis, observers: [ [ :log, "/tmp/debug.log" ] ] }.merge(options)
31
+ Collective::Policy.resolve(options)
32
+ end
33
+
34
+ it "should spawn a worker" do
35
+ job = ->(context) {}
36
+ pool = Collective::Pool.new( job, make_policy )
37
+
38
+ pool.stub(:spawn) {} # must be called at least once
39
+
40
+ pool.synchronize
41
+ end
42
+
43
+ it "spins up an actual worker" do
44
+ pool = Collective::Pool.new( ListenerJob, make_policy )
45
+
46
+ pool.registry.workers.size.should be == 0
47
+
48
+ pool.synchronize
49
+ wait_until { pool.registry.workers.size > 0 }
50
+ pool.registry.workers.size.should be > 0
51
+ other = pool.registry.workers.first
52
+
53
+ pool.mq.expect(/State/) { |message| puts message.body }
54
+ pool.mq.send "State?", to: other
55
+ pool.mq.receive
56
+ pool.mq.send "Quit", to: other
57
+ end
58
+
59
+ it "spins up a worker only once" do
60
+ policy = make_policy pool_max_workers: 1
61
+ pool = Collective::Pool.new( ListenerJob, policy )
62
+ registry = pool.registry
63
+
64
+ registry.checked_workers( policy ).live.size.should eq(0)
65
+
66
+ pool.synchronize
67
+ registry.checked_workers( policy ).live.size.should eq(1)
68
+
69
+ pool.synchronize
70
+ registry.checked_workers( policy ).live.size.should eq(1)
71
+ end
72
+
73
+ it "should spin up new workers as necessary" do
74
+ policy = make_policy pool_min_workers: 2, pool_max_workers: 2
75
+ pool = Collective::Pool.new( ListenerJob, policy )
76
+ registry = pool.registry
77
+
78
+ registry.checked_workers( policy ).live.size.should eq(0)
79
+
80
+ pool.synchronize
81
+ registry.checked_workers( policy ).live.size.should eq(2)
82
+
83
+ first = registry.checked_workers( policy ).live.first
84
+ pool.mq.send "Quit", to: first
85
+ wait_until { registry.checked_workers( policy ).live.size == 1 }
86
+ registry.checked_workers( policy ).live.size.should eq(1)
87
+
88
+ pool.synchronize
89
+ registry.checked_workers( policy ).live.size.should eq(2)
90
+ end
91
+
92
+ it "spins down workers when there are too many" do
93
+ policy = make_policy pool_min_workers: 2, pool_max_workers: 2
94
+ pool = Collective::Pool.new( ListenerJob, policy )
95
+ registry = pool.registry
96
+
97
+ registry.checked_workers( policy ).live.size.should eq(0)
98
+
99
+ pool.synchronize
100
+ registry.checked_workers( policy ).live.size.should eq(2)
101
+
102
+ # create a second pool with its own policy to force quitting
103
+ policy2 = make_policy pool_min_workers: 1, pool_max_workers: 1
104
+ pool2 = Collective::Pool.new( ListenerJob, policy2 )
105
+ pool2.synchronize
106
+ registry.checked_workers( policy ).live.size.should eq(1)
107
+ end
108
+
109
+ end
110
+
111
+ describe "when tracking how long workers run", time: true do
112
+
113
+ it "should warn when a worker is running late"
114
+
115
+ it "should kill when a worker is running too late"
116
+
117
+ end
118
+
119
+ end
@@ -0,0 +1,133 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Redis::Storage, redis: true do
6
+
7
+ before do
8
+ @it = Collective::Redis::Storage.new(redis)
9
+ end
10
+
11
+ it "should be concrete" do
12
+ @it.should_not be_nil
13
+ end
14
+
15
+ describe "simple values" do
16
+
17
+ before do
18
+ @it.put("foo","bar")
19
+ end
20
+
21
+ it "should be settable" do
22
+ @it.get("foo").should eq("bar")
23
+ end
24
+
25
+ it "should be replaceable" do
26
+ @it.put("foo","goo")
27
+ @it.get("foo").should eq("goo")
28
+ end
29
+
30
+ it "should be deleteable" do
31
+ @it.get("foo").should_not be_nil
32
+ @it.del("foo")
33
+ @it.get("foo").should be_nil
34
+ end
35
+
36
+ end # simple values
37
+
38
+ describe "lists" do
39
+
40
+ before do
41
+ @it.del("foos")
42
+ @it.set_add("foos","A")
43
+ end
44
+
45
+ it "should not add to a list twice" do
46
+ @it.set_add("foos","A")
47
+ @it.set_size("foos").should eq 1
48
+ end
49
+
50
+ it "should be able to add to, enumerate, and remove from a list" do
51
+ @it.set_add("foos","B")
52
+ @it.set_size("foos").should eq 2
53
+ @it.set_get_all("foos").should be_include("B")
54
+ @it.set_remove("foos","A")
55
+ @it.set_size("foos").should eq 1
56
+ end
57
+
58
+ it "can detect membership in a list" do
59
+ @it.set_add("foos","A")
60
+ @it.set_member?("foos","A").should be_true
61
+ end
62
+
63
+ end # lists
64
+
65
+ describe "maps" do
66
+
67
+ before do
68
+ @it.map_set("good","A","E")
69
+ @it.map_set("food","A","B")
70
+ @it.map_set("food","C","D")
71
+ end
72
+
73
+ it "should count a set in a key as one item" do
74
+ @it.map_size("food").should eq 2
75
+ @it.map_size("good").should eq 1
76
+ end
77
+
78
+ it "should retain the set" do
79
+ @it.map_get("food","A").should eq "B"
80
+ @it.map_get("food","C").should eq "D"
81
+ @it.map_get("good","A").should eq "E"
82
+ end
83
+
84
+ it "should add to the set" do
85
+ @it.map_get_all_keys("food").size.should eq 2
86
+ end
87
+
88
+ it "should delete the set" do
89
+ @it.del("food")
90
+ @it.map_size("food").should eq 0
91
+ end
92
+
93
+ end # maps
94
+
95
+ describe "priority queues" do
96
+
97
+ it "can add items and remove them in order" do
98
+ @it.queue_add "foo", "A", 1
99
+ @it.queue_add "foo", "C", 3
100
+ @it.queue_add "foo", "B", 2
101
+ @it.queue_pop("foo",0).should eq(nil)
102
+ @it.queue_pop("foo",9).should eq("A")
103
+ @it.queue_pop("foo",9).should eq("B")
104
+ @it.queue_pop("foo",9).should eq("C")
105
+ end
106
+
107
+ it "can handle some load" do
108
+ # expect 1..1000 in the queue
109
+ # write our 1..1000 in two separate processes
110
+ # plus some extra just to mess us up
111
+
112
+ Collective::Utilities::Process.fork_and_detach do
113
+ redis.client.reconnect
114
+ q = Collective::Redis::Storage.new(redis)
115
+ (1..2000).each { |i| q.queue_add( "foo", i.to_s, i ) if i % 2 == 0 }
116
+ end
117
+
118
+ Collective::Utilities::Process.fork_and_detach do
119
+ redis.client.reconnect
120
+ q = Collective::Redis::Storage.new(redis)
121
+ (1..2000).each { |i| q.queue_add( "foo", i.to_s, i ) if i % 2 == 1 }
122
+ end
123
+
124
+ (1..1000).each do |i|
125
+ i2 = @it.queue_pop_sync( "foo", i, timeout: 10 )
126
+ i2.should eq(i.to_s)
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+
133
+ end
@@ -0,0 +1,52 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Collective::Registry, redis: true do
6
+
7
+ before do
8
+ @policy = Collective::Policy.resolve
9
+ end
10
+
11
+ it "can register a worker" do
12
+ registry = Collective::Registry.new( "Test", @policy.storage )
13
+ worker = Collective::Worker.new( TrueJob, registry: registry )
14
+
15
+ registry.register( worker.key )
16
+ registry.workers.should be_include( worker.key )
17
+ end
18
+
19
+ it "can unregister a worker" do
20
+ registry = Collective::Registry.new( "Test", @policy.storage )
21
+ worker = Collective::Worker.new( TrueJob, registry: registry )
22
+
23
+ registry.register( worker.key )
24
+ registry.workers.should be_include( worker.key )
25
+
26
+ registry.unregister( worker.key )
27
+ registry.workers.should_not be_include( worker.key )
28
+ end
29
+
30
+ it "can find live workers" do
31
+ registry = Collective::Registry.new( "Test", @policy.storage )
32
+ heartbeat = @policy.worker_late / 2
33
+ key = Collective::Key.new("Test",1234)
34
+
35
+ registry.register(key)
36
+ checked = registry.checked_workers(@policy)
37
+ checked.live.should eq([key])
38
+ end
39
+
40
+ it "can find late workers" do
41
+ registry = Collective::Registry.new( "Test", @policy.storage )
42
+ heartbeat = @policy.worker_late
43
+ key = Collective::Key.new("Test",1234)
44
+
45
+ now = Time.now.to_i
46
+ registry.register(key) # should register with heartbeat = now or now+1
47
+ registry.stub(:now) { now + @policy.worker_late + 2 }
48
+ checked = registry.checked_workers(@policy)
49
+ checked.late.should eq([key])
50
+ end
51
+
52
+ end