mlanett-hive 0.3.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 (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