circuitry 1.1.0 → 1.2.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.
- 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
|