mlanett-hive 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.autotest +13 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +16 -0
  5. data/Guardfile +6 -0
  6. data/README +9 -0
  7. data/Rakefile +11 -0
  8. data/bin/hive +37 -0
  9. data/demo/demo +36 -0
  10. data/demo/demo.rb +30 -0
  11. data/demo/demo3 +36 -0
  12. data/demo/job1.rb +31 -0
  13. data/demo/job2.rb +42 -0
  14. data/demo/job3.rb +44 -0
  15. data/demo/populate.rb +22 -0
  16. data/hive.gemspec +21 -0
  17. data/lib/hive.rb +42 -0
  18. data/lib/hive/checker.rb +51 -0
  19. data/lib/hive/configuration.rb +251 -0
  20. data/lib/hive/idler.rb +81 -0
  21. data/lib/hive/key.rb +48 -0
  22. data/lib/hive/lifecycle_observer.rb +25 -0
  23. data/lib/hive/log.rb +29 -0
  24. data/lib/hive/messager.rb +217 -0
  25. data/lib/hive/mocks/storage.rb +112 -0
  26. data/lib/hive/monitor.rb +57 -0
  27. data/lib/hive/policy.rb +68 -0
  28. data/lib/hive/pool.rb +180 -0
  29. data/lib/hive/redis/storage.rb +145 -0
  30. data/lib/hive/registry.rb +123 -0
  31. data/lib/hive/squiggly.rb +20 -0
  32. data/lib/hive/trace.rb +5 -0
  33. data/lib/hive/utilities/airbrake_observer.rb +26 -0
  34. data/lib/hive/utilities/hoptoad_observer.rb +26 -0
  35. data/lib/hive/utilities/log_observer.rb +40 -0
  36. data/lib/hive/utilities/observeable.rb +18 -0
  37. data/lib/hive/utilities/observer_base.rb +59 -0
  38. data/lib/hive/utilities/process.rb +82 -0
  39. data/lib/hive/utilities/resolver.rb +12 -0
  40. data/lib/hive/utilities/signal_hook.rb +47 -0
  41. data/lib/hive/utilities/storage_base.rb +41 -0
  42. data/lib/hive/version.rb +3 -0
  43. data/lib/hive/worker.rb +162 -0
  44. data/spec/checker_spec.rb +20 -0
  45. data/spec/configuration_spec.rb +50 -0
  46. data/spec/helper.rb +33 -0
  47. data/spec/idler_spec.rb +58 -0
  48. data/spec/key_spec.rb +41 -0
  49. data/spec/messager_spec.rb +131 -0
  50. data/spec/mocks/storage_spec.rb +108 -0
  51. data/spec/monitor_spec.rb +15 -0
  52. data/spec/policy_spec.rb +43 -0
  53. data/spec/pool_spec.rb +119 -0
  54. data/spec/redis/storage_spec.rb +133 -0
  55. data/spec/registry_spec.rb +52 -0
  56. data/spec/support/jobs.rb +68 -0
  57. data/spec/support/redis.rb +22 -0
  58. data/spec/support/timing.rb +32 -0
  59. data/spec/utilities/observer_base_spec.rb +50 -0
  60. data/spec/utilities/process_spec.rb +17 -0
  61. data/spec/worker_spec.rb +121 -0
  62. data/unused/times.rb +45 -0
  63. metadata +148 -0
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class Hive::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( Hive::Mocks::Storage, *args )
22
+ when :redis
23
+ resolve( Hive::Redis::Storage, *args )
24
+ when Class
25
+ storage.new(*args)
26
+ when String
27
+ resolve( Hive::Utilities::Resolver.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 # Hive::Utilities::StorageBase
@@ -0,0 +1,3 @@
1
+ module Hive
2
+ VERSION = "0.3.0"
3
+ end
@@ -0,0 +1,162 @@
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 Hive::Worker
11
+
12
+ include Hive::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] || Hive::Policy.resolve
19
+ name = options[:name] || policy.name || prototype_job.to_s
20
+ storage = policy.storage
21
+ registry = options[:registry] || Hive::Registry.new( name, storage )
22
+
23
+ foptions = { stdout: "/tmp/debug.log" }
24
+ policy.before_fork
25
+ Hive::Utilities::Process.fork_and_detach( foptions ) do
26
+ policy.after_fork
27
+ # $0 = "$0 #{name}"
28
+ worker = new( prototype_job, options )
29
+ trap("TERM") { worker.quit! }
30
+ worker.run
31
+ end
32
+ end
33
+
34
+ attr :job
35
+ attr :name
36
+ attr :policy
37
+ attr :registry
38
+ attr :state
39
+ attr :storage
40
+ attr :worker_expire
41
+ attr :worker_jobs
42
+
43
+ # @param options[:name] is optional
44
+ # @param options[:policy] is optional
45
+ # @param options[:registry] is optional
46
+ def initialize( prototype_job, options = {} )
47
+ @policy = options[:policy] || Hive::Policy.resolve
48
+ @name = options[:name] || policy.name || prototype_job.to_s
49
+ @storage = policy.storage
50
+ @registry = options[:registry] || Hive::Registry.new( name, storage )
51
+ @job = Hive::Idler.new( resolve_job( prototype_job ), min_sleep: policy.worker_idle_min_sleep, max_sleep: policy.worker_idle_max_sleep )
52
+
53
+ # type checks
54
+ policy.pool_min_workers
55
+ registry.workers
56
+
57
+ # post-fork processing
58
+ storage.reconnect_after_fork
59
+ registry.reconnect_after_fork
60
+
61
+ # set up observers
62
+ policy.observers.each do |observer|
63
+ o = Hive::Utilities::ObserverBase.resolve(observer)
64
+ add_observer(o)
65
+ end
66
+
67
+ # manage the registry via an observer
68
+ add_observer( Hive::LifecycleObserver.new( key, registry ) )
69
+ end
70
+
71
+ def run()
72
+ @state = :running
73
+ @worker_jobs = 0
74
+ @worker_expire = Time.now + policy.worker_max_lifetime
75
+
76
+ context = { worker: self }
77
+ with_start_and_stop do
78
+ while running? do
79
+ with_quitting_checks do
80
+ with_heartbeat do
81
+ job.call(context)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def quit!()
89
+ @state = :quitting
90
+ end
91
+
92
+ def running?
93
+ state == :running
94
+ end
95
+
96
+ def to_s
97
+ %Q[Worker(#{key})]
98
+ end
99
+
100
+ # the key is a constant string which uniquely identifies this worker
101
+ # WARNING this would be invalidated if we forked or set this before forking
102
+ def key
103
+ @key ||= Hive::Key.new( name, Process.pid )
104
+ end
105
+
106
+ def mq
107
+ @mq ||= Hive::Messager.new( storage, my_address: key )
108
+ end
109
+
110
+ # ----------------------------------------------------------------------------
111
+ protected
112
+ # ----------------------------------------------------------------------------
113
+
114
+ def with_start_and_stop(&block)
115
+ notify :worker_started
116
+ begin
117
+ yield
118
+ ensure
119
+ notify :worker_stopped
120
+ end
121
+ end
122
+
123
+ def with_quitting_checks(&block)
124
+ yield
125
+ ensure
126
+ @worker_jobs += 1
127
+ quit! if policy.worker_max_jobs <= worker_jobs
128
+ quit! if worker_expire <= Time.now
129
+ end
130
+
131
+ def with_heartbeat(&block)
132
+ begin
133
+ yield
134
+ rescue => x
135
+ notify :job_error, x
136
+ ensure
137
+ notify :worker_heartbeat
138
+ end
139
+ end
140
+
141
+ def resolve_job( job_factory )
142
+ raise Hive::ConfigurationError if ! job_factory
143
+
144
+ case
145
+ when job_factory.respond_to?(:call)
146
+ # A job factory can not be a proc, because a job itself is a proc; we would call it.
147
+ # Once we've found a proc, we have the result.
148
+ job_factory
149
+ when job_factory.respond_to?(:new)
150
+ context = { worker: self }
151
+ resolve_job(job_factory.new(context))
152
+ else
153
+ case job_factory
154
+ when String, Symbol
155
+ resolve_job(Hive::Utilities::Resolver.resolve_class(job_factory.to_s))
156
+ else
157
+ raise Hive::ConfigurationError, "Unknown kind of job #{job_factory.inspect}"
158
+ end
159
+ end
160
+ end
161
+
162
+ end # Hive::Worker
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+ require "hive/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,50 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Hive::Configuration do
6
+
7
+ it "should parse command-line switches" do
8
+ c = Hive::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
+ before_fork() { true }
19
+ after_fork() { true }
20
+ EOT
21
+ c = Hive::Configuration.parse ["--dry-run", "--script", script]
22
+ c.env.should eq("the_env")
23
+ c.name.should eq("a_name")
24
+ c.before_forks.size.should eq(1)
25
+ c.after_forks.size.should eq(1)
26
+ end
27
+
28
+ it "enumerates pool names and policies" do
29
+ script = <<-EOT.gsub(/^ +/,'')
30
+ add_pool "Test", pool_max_workers: 1
31
+ EOT
32
+ c = Hive::Configuration.parse ["--dry-run", "--script", script]
33
+ c.policies.first.first.should eq("Test")
34
+ c.policies.first.last.pool_max_workers.should eq(1)
35
+ end
36
+
37
+ it "adds blocks to pools" do
38
+ script = <<-EOT.gsub(/^ +/,'')
39
+ before_fork() { true }
40
+ after_fork() { false }
41
+ add_pool "Test"
42
+ EOT
43
+ c = Hive::Configuration.parse ["--dry-run", "--script", script]
44
+ c.policies.to_a.size.should eq(1)
45
+ pool = c.policies.to_a.first.last
46
+ pool.before_forks.first.call.should eq(true)
47
+ pool.after_forks.first.call.should eq(false)
48
+ end
49
+
50
+ end
data/spec/helper.rb ADDED
@@ -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 "hive" # 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 Hive::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 Hive::Idler do
6
+
7
+ it "should accept a proc" do
8
+ idler = Hive::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 = Hive::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 = Hive::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 { Hive::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
+ Hive::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 = Hive::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 = Hive::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 = Hive::Idler.new( job, min_sleep: 0.125 )
53
+ time { idler.call }.should be < 0.25
54
+ end
55
+
56
+ end
57
+
58
+ end
data/spec/key_spec.rb ADDED
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require "helper"
4
+
5
+ describe Hive::Key do
6
+
7
+ it "can make keys" do
8
+ name = "processor"
9
+ pid = 1234
10
+ host = "foo.example.com"
11
+ Hive::Key.new( name, pid, host ).to_s.should eq("processor-1234@foo.example.com")
12
+ end
13
+
14
+ it "can parse keys" do
15
+ Hive::Key.parse("processor-1234@foo.example.com").should eq(Hive::Key.new( "processor", "1234", "foo.example.com" ))
16
+ Hive::Key.parse("test-job-1234@foo.example.com").should eq(Hive::Key.new( "test-job", "1234", "foo.example.com" ))
17
+ end
18
+
19
+ it "can compare to another key" do
20
+ key1 = Hive::Key.new "Alpha", 1234, "example.com"
21
+ key2 = Hive::Key.new "Alpha", 1234, "example.com"
22
+ key1.should eq(key2)
23
+
24
+ key1 = Hive::Key.new "Alpha", 1234, "example.com"
25
+ key2 = Hive::Key.new "Alpha", "1234", "example.com"
26
+ key1.should eq(key2)
27
+
28
+ key1 = Hive::Key.new "Alpha", 1234, "example.com"
29
+ key2 = Hive::Key.new "Beta", 1234, "example.com"
30
+ key1.should_not eq(key2)
31
+
32
+ key1 = Hive::Key.new "Alpha", 1234, "example.com"
33
+ key2 = Hive::Key.new "Alpha", 2345, "example.com"
34
+ key1.should_not eq(key2)
35
+
36
+ key1 = Hive::Key.new "Alpha", 1234, "example.com"
37
+ key2 = Hive::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 Hive::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 = Hive::Mocks::Storage.new
14
+
15
+ a = Hive::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 = Hive::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 = Hive::Mocks::Storage.new
25
+ a = Hive::Messager.new( storage, my_address: @a, to_address: @b )
26
+ b = Hive::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 = Hive::Mocks::Storage.new
43
+ a = Hive::Messager.new( storage, my_address: @a )
44
+ b = Hive::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 = Hive::Mocks::Storage.new
52
+ a = Hive::Messager.new( storage, my_address: @a )
53
+ b = Hive::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 = Hive::Mocks::Storage.new
61
+ a = Hive::Messager.new( storage, my_address: @a )
62
+ b = Hive::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 = Hive::Mocks::Storage.new
72
+ a = Hive::Messager.new( storage, my_address: @a )
73
+ b = Hive::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 = Hive::Redis::Storage.new(redis)
106
+ me = Hive::Messager.new( storage, my_address: @a )
107
+
108
+ ok = false
109
+ me.expect("Goodbye") do |message|
110
+ ok = true
111
+ end
112
+
113
+ Hive::Utilities::Process.fork_and_detach do
114
+ redis.client.disconnect
115
+ me = Hive::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