gru 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aaafbe9673dc1591126b49f9f896691011de317a
4
+ data.tar.gz: 181e47be5873eb7ad5a13bcda5a43fb2560a9984
5
+ SHA512:
6
+ metadata.gz: 1690313caa470b0573fb847928d2d0f04e312e0492ec0c9f3fc1da0fd0bd9ad4b1b49b9a1d8206c8c3418c72318f6589bd29a747516208da1a18a02a4ca43445
7
+ data.tar.gz: 94f8aa06b2884463bb4a24bb36be8b9281ea25e518adc3958423126302473806f35cdc6dc43cca4d9f3b621da25e0b7bd2af0378c4bb63ecfcf7477b3ba49997
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gru.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jeffrey Gillis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Gru
2
+
3
+ Manage worker/minion counts in an atomic fashion.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'gru'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install gru
21
+
22
+ ## Usage
23
+
24
+ require 'gru'
25
+ require 'redis'
26
+ require 'logger'
27
+
28
+ client = Redis.new
29
+
30
+ class Gru::Adapters::RedisAdapter
31
+ def hostname
32
+ @hostname ||= ARGV[0]
33
+ end
34
+ end
35
+
36
+ workers = { 'test_worker' => 5 }
37
+ global = { 'test_worker' => 10 }
38
+ manager = Gru.with_redis_connection(client,workers,global,true)
39
+ manager.register_worker_queues
40
+ logger = Logger.new(STDOUT)
41
+
42
+ loop do
43
+ logger.info("STATE: #{manager.adjust_workers.inspect}")
44
+ sleep(1)
45
+ end
46
+
47
+ More to come soon!
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it ( https://github.com/[my-github-username]/gru/fork )
52
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
53
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
54
+ 4. Push to the branch (`git push origin my-new-feature`)
55
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/gru.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gru/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gru"
8
+ spec.version = Gru::VERSION
9
+ spec.authors = ["Jeffrey Gillis"]
10
+ spec.email = ["jeffrey.gillis1@gmail.com"]
11
+ spec.summary = %q{An atomic worker/minion manager.}
12
+ spec.description = %q{This is a worker/minion manager using different atomic data stores.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "redis", "> 0.0"
22
+ spec.add_development_dependency "bundler", "~> 1.7"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.1.0"
25
+ spec.add_development_dependency "pry", "> 0.0"
26
+ spec.add_development_dependency "awesome_print", "> 0.0"
27
+ end
data/lib/gru.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'gru/version'
2
+ require 'gru/worker_manager'
3
+ require 'gru/adapters'
4
+ require 'gru/adapters/redis_adapter'
5
+
6
+ module Gru
7
+ def self.with_redis_connection(client, worker_config, global_config,balanced)
8
+ manager = WorkerManager.with_redis_connection(client,worker_config,global_config,balanced)
9
+ manager.register_worker_queues
10
+ manager
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module Gru
2
+ module Adapters
3
+ end
4
+ end
@@ -0,0 +1,199 @@
1
+ require 'socket'
2
+
3
+ module Gru
4
+ module Adapters
5
+ class RedisAdapter
6
+ attr_reader :client
7
+
8
+ def initialize(client,global_config=nil)
9
+ @client = client
10
+ @global_config = global_config
11
+ end
12
+
13
+ def process_workers(workers)
14
+ register_workers(workers)
15
+ set_max_worker_counts(workers)
16
+ register_global_workers(@global_config || workers)
17
+ set_max_global_worker_counts(@global_config || workers)
18
+ end
19
+
20
+ def provision_workers(balanced=false)
21
+ available = {}
22
+ workers = max_host_workers
23
+ workers.each do |worker, count|
24
+ i = 0
25
+ Integer(count).times do
26
+ if reserve_worker?(worker,balanced)
27
+ i += 1 if reserve_worker(worker)
28
+ end
29
+ end
30
+ available[worker] = i
31
+ end
32
+ available
33
+ end
34
+
35
+ def expire_workers(balanced=false)
36
+ removable = {}
37
+ workers = max_host_workers
38
+ workers.each do |worker, count|
39
+ i = 0
40
+ Integer(count).times do
41
+ if expire_worker?(worker,balanced)
42
+ i -= 1 if expire_worker(worker)
43
+ end
44
+ end
45
+ removable[worker] = i
46
+ end
47
+ removable
48
+ end
49
+
50
+ def release_workers
51
+ workers = max_host_workers
52
+ workers.keys.each do |worker|
53
+ host_running_count = local_running_count(worker)
54
+ host_running_count.times do
55
+ send_message(:hincrby, global_workers_running_key,worker,-1)
56
+ send_message(:hincrby, host_workers_running_key,worker,-1)
57
+ end
58
+ end
59
+ send_message(:del, host_workers_running_key)
60
+ send_message(:del, host_max_worker_key)
61
+ end
62
+
63
+ private
64
+
65
+ def register_workers(workers)
66
+ workers.each {|worker, count| register_worker(worker,0) }
67
+ end
68
+
69
+ def register_global_workers(workers)
70
+ workers.each {|worker, count| register_global_worker(worker,0) }
71
+ end
72
+
73
+ def set_max_worker_counts(workers)
74
+ workers.each_pair {|worker,count| set_max_worker_count(worker,count) }
75
+ end
76
+
77
+ def set_max_global_worker_counts(workers)
78
+ workers.each_pair {|worker,count| set_max_global_worker_count(worker,count) }
79
+ end
80
+
81
+ def register_worker(worker,count)
82
+ send_message(:hsetnx,"#{host_key}:workers_running",worker,count)
83
+ end
84
+
85
+ def register_global_worker(worker,count)
86
+ send_message(:hsetnx,"#{global_key}:workers_running",worker,count)
87
+ end
88
+
89
+ def set_max_worker_count(worker,count)
90
+ send_message(:hsetnx,"#{host_key}:max_workers",worker,count)
91
+ end
92
+
93
+ def set_max_global_worker_count(worker,count)
94
+ send_message(:hsetnx,"#{global_key}:max_workers",worker,count)
95
+ end
96
+
97
+ def reserve_worker(worker)
98
+ adjust_workers(worker,1)
99
+ end
100
+
101
+ def expire_worker(worker)
102
+ adjust_workers(worker,-1)
103
+ end
104
+
105
+ def adjust_workers(worker,amount)
106
+ lock_key = "GRU:#{worker}"
107
+ if send_message(:setnx,lock_key,Time.now.to_i)
108
+ send_message(:hincrby,host_workers_running_key,worker,amount)
109
+ send_message(:hincrby,global_workers_running_key,worker,amount)
110
+ send_message(:del,lock_key)
111
+ return true
112
+ end
113
+ false
114
+ end
115
+
116
+ def max_host_workers
117
+ send_message(:hgetall,host_max_worker_key)
118
+ end
119
+
120
+ def reserve_worker?(worker,balanced)
121
+ host_running,global_running,host_max,global_max = worker_counts(worker)
122
+ result = false
123
+ if balanced
124
+ result = host_running.to_i < max_workers_per_host(global_max,host_max)
125
+ else
126
+ result = host_running.to_i < host_max.to_i
127
+ end
128
+ result && global_running.to_i < global_max.to_i
129
+ end
130
+
131
+ def expire_worker?(worker,balanced)
132
+ host_running,global_running,host_max,global_max = worker_counts(worker)
133
+ result = false
134
+ if balanced
135
+ result = host_running.to_i > max_workers_per_host(global_max,host_max)
136
+ else
137
+ result = host_running.to_i > host_max.to_i
138
+ end
139
+ (result || global_running.to_i > global_max.to_i) && host_running.to_i >= 0
140
+ end
141
+
142
+ def worker_counts(worker)
143
+ @client.multi do |multi|
144
+ multi.hget(host_workers_running_key,worker)
145
+ multi.hget(global_workers_running_key,worker)
146
+ multi.hget(host_max_worker_key,worker)
147
+ multi.hget(global_max_worker_key,worker)
148
+ end
149
+ end
150
+
151
+ def local_running_count(worker)
152
+ send_message(:hget,host_workers_running_key,worker).to_i
153
+ end
154
+
155
+ def gru_host_count
156
+ send_message(:keys,"GRU:*:workers_running").count - 1
157
+ end
158
+
159
+ def max_workers_per_host(global_worker_max_count,host_max)
160
+ host_count = gru_host_count
161
+ rebalance_count = host_count > 0 ? (global_worker_max_count.to_i/host_count.to_f).ceil : host_max.to_i
162
+ rebalance_count <= host_max.to_i && host_count > 1 ? rebalance_count : host_max.to_i
163
+ end
164
+
165
+ def host_max_worker_key
166
+ "#{host_key}:max_workers"
167
+ end
168
+
169
+ def host_workers_running_key
170
+ "#{host_key}:workers_running"
171
+ end
172
+
173
+ def global_max_worker_key
174
+ "#{global_key}:max_workers"
175
+ end
176
+
177
+ def global_workers_running_key
178
+ "#{global_key}:workers_running"
179
+ end
180
+
181
+ def global_key
182
+ "GRU:global"
183
+ end
184
+
185
+ def host_key
186
+ "GRU:#{hostname}"
187
+ end
188
+
189
+ def hostname
190
+ @hostname ||= Socket.gethostname
191
+ end
192
+
193
+ def send_message(action,*args)
194
+ @client.send(action,*args)
195
+ end
196
+
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,3 @@
1
+ module Gru
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,54 @@
1
+ module Gru
2
+ class WorkerManager
3
+ attr_reader :workers, :adapter
4
+
5
+ def self.create(settings,workers)
6
+ redis = Redis.new(settings)
7
+ adapter = Gru::Adapters::RedisAdapter.new(redis)
8
+ new(adapter,workers)
9
+ end
10
+
11
+ def self.with_redis_connection(client,workers,global_config=nil,balanced=false)
12
+ adapter = Gru::Adapters::RedisAdapter.new(client,global_config)
13
+ new(adapter,workers,balanced)
14
+ end
15
+
16
+ def initialize(adapter,workers,balanced=false)
17
+ @adapter = adapter
18
+ @workers = workers
19
+ @balanced = balanced
20
+ end
21
+
22
+ def register_worker_queues
23
+ @adapter.process_workers(@workers)
24
+ end
25
+
26
+ def provision_workers
27
+ @adapter.provision_workers(@balanced)
28
+ end
29
+
30
+ def expire_workers
31
+ @adapter.expire_workers(@balanced)
32
+ end
33
+
34
+ def adjust_workers
35
+ result = {}
36
+ add = provision_workers
37
+ remove = expire_workers
38
+ keys = add.keys + remove.keys
39
+ keys.uniq.each do |key|
40
+ result[key] = add.fetch(key) {0} + remove.fetch(key) {0}
41
+ end
42
+ result
43
+ end
44
+
45
+ def release_workers
46
+ @adapter.release_workers
47
+ end
48
+
49
+ def cleanup
50
+ @adapter.cleanup
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,204 @@
1
+ require 'rspec'
2
+ require_relative '../../../lib/gru'
3
+
4
+ describe Gru::Adapters::RedisAdapter do
5
+ before(:each) do
6
+ allow(Socket).to receive(:gethostname).and_return(hostname)
7
+ end
8
+
9
+ let(:hostname) { 'foo' }
10
+ let(:client) { double('client') }
11
+
12
+ let(:adapter) {
13
+ Gru::Adapters::RedisAdapter.new(client)
14
+ }
15
+
16
+ let(:workers) { { 'test_worker' => 3 } }
17
+
18
+ context "initialization" do
19
+ it "has a client" do
20
+ expect(adapter.client).to eq(client)
21
+ end
22
+ end
23
+
24
+ context "processing workers" do
25
+
26
+ it "determines the host key" do
27
+ expect(adapter.send(:host_key)).to eq("GRU:#{hostname}")
28
+ end
29
+
30
+ it "registers workers" do
31
+ expect(client).to receive(:hsetnx).with("GRU:#{hostname}:workers_running",'test_worker',0)
32
+ adapter.send(:register_workers,workers)
33
+ end
34
+
35
+ it "sets worker counts" do
36
+ expect(client).to receive(:hsetnx).with("GRU:#{hostname}:max_workers",'test_worker',3)
37
+ adapter.send(:set_max_worker_counts,workers)
38
+ end
39
+
40
+ it "sets global worker counts" do
41
+ expect(client).to receive(:hsetnx).with("GRU:global:max_workers",'test_worker',3)
42
+ adapter.send(:set_max_global_worker_counts,workers)
43
+ end
44
+
45
+ end
46
+
47
+ context "Determining Available Workers" do
48
+
49
+ it "gets all workers from redis" do
50
+ expect(client).to receive(:hgetall).with("GRU:#{hostname}:max_workers").and_return({
51
+ 'test_worker' => 3
52
+ })
53
+ adapter.send(:max_host_workers)
54
+ end
55
+
56
+ context "Provisioning workers with same local and global max" do
57
+ before(:each) do
58
+ expect(client).to receive(:hgetall).with("GRU:#{hostname}:max_workers").and_return(workers)
59
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(1).times
60
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(1).times
61
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(1).times
62
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(1).times
63
+ end
64
+
65
+ it "returns workers with 0 existing workers" do
66
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([0,0,3,3])
67
+ expect(client).to receive(:setnx).exactly(3).times.and_return(true)
68
+ expect(client).to receive(:del).with("GRU:test_worker").exactly(3).times
69
+ expect(client).to receive(:hincrby).with("GRU:global:workers_running",'test_worker',1).exactly(3).times
70
+ expect(client).to receive(:hincrby).with("GRU:#{hostname}:workers_running",'test_worker',1).exactly(3).times
71
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(2).times
72
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(2).times
73
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(2).times
74
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(2).times
75
+ available_workers = adapter.provision_workers
76
+ expect(available_workers).to eq({'test_worker' => 3})
77
+ end
78
+
79
+ it "returns workers when max local and global counts have not been reached" do
80
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([1,1,3,3])
81
+ expect(client).to receive(:setnx).exactly(3).times.and_return(true)
82
+ expect(client).to receive(:del).with("GRU:test_worker").exactly(3).times
83
+ expect(client).to receive(:hincrby).with("GRU:global:workers_running",'test_worker',1).exactly(3).times
84
+ expect(client).to receive(:hincrby).with("GRU:#{hostname}:workers_running",'test_worker',1).exactly(3).times
85
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(2).times
86
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(2).times
87
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(2).times
88
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(2).times
89
+ available_workers = adapter.provision_workers
90
+ expect(available_workers).to eq({'test_worker' => 3})
91
+ end
92
+
93
+ it "does not return workers if max global count has been reached" do
94
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([0,3,3,3])
95
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(2).times
96
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(2).times
97
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(2).times
98
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(2).times
99
+ available_workers = adapter.provision_workers
100
+ expect(available_workers).to eq({'test_worker' => 0})
101
+ end
102
+
103
+ it "doesn't return workers if max local count has been reached" do
104
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([3,4,3,6])
105
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(2).times
106
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(2).times
107
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(2).times
108
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(2).times
109
+ reserved_workers = adapter.provision_workers
110
+ expect(reserved_workers).to eq({'test_worker' => 0})
111
+ end
112
+
113
+ it "doesn't return workers if global max is 0" do
114
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([0,0,3,0])
115
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(2).times
116
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(2).times
117
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(2).times
118
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(2).times
119
+ available_workers = adapter.provision_workers
120
+ expect(available_workers).to eq({'test_worker' => 0})
121
+ end
122
+
123
+ it "doesn't provision workers if local max is 0" do
124
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([0,1,0,3])
125
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(2).times
126
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(2).times
127
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(2).times
128
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(2).times
129
+ available_workers = adapter.provision_workers
130
+ expect(available_workers).to eq({'test_worker' => 0})
131
+ end
132
+ end
133
+ end
134
+
135
+ context "Determining Removeable Workers" do
136
+ let(:workers) {
137
+ { 'test_worker' => 1 }
138
+ }
139
+
140
+ before(:each) do
141
+ expect(client).to receive(:hgetall).with("GRU:#{hostname}:max_workers").and_return(workers)
142
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(1).times
143
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(1).times
144
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(1).times
145
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(1).times
146
+ end
147
+
148
+ it "removes workers when local maximum has been exceeded" do
149
+ expect(client).to receive(:multi).exactly(1).times.and_yield(client).and_return([3,3,1,3])
150
+ expect(client).to receive(:setnx).exactly(1).times.and_return(true)
151
+ expect(client).to receive(:del).with("GRU:test_worker").exactly(1).times
152
+ expect(client).to receive(:hincrby).with("GRU:global:workers_running",'test_worker',-1).exactly(1).times
153
+ expect(client).to receive(:hincrby).with("GRU:#{hostname}:workers_running",'test_worker',-1).exactly(1).times
154
+ expect(adapter.expire_workers).to eq({'test_worker' => -1})
155
+ end
156
+
157
+ it "removes workers when global maximum has been exceeded" do
158
+ expect(client).to receive(:multi).exactly(1).times.and_yield(client).and_return([3,3,3,1])
159
+ expect(client).to receive(:setnx).exactly(1).times.and_return(true)
160
+ expect(client).to receive(:del).with("GRU:test_worker").exactly(1).times
161
+ expect(client).to receive(:hincrby).with("GRU:global:workers_running",'test_worker',-1).exactly(1).times
162
+ expect(client).to receive(:hincrby).with("GRU:#{hostname}:workers_running",'test_worker',-1).exactly(1).times
163
+ expect(adapter.expire_workers).to eq({'test_worker' => -1})
164
+ end
165
+
166
+ it "doesn't remove workers when local maximum has not been exceeded" do
167
+ expect(client).to receive(:multi).exactly(1).times.and_yield(client).and_return([3,3,4,3])
168
+ expect(adapter.expire_workers).to eq({'test_worker' => 0})
169
+ end
170
+
171
+ it "doesn't remove workers when global maximum has not been exceeded" do
172
+ expect(client).to receive(:multi).exactly(1).times.and_yield(client).and_return([3,4,3,5])
173
+ expect(adapter.expire_workers).to eq({'test_worker' => 0})
174
+ end
175
+ end
176
+
177
+ context "Rebalancing workers" do
178
+
179
+ before(:each) do
180
+
181
+ end
182
+
183
+ it "reduces load when workers are added" do
184
+ expect(client).to receive(:multi).exactly(3).times.and_yield(client).and_return([2,4,3,5])
185
+ expect(client).to receive(:hgetall).with("GRU:#{hostname}:max_workers").and_return(workers)
186
+ expect(client).to receive(:hget).with("GRU:#{hostname}:workers_running",'test_worker').exactly(3).times
187
+ expect(client).to receive(:hget).with("GRU:global:workers_running",'test_worker').exactly(3).times
188
+ expect(client).to receive(:hget).with("GRU:#{hostname}:max_workers",'test_worker').exactly(3).times
189
+ expect(client).to receive(:hget).with("GRU:global:max_workers",'test_worker').exactly(3).times
190
+ expect(client).to receive(:keys).with("GRU:*:workers_running").exactly(3).times.and_return(['foo'])
191
+ expect(client).to receive(:setnx).exactly(3).times.and_return(true)
192
+ expect(client).to receive(:hincrby).with("GRU:global:workers_running",'test_worker',1).exactly(3).times
193
+ expect(client).to receive(:hincrby).with("GRU:#{hostname}:workers_running",'test_worker',1).exactly(3).times
194
+ expect(client).to receive(:del).with("GRU:test_worker").exactly(3).times
195
+ adapter.provision_workers(true)
196
+
197
+ end
198
+
199
+ it "increases load when workers are removed" do
200
+
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,46 @@
1
+ require 'rspec'
2
+ require './lib/gru'
3
+
4
+ describe Gru::WorkerManager do
5
+ let (:adapter) {
6
+ double("adapter")
7
+ }
8
+
9
+ let(:manager) {
10
+ Gru::WorkerManager.new(adapter, workers)
11
+ }
12
+
13
+ let(:workers) {
14
+ {}
15
+ }
16
+
17
+ context "When initialized" do
18
+ it "has workers" do
19
+ expect(manager.workers).not_to be_nil
20
+ end
21
+
22
+ it "has an adapter instance" do
23
+ expect(manager.adapter).not_to be_nil
24
+ end
25
+ end
26
+
27
+ context "Creating Worker Queues" do
28
+ it "Creates workers" do
29
+ expect(adapter).to receive(:process_workers).with(workers).and_return(true)
30
+ manager.register_worker_queues
31
+ end
32
+ end
33
+
34
+ context "Finding available or removeable workers" do
35
+ it "determines new workers to create" do
36
+ expect(adapter).to receive(:provision_workers).and_return({})
37
+ expect(adapter).to receive(:expire_workers).and_return({})
38
+ manager.adjust_workers
39
+ end
40
+
41
+ it "determines workers to expire" do
42
+ expect(adapter).to receive(:expire_workers).and_return(instance_of(Array))
43
+ manager.expire_workers
44
+ end
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gru
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jeffrey Gillis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.1.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.1.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: awesome_print
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.0'
97
+ description: This is a worker/minion manager using different atomic data stores.
98
+ email:
99
+ - jeffrey.gillis1@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - Gemfile
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - gru.gemspec
110
+ - lib/gru.rb
111
+ - lib/gru/adapters.rb
112
+ - lib/gru/adapters/redis_adapter.rb
113
+ - lib/gru/version.rb
114
+ - lib/gru/worker_manager.rb
115
+ - spec/gru/adapters/redis_adapter_spec.rb
116
+ - spec/gru/worker_manager_spec.rb
117
+ homepage: ''
118
+ licenses:
119
+ - MIT
120
+ metadata: {}
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubyforge_project:
137
+ rubygems_version: 2.4.5
138
+ signing_key:
139
+ specification_version: 4
140
+ summary: An atomic worker/minion manager.
141
+ test_files:
142
+ - spec/gru/adapters/redis_adapter_spec.rb
143
+ - spec/gru/worker_manager_spec.rb