circuitry 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +177 -6
- data/circuitry.gemspec +5 -1
- data/lib/circuitry/configuration.rb +1 -0
- data/lib/circuitry/locks/base.rb +41 -0
- data/lib/circuitry/locks/memcache.rb +30 -0
- data/lib/circuitry/locks/memory.rb +59 -0
- data/lib/circuitry/locks/noop.rb +17 -0
- data/lib/circuitry/locks/redis.rb +30 -0
- data/lib/circuitry/processor.rb +1 -1
- data/lib/circuitry/publisher.rb +13 -2
- data/lib/circuitry/subscriber.rb +35 -11
- data/lib/circuitry/version.rb +1 -1
- data/lib/circuitry.rb +5 -0
- metadata +64 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aabb7e5d67924cf49e3d6efdd5f34c27d4d850f6
|
4
|
+
data.tar.gz: b63bad1d70be23cec5c666b699f8473b25a2d7cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 01e354ad6cfaccaf54184d932714609fbad3e95c9f0a35137e756abe5448fc4fda5b7a8f52333d7dc7f05e4b80d4d2cb2c3a30b52e271e90cc70ddd2e6efd4ab
|
7
|
+
data.tar.gz: 146e463dc1f63ea887ec99ffbd350d9b28d9f3aff2d96fca12ce407c200a2f3a1480ce2b4be15747980f533ec00e2716854607ec53e46249b6a705e74d137bcf
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -36,6 +36,7 @@ Circuitry.config do |c|
|
|
36
36
|
HoneyBadger.notify(error)
|
37
37
|
HoneyBadger.flush
|
38
38
|
end
|
39
|
+
c.lock_strategy = Circuitry::Lock::Redis.new(url: 'redis://localhost:6379')
|
39
40
|
c.publish_async_strategy = :batch
|
40
41
|
c.subscribe_async_strategy = :thread
|
41
42
|
end
|
@@ -54,6 +55,9 @@ Available configuration options include:
|
|
54
55
|
* `error_handler`: An object that responds to `call` with two arguments: the
|
55
56
|
deserialized message contents and the topic name used when publishing to SNS.
|
56
57
|
*(optional, default: `nil`)*
|
58
|
+
* `:lock_strategy` - The store used to ensure that no duplicate messages are
|
59
|
+
processed. Please refer to the [Lock Strategies](#lock-strategies) section for
|
60
|
+
more details regarding this option. *(default: `Circuitry::Locks::Memory.new`)*
|
57
61
|
* `publish_async_strategy`: One of `:fork`, `:thread`, or `:batch` that
|
58
62
|
determines how asynchronous publish requests are processed. *(optional,
|
59
63
|
default: `:fork`)*
|
@@ -90,10 +94,14 @@ The `publish` method also accepts options that impact instantiation of the
|
|
90
94
|
the `publish_async_strategy` value from the gem configuration. Please refer to
|
91
95
|
the [Asynchronous Support](#asynchronous-support) section for more details
|
92
96
|
regarding this option. *(default: `false`)*
|
97
|
+
* `:timeout` - The maximum amount of time in seconds that publishing a message
|
98
|
+
will be attempted before giving up. If the timeout is exceeded, an exception
|
99
|
+
will raised to be handled by your application or `error_handler`. *(default:
|
100
|
+
15)*
|
93
101
|
|
94
102
|
```ruby
|
95
103
|
obj = { foo: 'foo', bar: 'bar' }
|
96
|
-
Circuitry.publish('my-topic-name', obj, async: true)
|
104
|
+
Circuitry.publish('my-topic-name', obj, async: true, timeout: 20)
|
97
105
|
```
|
98
106
|
|
99
107
|
Alternatively, if your options hash will remain unchanged, you can build a single
|
@@ -120,13 +128,22 @@ end
|
|
120
128
|
The `subscribe` method also accepts options that impact instantiation of the
|
121
129
|
`Subscriber` object, which currently includes the following options.
|
122
130
|
|
131
|
+
* `:lock` - The strategy used to ensure that no duplicate messages are processed.
|
132
|
+
Accepts `true`, `false`, or an instance of a class inheriting from
|
133
|
+
`Circuitry::Locks::Base`. Passing `true` uses the `lock_strategy` value from
|
134
|
+
the gem configuration. Passing `false` uses the [NOOP](#NOOP) strategy. Please
|
135
|
+
refer to the [Lock Strategies](#lock-strategies) section for more details
|
136
|
+
regarding this option. *(default: `true`)*
|
123
137
|
* `:async` - Whether or not subscribing should occur in the background. Accepts
|
124
138
|
one of `:fork`, `:thread`, `true`, or `false`. Passing `true` uses the
|
125
139
|
`subscribe_async_strategy` value from the gem configuration. Passing an
|
126
|
-
asynchronous value will cause messages to be handled concurrently
|
127
|
-
|
128
|
-
|
129
|
-
|
140
|
+
asynchronous value will cause messages to be handled concurrently. Please
|
141
|
+
refer to the [Asynchronous Support](#asynchronous-support) section for more
|
142
|
+
details regarding this option. *(default: `false`)*
|
143
|
+
* `:timeout` - The maximum amount of time in seconds that processing a message
|
144
|
+
will be attempted before giving up. If the timeout is exceeded, an exception
|
145
|
+
will raised to be handled by your application or `error_handler`. *(default:
|
146
|
+
15)*
|
130
147
|
* `:wait_time` - The number of seconds to wait for messages while connected to
|
131
148
|
SQS. Anything above 0 results in long-polling, while 0 results in
|
132
149
|
short-polling. *(default: 10)*
|
@@ -134,7 +151,15 @@ The `subscribe` method also accepts options that impact instantiation of the
|
|
134
151
|
*(default: 10)*
|
135
152
|
|
136
153
|
```ruby
|
137
|
-
|
154
|
+
options = {
|
155
|
+
lock: true,
|
156
|
+
async: true,
|
157
|
+
timeout: 20,
|
158
|
+
wait_time: 60,
|
159
|
+
batch_size: 20
|
160
|
+
}
|
161
|
+
|
162
|
+
Circuitry.subscribe('https://...', options) do |message, topic_name|
|
138
163
|
# ...
|
139
164
|
end
|
140
165
|
```
|
@@ -204,6 +229,152 @@ Batched publish and subscribe requests are queued in memory and do not begin
|
|
204
229
|
processing until you explicit flush them. This can be done by calling
|
205
230
|
`Circuitry.flush`.
|
206
231
|
|
232
|
+
### Lock Strategies
|
233
|
+
|
234
|
+
The [Amazon SQS FAQ](http://aws.amazon.com/sqs/faqs/) includes the following
|
235
|
+
important point:
|
236
|
+
|
237
|
+
> Amazon SQS is engineered to provide “at least once” delivery of all messages in
|
238
|
+
> its queues. Although most of the time each message will be delivered to your
|
239
|
+
> application exactly once, you should design your system so that processing a
|
240
|
+
> message more than once does not create any errors or inconsistencies.
|
241
|
+
|
242
|
+
Given this, it's up to the user to ensure messages are not processed multiple
|
243
|
+
times in the off chance that Amazon does not recognize that a message has been
|
244
|
+
processed.
|
245
|
+
|
246
|
+
The circuitry gem handles this by caching SQS message IDs: first via a "soft
|
247
|
+
lock" that denotes the message is about to be processed, then via a "hard lock"
|
248
|
+
that denotes the message has finished processing.
|
249
|
+
|
250
|
+
The soft lock has a default TTL of 15 minutes (a seemingly sane amount of time
|
251
|
+
during which processing most queue messages should certainly be able to
|
252
|
+
complete), while the hard lock has a default TTL of 24 hours (based upon
|
253
|
+
[a suggestion by an AWS employee](https://forums.aws.amazon.com/thread.jspa?threadID=140782#507605)).
|
254
|
+
The soft and hard TTL values can be changed by passing a `:soft_ttl` or
|
255
|
+
`:hard_ttl` value to the lock initializer, representing the number of seconds
|
256
|
+
that a lock should persist. For example:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
Circuitry.config.lock_strategy = Circuitry::Lock::Memory.new(
|
260
|
+
soft_ttl: 10 * 60, # 10 minutes
|
261
|
+
hard_ttl: 48 * 60 * 60 # 48 hours
|
262
|
+
)
|
263
|
+
```
|
264
|
+
|
265
|
+
#### Memory
|
266
|
+
|
267
|
+
If not specified in your circuitry configuration, the memory store will be used
|
268
|
+
by default. This lock strategy is provided as the lowest barrier to entry given
|
269
|
+
that it has no third-party dependencies. It should be avoided if running
|
270
|
+
multiple subscriber processes or if expecting a high throughput that would result
|
271
|
+
in a large amount of memory consumption.
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
Circuitry::Lock::Memory.new
|
275
|
+
```
|
276
|
+
|
277
|
+
#### Redis
|
278
|
+
|
279
|
+
Using the redis lock strategy requires that you add `gem 'redis'` to your
|
280
|
+
`Gemfile`, as it is not included bundled with the circuitry gem by default.
|
281
|
+
|
282
|
+
There are two ways to use the redis lock strategy. The first is to pass your
|
283
|
+
redis connection options to the lock in the same way that you would when building
|
284
|
+
a new `Redis` object.
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
Circuitry::Lock::Redis.new(url: 'redis://localhost:6379')
|
288
|
+
```
|
289
|
+
|
290
|
+
The second way is to pass in a `:client` option that specifies the redis client
|
291
|
+
itself. This is useful for more advanced usage such as sharing an existing redis
|
292
|
+
connection, utilizing [Redis::Namespace](https://github.com/resque/redis-namespace),
|
293
|
+
or utilizing [hiredis](https://github.com/redis/hiredis-rb).
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
client = Redis.new(url: 'redis://localhost:6379')
|
297
|
+
Circuitry::Lock::Redis.new(client: client)
|
298
|
+
```
|
299
|
+
|
300
|
+
#### Memcache
|
301
|
+
|
302
|
+
Using the memcache lock strategy requires that you add `gem 'dalli'` to your
|
303
|
+
`Gemfile`, as it is not included bundled with the circuitry gem by default.
|
304
|
+
|
305
|
+
There are two ways to use the memcache lock strategy. The first is to pass your
|
306
|
+
dalli connection host and options to the lock in the same way that you would when
|
307
|
+
building a new `Dalli::Client` object. The special `host` option will be treated
|
308
|
+
as the memcache host, just as the first argument to `Dalli::Client`.
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
Circuitry::Lock::Memcache.new(host: 'localhost:11211', namespace: '...')
|
312
|
+
```
|
313
|
+
|
314
|
+
The second way is to pass in a `:client` option that specifies the dalli client
|
315
|
+
itself. This is useful for sharing an existing memcache connection.
|
316
|
+
|
317
|
+
```ruby
|
318
|
+
client = Dalli::Client.new('localhost:11211', namespace: '...')
|
319
|
+
Circuitry::Lock::Memcache.new(client: client)
|
320
|
+
```
|
321
|
+
|
322
|
+
#### NOOP
|
323
|
+
|
324
|
+
Using the noop lock strategy permits you to continue to treat SQS as a
|
325
|
+
distributed queue in a true sense, meaning that you might receive duplicate
|
326
|
+
messages. Please refer to the Amazon SQS documentation pertaining to the
|
327
|
+
[Properties of Distributed Queues](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/DistributedQueues.html).
|
328
|
+
|
329
|
+
#### Custom
|
330
|
+
|
331
|
+
It's also possible to roll your own lock strategy. Simply create a class that
|
332
|
+
includes (or module that extends) `Circuitry::Lock::Base` and implements the
|
333
|
+
following methods:
|
334
|
+
|
335
|
+
* `lock`: Accepts the `key` and `ttl` as parameters. If the key is already
|
336
|
+
locked, this method must return false. If the key is not already locked, it
|
337
|
+
must lock the key for `ttl` seconds and return true. It is important that
|
338
|
+
the check and update are **atomic** in order to ensure the same message isn't
|
339
|
+
processed more than once.
|
340
|
+
* `lock!`: Accepts the `key` and `ttl` as parameters. Must lock the key for
|
341
|
+
`ttl` seconds regardless of whether or not the key was previously locked.
|
342
|
+
|
343
|
+
For example, a database-backed solution might look something like the following:
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
class DatabaseLockStrategy
|
347
|
+
include Circuitry::Lock::Base
|
348
|
+
|
349
|
+
def initialize(options = {})
|
350
|
+
super(options)
|
351
|
+
self.connection = options.fetch(:connection)
|
352
|
+
end
|
353
|
+
|
354
|
+
protected
|
355
|
+
|
356
|
+
def lock(key, ttl)
|
357
|
+
connection.exec("INSERT INTO locks (key, expires_at) VALUES ('#{key}', '#{Time.now + ttl}')")
|
358
|
+
end
|
359
|
+
|
360
|
+
def lock!(key, ttl)
|
361
|
+
connection.exec("UPSERT INTO locks (key, expires_at) VALUES ('#{key}', '#{Time.now + ttl}')")
|
362
|
+
end
|
363
|
+
|
364
|
+
private
|
365
|
+
|
366
|
+
attr_reader :connection
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
To use, simply create an instance of the class with your necessary options, and
|
371
|
+
pass your lock instance to the configuration as the `:lock_strategy`.
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
connection = PG.connect(...)
|
375
|
+
Circuitry.config.lock_strategy = DatabaseLockStrategy.new(connection: connection)
|
376
|
+
```
|
377
|
+
|
207
378
|
## Development
|
208
379
|
|
209
380
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
data/circuitry.gemspec
CHANGED
@@ -22,10 +22,14 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency 'fog-aws', '~> 0.4'
|
23
23
|
spec.add_dependency 'virtus', '~> 1.0'
|
24
24
|
|
25
|
-
spec.add_development_dependency 'pry-
|
25
|
+
spec.add_development_dependency 'pry-byebug'
|
26
26
|
spec.add_development_dependency 'bundler', '~> 1.8'
|
27
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
28
28
|
spec.add_development_dependency 'rspec', '~> 3.2'
|
29
29
|
spec.add_development_dependency 'rspec-its', '~> 1.2'
|
30
30
|
spec.add_development_dependency 'codeclimate-test-reporter'
|
31
|
+
spec.add_development_dependency 'redis'
|
32
|
+
spec.add_development_dependency 'mock_redis'
|
33
|
+
spec.add_development_dependency 'dalli'
|
34
|
+
spec.add_development_dependency 'memcache_mock'
|
31
35
|
end
|
@@ -10,6 +10,7 @@ module Circuitry
|
|
10
10
|
attribute :region, String, default: 'us-east-1'
|
11
11
|
attribute :logger, Logger, default: Logger.new(STDERR)
|
12
12
|
attribute :error_handler
|
13
|
+
attribute :lock_strategy, Object, default: ->(page, attribute) { Circuitry::Locks::Memory.new }
|
13
14
|
attribute :publish_async_strategy, Symbol, default: ->(page, attribute) { :fork }
|
14
15
|
attribute :subscribe_async_strategy, Symbol, default: ->(page, attribute) { :fork }
|
15
16
|
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Circuitry
|
2
|
+
module Locks
|
3
|
+
module Base
|
4
|
+
DEFAULT_SOFT_TTL = (15 * 60).freeze # 15 minutes
|
5
|
+
DEFAULT_HARD_TTL = (24 * 60 * 60).freeze # 24 hours
|
6
|
+
|
7
|
+
attr_reader :soft_ttl, :hard_ttl
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
self.soft_ttl = options.fetch(:soft_ttl, DEFAULT_SOFT_TTL)
|
11
|
+
self.hard_ttl = options.fetch(:hard_ttl, DEFAULT_HARD_TTL)
|
12
|
+
end
|
13
|
+
|
14
|
+
def soft_lock(id)
|
15
|
+
lock(lock_key(id), soft_ttl)
|
16
|
+
end
|
17
|
+
|
18
|
+
def hard_lock(id)
|
19
|
+
lock!(lock_key(id), hard_ttl)
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def lock(key, ttl)
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def lock!(key, ttl)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_writer :soft_ttl, :hard_ttl
|
35
|
+
|
36
|
+
def lock_key(id)
|
37
|
+
"circuitry:lock:#{id}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Circuitry
|
2
|
+
module Locks
|
3
|
+
class Memcache
|
4
|
+
include Base
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
super(options)
|
8
|
+
|
9
|
+
self.client = options.fetch(:client) do
|
10
|
+
require 'dalli'
|
11
|
+
::Dalli::Client.new(options[:host], options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def lock(key, ttl)
|
18
|
+
client.add(key, (Time.now + ttl).to_i, ttl)
|
19
|
+
end
|
20
|
+
|
21
|
+
def lock!(key, ttl)
|
22
|
+
client.set(key, (Time.now + ttl).to_i, ttl)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_accessor :client
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Circuitry
|
2
|
+
module Locks
|
3
|
+
class Memory
|
4
|
+
include Base
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def store
|
8
|
+
@store ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def semaphore
|
12
|
+
@semaphore ||= Mutex.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def lock(key, ttl)
|
19
|
+
reap
|
20
|
+
|
21
|
+
store do |store|
|
22
|
+
if store.has_key?(key)
|
23
|
+
false
|
24
|
+
else
|
25
|
+
store[key] = Time.now + ttl
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def lock!(key, ttl)
|
32
|
+
reap
|
33
|
+
|
34
|
+
store do |store|
|
35
|
+
store[key] = Time.now + ttl
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def store(&block)
|
42
|
+
semaphore.synchronize do
|
43
|
+
block.call(self.class.store)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def reap
|
48
|
+
store do |store|
|
49
|
+
now = Time.now
|
50
|
+
store.delete_if { |key, expires_at| expires_at <= now }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def semaphore
|
55
|
+
self.class.semaphore
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Circuitry
|
2
|
+
module Locks
|
3
|
+
class Redis
|
4
|
+
include Base
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
super(options)
|
8
|
+
|
9
|
+
self.client = options.fetch(:client) do
|
10
|
+
require 'redis'
|
11
|
+
::Redis.new(options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def lock(key, ttl)
|
18
|
+
client.set(key, (Time.now + ttl).to_i, ex: ttl, nx: true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def lock!(key, ttl)
|
22
|
+
client.set(key, (Time.now + ttl).to_i, ex: ttl)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_accessor :client
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/circuitry/processor.rb
CHANGED
data/lib/circuitry/publisher.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'timeout'
|
2
3
|
require 'circuitry/concerns/async'
|
3
4
|
require 'circuitry/services/sns'
|
4
5
|
require 'circuitry/topic_creator'
|
@@ -12,12 +13,16 @@ module Circuitry
|
|
12
13
|
|
13
14
|
DEFAULT_OPTIONS = {
|
14
15
|
async: false,
|
16
|
+
timeout: 15,
|
15
17
|
}.freeze
|
16
18
|
|
19
|
+
attr_reader :timeout
|
20
|
+
|
17
21
|
def initialize(options = {})
|
18
22
|
options = DEFAULT_OPTIONS.merge(options)
|
19
23
|
|
20
24
|
self.async = options[:async]
|
25
|
+
self.timeout = options[:timeout]
|
21
26
|
end
|
22
27
|
|
23
28
|
def publish(topic_name, object)
|
@@ -30,8 +35,10 @@ module Circuitry
|
|
30
35
|
end
|
31
36
|
|
32
37
|
process = -> do
|
33
|
-
|
34
|
-
|
38
|
+
Timeout.timeout(timeout) do
|
39
|
+
topic = TopicCreator.find_or_create(topic_name)
|
40
|
+
sns.publish(topic.arn, object.to_json)
|
41
|
+
end
|
35
42
|
end
|
36
43
|
|
37
44
|
if async?
|
@@ -45,6 +52,10 @@ module Circuitry
|
|
45
52
|
Circuitry.config.publish_async_strategy
|
46
53
|
end
|
47
54
|
|
55
|
+
protected
|
56
|
+
|
57
|
+
attr_writer :timeout
|
58
|
+
|
48
59
|
private
|
49
60
|
|
50
61
|
def logger
|
data/lib/circuitry/subscriber.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'timeout'
|
1
2
|
require 'circuitry/concerns/async'
|
2
3
|
require 'circuitry/services/sqs'
|
3
4
|
require 'circuitry/message'
|
@@ -9,10 +10,12 @@ module Circuitry
|
|
9
10
|
include Concerns::Async
|
10
11
|
include Services::SQS
|
11
12
|
|
12
|
-
attr_reader :queue, :
|
13
|
+
attr_reader :queue, :timeout, :wait_time, :batch_size, :lock
|
13
14
|
|
14
15
|
DEFAULT_OPTIONS = {
|
16
|
+
lock: true,
|
15
17
|
async: false,
|
18
|
+
timeout: 15,
|
16
19
|
wait_time: 10,
|
17
20
|
batch_size: 10,
|
18
21
|
}.freeze
|
@@ -27,7 +30,9 @@ module Circuitry
|
|
27
30
|
options = DEFAULT_OPTIONS.merge(options)
|
28
31
|
|
29
32
|
self.queue = queue
|
33
|
+
self.lock = options[:lock]
|
30
34
|
self.async = options[:async]
|
35
|
+
self.timeout = options[:timeout]
|
31
36
|
self.wait_time = options[:wait_time]
|
32
37
|
self.batch_size = options[:batch_size]
|
33
38
|
end
|
@@ -60,7 +65,18 @@ module Circuitry
|
|
60
65
|
|
61
66
|
protected
|
62
67
|
|
63
|
-
attr_writer :queue, :wait_time, :batch_size
|
68
|
+
attr_writer :queue, :timeout, :wait_time, :batch_size
|
69
|
+
|
70
|
+
def lock=(value)
|
71
|
+
value = case value
|
72
|
+
when true then Circuitry.config.lock_strategy
|
73
|
+
when false then Circuitry::Locks::NOOP.new
|
74
|
+
when Circuitry::Locks::Base then value
|
75
|
+
else raise ArgumentError, "Invalid value `#{value}`, must be one of `true`, `false`, or instance of `#{Circuitry::Locks::Base}`"
|
76
|
+
end
|
77
|
+
|
78
|
+
@lock = value
|
79
|
+
end
|
64
80
|
|
65
81
|
private
|
66
82
|
|
@@ -85,21 +101,29 @@ module Circuitry
|
|
85
101
|
def process_message(message, &block)
|
86
102
|
message = Message.new(message)
|
87
103
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
delete_message(message)
|
92
|
-
end
|
104
|
+
logger.info("Processing message #{message.id}")
|
105
|
+
handle_message(message, &block)
|
106
|
+
delete_message(message)
|
93
107
|
rescue => e
|
94
108
|
logger.error("Error processing message #{message.id}: #{e}")
|
95
109
|
error_handler.call(e) if error_handler
|
96
110
|
end
|
97
111
|
|
98
112
|
def handle_message(message, &block)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
113
|
+
if lock.soft_lock(message.id)
|
114
|
+
begin
|
115
|
+
Timeout.timeout(timeout) do
|
116
|
+
block.call(message.body, message.topic.name)
|
117
|
+
end
|
118
|
+
rescue => e
|
119
|
+
logger.error("Error handling message #{message.id}: #{e}")
|
120
|
+
raise e
|
121
|
+
end
|
122
|
+
|
123
|
+
lock.hard_lock(message.id)
|
124
|
+
else
|
125
|
+
logger.info("Ignoring duplicate message #{message.id}")
|
126
|
+
end
|
103
127
|
end
|
104
128
|
|
105
129
|
def delete_message(message)
|
data/lib/circuitry/version.rb
CHANGED
data/lib/circuitry.rb
CHANGED
@@ -1,4 +1,9 @@
|
|
1
1
|
require 'circuitry/version'
|
2
|
+
require 'circuitry/locks/base'
|
3
|
+
require 'circuitry/locks/memcache'
|
4
|
+
require 'circuitry/locks/memory'
|
5
|
+
require 'circuitry/locks/noop'
|
6
|
+
require 'circuitry/locks/redis'
|
2
7
|
require 'circuitry/processor'
|
3
8
|
require 'circuitry/processors/batcher'
|
4
9
|
require 'circuitry/processors/forker'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: circuitry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Huggins
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-07-
|
11
|
+
date: 2015-07-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fog-aws
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: pry-
|
42
|
+
name: pry-byebug
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -122,6 +122,62 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: redis
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: mock_redis
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: dalli
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: memcache_mock
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
125
181
|
description: Amazon SNS publishing and SQS queue processing.
|
126
182
|
email:
|
127
183
|
- matt.huggins@kapost.com
|
@@ -142,6 +198,11 @@ files:
|
|
142
198
|
- lib/circuitry.rb
|
143
199
|
- lib/circuitry/concerns/async.rb
|
144
200
|
- lib/circuitry/configuration.rb
|
201
|
+
- lib/circuitry/locks/base.rb
|
202
|
+
- lib/circuitry/locks/memcache.rb
|
203
|
+
- lib/circuitry/locks/memory.rb
|
204
|
+
- lib/circuitry/locks/noop.rb
|
205
|
+
- lib/circuitry/locks/redis.rb
|
145
206
|
- lib/circuitry/message.rb
|
146
207
|
- lib/circuitry/processor.rb
|
147
208
|
- lib/circuitry/processors/batcher.rb
|