restrainer 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGE_LOG.md +6 -0
- data/README.md +20 -1
- data/VERSION +1 -1
- data/lib/restrainer.rb +38 -10
- data/spec/restrainer_spec.rb +67 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e999370930d2d5686a75214e30aa63223b828f77ba5ba94d48506f5afcca8c9
|
4
|
+
data.tar.gz: 494b772ddc967646f15f3ba1db809fd34eb6f12a0714242f327326e33af1b648
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2b8dd65c7cd2a5d96daa8c67fe2d7b3a71a77a9035ff00a065b693d179ec1d71f6084ab1d1d8b98f5b9d62bda7628e0d898a0c32d637bc1fcb02032e3e6d7fa
|
7
|
+
data.tar.gz: e4c85a817f9e0f9b2445e000b8f1f3c2cc79d74023020b8f02fa9f9895e61ee528f6b69aacc88dde8a3fbe6c04cb10f494075e7188bacf37e049486e516a2513
|
data/CHANGE_LOG.md
CHANGED
data/README.md
CHANGED
@@ -17,11 +17,24 @@ If the throttle is already full, the block will not be run and a `Restrainer::Th
|
|
17
17
|
|
18
18
|
You can also override the limit in the `throttle` method. Setting a limit of zero will disable processing entirely. Setting a limit less than zero will remove the limit. Note that the limit set in the throttle is not shared with other processes, but the count of the number of processes is shared. Thus it is possible to have the throttle allow one process but reject another if the limits are different.
|
19
19
|
|
20
|
+
You can also manually lock and release processes using the `lock` and `release` methods if your logic needs to break out of a block.
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
process_id = restrainer.lock!
|
24
|
+
begin
|
25
|
+
# Do something
|
26
|
+
ensure
|
27
|
+
restrainer.release!(process_id)
|
28
|
+
end
|
29
|
+
```
|
30
|
+
|
31
|
+
If you already hava a unique identifier, you can pass it in to the lock! method. This can be useful if the calls to `lock!` and `release!` are in different parts of the code but have access to the same common identifier. Identifiers are unique per throttle name, so you can use something as simple as database row id.
|
32
|
+
|
20
33
|
Instances of Restrainer do not use any internal state to keep track of the number of running processes. All of that information is maintained in redis. Therefore you don't need to worry about maintaining references to Restrainer instances and you can create them as needed as long as they are named consistently. You can create multiple Restrainers for different uses in your application by simply giving them different names.
|
21
34
|
|
22
35
|
### Configuration
|
23
36
|
|
24
|
-
To set the redis connection used by for the gem you can either specify a block that yields a Redis object (from the [redis](https://github.com/redis/redis-rb) gem) or you can explicitly set the attribute. The block form is generally preferred since it can work with connection pools, etc.
|
37
|
+
To set the redis connection used by for the gem you can either specify a block that yields a `Redis` object (from the [redis](https://github.com/redis/redis-rb) gem) or you can explicitly set the attribute. The block form is generally preferred since it can work with connection pools, etc.
|
25
38
|
|
26
39
|
```ruby
|
27
40
|
Restrainer.redis{ connection_pool.redis }
|
@@ -29,6 +42,12 @@ Restrainer.redis{ connection_pool.redis }
|
|
29
42
|
Restrainer.redis = redis_client
|
30
43
|
```
|
31
44
|
|
45
|
+
You can also pass in a `Redis` instance in the constructor.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
restrainer = Restrainer.new(limit: 5, redis: my_redis)
|
49
|
+
```
|
50
|
+
|
32
51
|
### Internals
|
33
52
|
|
34
53
|
To protect against situations where a process is killed without a chance to cleanup after itself (i.e. `kill -9`), each process is only tracked for a limited amount of time (one minute by default). After this time, the Restrainer will assume that the process has been orphaned and removes it from the list.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.0
|
1
|
+
1.1.0
|
data/lib/restrainer.rb
CHANGED
@@ -88,11 +88,12 @@ class Restrainer
|
|
88
88
|
# if their process is killed. Processes will automatically be removed from the running jobs list after the
|
89
89
|
# specified number of seconds. Note that the Restrainer will not handle timing out any code itself. This
|
90
90
|
# value is just used to insure the integrity of internal data structures.
|
91
|
-
def initialize(name, limit:, timeout: 60)
|
91
|
+
def initialize(name, limit:, timeout: 60, redis: nil)
|
92
92
|
@name = name
|
93
93
|
@limit = limit
|
94
94
|
@timeout = timeout
|
95
95
|
@key = "#{self.class.name}.#{name.to_s}"
|
96
|
+
@redis ||= redis
|
96
97
|
end
|
97
98
|
|
98
99
|
# Wrap a block with this method to throttle concurrent execution. If more than the alotted number
|
@@ -105,28 +106,55 @@ class Restrainer
|
|
105
106
|
|
106
107
|
# limit of less zero is no limit; limit of zero is allow none
|
107
108
|
return yield if limit < 0
|
108
|
-
raise ThrottledError.new("#{self.class}: #{@name} is not allowing any processing") if limit == 0
|
109
|
-
|
110
|
-
# Grab a reference to the redis instance to that it will be consistent throughout the method
|
111
|
-
redis = self.class.redis
|
112
|
-
process_id = SecureRandom.uuid
|
113
|
-
add_process!(redis, process_id, limit)
|
114
109
|
|
110
|
+
process_id = lock!(limit: limit)
|
115
111
|
begin
|
116
112
|
yield
|
117
113
|
ensure
|
118
|
-
|
114
|
+
release!(process_id)
|
119
115
|
end
|
120
116
|
end
|
117
|
+
|
118
|
+
# Obtain a lock on one the allowed processes. The method returns a process
|
119
|
+
# identifier that must be passed to the release! to release the lock.
|
120
|
+
# You can pass in a unique identifier if you already have one.
|
121
|
+
#
|
122
|
+
# Raises a Restrainer::ThrottledError if the lock cannot be obtained.
|
123
|
+
#
|
124
|
+
# The limit argument can be used to override the value set in the constructor.
|
125
|
+
def lock!(process_id = nil, limit: limit)
|
126
|
+
process_id ||= SecureRandom.uuid
|
127
|
+
limit ||= self.limit
|
128
|
+
|
129
|
+
# limit of less zero is no limit; limit of zero is allow none
|
130
|
+
return nil if limit < 0
|
131
|
+
raise ThrottledError.new("#{self.class}: #{@name} is not allowing any processing") if limit == 0
|
132
|
+
|
133
|
+
add_process!(redis, process_id, limit)
|
134
|
+
process_id
|
135
|
+
end
|
136
|
+
|
137
|
+
# release one of the allowed processes. You must pass in a process id returned by the lock method.
|
138
|
+
def release!(process_id)
|
139
|
+
remove_process!(redis, process_id) unless process_id.nil?
|
140
|
+
end
|
121
141
|
|
122
142
|
# Get the number of processes currently being executed for this restrainer.
|
123
|
-
def current
|
124
|
-
redis ||= self.class.redis
|
143
|
+
def current
|
125
144
|
redis.zcard(key).to_i
|
126
145
|
end
|
146
|
+
|
147
|
+
# Clear all locks
|
148
|
+
def clear!
|
149
|
+
redis.del(key)
|
150
|
+
end
|
127
151
|
|
128
152
|
private
|
129
153
|
|
154
|
+
def redis
|
155
|
+
@redis || self.class.redis
|
156
|
+
end
|
157
|
+
|
130
158
|
# Hash key in redis to story a sorted set of current processes.
|
131
159
|
def key
|
132
160
|
@key
|
data/spec/restrainer_spec.rb
CHANGED
@@ -3,21 +3,25 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
|
5
5
|
describe Restrainer do
|
6
|
-
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
Restrainer.new(:restrainer_test, limit: 1).clear!
|
9
|
+
end
|
10
|
+
|
7
11
|
it "should have a name and max_processes" do
|
8
12
|
restrainer = Restrainer.new(:restrainer_test, limit: 1)
|
9
13
|
expect(restrainer.name).to eq(:restrainer_test)
|
10
14
|
expect(restrainer.limit).to eq(1)
|
11
15
|
end
|
12
|
-
|
13
|
-
it "should run a block" do
|
16
|
+
|
17
|
+
it "should run a block!" do
|
14
18
|
restrainer = Restrainer.new(:restrainer_test, limit: 1)
|
15
19
|
x = nil
|
16
20
|
expect(restrainer.throttle{ x = restrainer.current }).to eq(1)
|
17
21
|
expect(x).to eq(1)
|
18
22
|
expect(restrainer.current).to eq(0)
|
19
23
|
end
|
20
|
-
|
24
|
+
|
21
25
|
it "should throw an error if too many processes are already running" do
|
22
26
|
restrainer = Restrainer.new(:restrainer_test, limit: 5)
|
23
27
|
x = nil
|
@@ -34,7 +38,7 @@ describe Restrainer do
|
|
34
38
|
end
|
35
39
|
expect(x).to eq(nil)
|
36
40
|
end
|
37
|
-
|
41
|
+
|
38
42
|
it "should not throw an error if the number of processes is under the limit" do
|
39
43
|
restrainer = Restrainer.new(:restrainer_test, limit: 2)
|
40
44
|
x = nil
|
@@ -43,7 +47,7 @@ describe Restrainer do
|
|
43
47
|
end
|
44
48
|
expect(x).to eq(1)
|
45
49
|
end
|
46
|
-
|
50
|
+
|
47
51
|
it "should let the throttle method override the limit" do
|
48
52
|
restrainer = Restrainer.new(:restrainer_test, limit: 1)
|
49
53
|
x = nil
|
@@ -52,21 +56,21 @@ describe Restrainer do
|
|
52
56
|
end
|
53
57
|
expect(x).to eq(1)
|
54
58
|
end
|
55
|
-
|
59
|
+
|
56
60
|
it "should allow processing to be turned off entirely by setting the limit to zero" do
|
57
61
|
restrainer = Restrainer.new(:restrainer_test, limit: 1)
|
58
62
|
x = nil
|
59
63
|
expect(lambda{restrainer.throttle(limit: 0){ x = 1 }}).to raise_error(Restrainer::ThrottledError)
|
60
64
|
expect(x).to eq(nil)
|
61
65
|
end
|
62
|
-
|
66
|
+
|
63
67
|
it "should allow the throttle to be opened up entirely with a negative limit" do
|
64
68
|
restrainer = Restrainer.new(:restrainer_test, limit: 0)
|
65
69
|
x = nil
|
66
70
|
restrainer.throttle(limit: -1){ x = 1 }
|
67
71
|
expect(x).to eq(1)
|
68
72
|
end
|
69
|
-
|
73
|
+
|
70
74
|
it "should cleanup the running process list if orphaned processes exist" do
|
71
75
|
restrainer = Restrainer.new(:restrainer_test, limit: 1, timeout: 10)
|
72
76
|
x = nil
|
@@ -77,4 +81,58 @@ describe Restrainer do
|
|
77
81
|
end
|
78
82
|
expect(x).to eq(1)
|
79
83
|
end
|
84
|
+
|
85
|
+
it "should be able to lock! and release! processes manually" do
|
86
|
+
restrainer = Restrainer.new(:restrainer_test, limit: 5)
|
87
|
+
p1 = restrainer.lock!
|
88
|
+
begin
|
89
|
+
p2 = restrainer.lock!
|
90
|
+
begin
|
91
|
+
p3 = restrainer.lock!
|
92
|
+
begin
|
93
|
+
p4 = restrainer.lock!
|
94
|
+
begin
|
95
|
+
p5 = restrainer.lock!
|
96
|
+
begin
|
97
|
+
expect{ restrainer.lock! }.to raise_error(Restrainer::ThrottledError)
|
98
|
+
ensure
|
99
|
+
restrainer.release!(p5)
|
100
|
+
end
|
101
|
+
p6 = restrainer.lock!
|
102
|
+
restrainer.release!(p6)
|
103
|
+
ensure
|
104
|
+
restrainer.release!(p4)
|
105
|
+
end
|
106
|
+
ensure
|
107
|
+
restrainer.release!(p3)
|
108
|
+
end
|
109
|
+
ensure
|
110
|
+
restrainer.release!(p2)
|
111
|
+
end
|
112
|
+
ensure
|
113
|
+
restrainer.release!(p1)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should be able to pass in the process id" do
|
118
|
+
restrainer = Restrainer.new(:restrainer_test, limit: 1)
|
119
|
+
expect(restrainer.lock!("foo")).to eq "foo"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should not get a lock! if the limit is 0" do
|
123
|
+
restrainer = Restrainer.new(:restrainer_test, limit: 0)
|
124
|
+
expect{ restrainer.lock! }.to raise_error(Restrainer::ThrottledError)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should get a lock! if the limit is negative" do
|
128
|
+
restrainer = Restrainer.new(:restrainer_test, limit: -1)
|
129
|
+
process_id = restrainer.lock!
|
130
|
+
expect(process_id).to eq nil
|
131
|
+
restrainer.release!(nil)
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should be able to override the limit in lock!" do
|
135
|
+
restrainer = Restrainer.new(:restrainer_test, limit: 0)
|
136
|
+
restrainer.lock!(limit: 1)
|
137
|
+
end
|
80
138
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restrainer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- We Heart It
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-07-
|
12
|
+
date: 2019-07-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|