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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff5a0d8ca6659d4feeb9d6fd8e453e1b1733917c20dbfadb4686173998085903
4
- data.tar.gz: 5f3bb001fd3fde135999d50e2383d6f4e826c54a35c4ab02a11d45c7cc29bce3
3
+ metadata.gz: 6e999370930d2d5686a75214e30aa63223b828f77ba5ba94d48506f5afcca8c9
4
+ data.tar.gz: 494b772ddc967646f15f3ba1db809fd34eb6f12a0714242f327326e33af1b648
5
5
  SHA512:
6
- metadata.gz: e9c097fbdb451fb6de6574427f3b672a09db445cb0df2f0784bd9133adbb97c614a73177c6017f0b6fe57618da24b69734538d127c84b54ec8495514d1c86660
7
- data.tar.gz: 0404b8096e47387750cbef9b2ba89d5de6e795f19633c6d613ae202d211b510758db3d1d6d342a2f8c1da4a1d2e1d15727ec6034c38e2de4d6cac23341ffc4d2
6
+ metadata.gz: b2b8dd65c7cd2a5d96daa8c67fe2d7b3a71a77a9035ff00a065b693d179ec1d71f6084ab1d1d8b98f5b9d62bda7628e0d898a0c32d637bc1fcb02032e3e6d7fa
7
+ data.tar.gz: e4c85a817f9e0f9b2445e000b8f1f3c2cc79d74023020b8f02fa9f9895e61ee528f6b69aacc88dde8a3fbe6c04cb10f494075e7188bacf37e049486e516a2513
data/CHANGE_LOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 1.1.0
2
+
3
+ * Expose manually locking and unlocking processes.
4
+
5
+ * Allow passing in a redis connection in the constructor.
6
+
1
7
  # 1.0.1
2
8
 
3
9
  * Use Lua script to avoid race conditions and ensure no extra processes slip through.
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.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
- remove_process!(redis, process_id)
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(redis = nil)
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
@@ -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.1
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-03 00:00:00.000000000 Z
12
+ date: 2019-07-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redis