circuitry 2.1.1 → 3.0.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/CHANGELOG.md +7 -0
- data/README.md +107 -59
- data/circuitry.gemspec +2 -2
- data/lib/circuitry.rb +32 -15
- data/lib/circuitry/cli.rb +32 -32
- data/lib/circuitry/config/file_loader.rb +24 -0
- data/lib/circuitry/config/publisher_settings.rb +16 -0
- data/lib/circuitry/config/shared_settings.rb +39 -0
- data/lib/circuitry/config/subscriber_settings.rb +32 -0
- data/lib/circuitry/locks/base.rb +3 -3
- data/lib/circuitry/locks/memory.rb +4 -4
- data/lib/circuitry/locks/noop.rb +1 -1
- data/lib/circuitry/locks/redis.rb +1 -1
- data/lib/circuitry/middleware/chain.rb +1 -1
- data/lib/circuitry/processor.rb +12 -10
- data/lib/circuitry/processors/forker.rb +1 -1
- data/lib/circuitry/processors/threader.rb +1 -1
- data/lib/circuitry/provisioning.rb +9 -0
- data/lib/circuitry/provisioning/provisioner.rb +71 -0
- data/lib/circuitry/provisioning/queue_creator.rb +64 -0
- data/lib/circuitry/provisioning/subscription_creator.rb +65 -0
- data/lib/circuitry/provisioning/topic_creator.rb +31 -0
- data/lib/circuitry/publisher.rb +5 -6
- data/lib/circuitry/queue.rb +25 -3
- data/lib/circuitry/railtie.rb +9 -0
- data/lib/circuitry/services/sns.rb +1 -1
- data/lib/circuitry/services/sqs.rb +1 -1
- data/lib/circuitry/subscriber.rb +8 -8
- data/lib/circuitry/tasks.rb +3 -3
- data/lib/circuitry/topic.rb +24 -2
- data/lib/circuitry/version.rb +1 -1
- metadata +11 -7
- data/lib/circuitry/configuration.rb +0 -64
- data/lib/circuitry/provisioner.rb +0 -60
- data/lib/circuitry/queue_creator.rb +0 -50
- data/lib/circuitry/subscription_creator.rb +0 -60
- data/lib/circuitry/topic_creator.rb +0 -25
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Circuitry
|
6
|
+
module Config
|
7
|
+
module FileLoader
|
8
|
+
def self.load(cfile, environment = 'development')
|
9
|
+
return nil unless File.exist?(cfile)
|
10
|
+
|
11
|
+
opts = {}
|
12
|
+
opts = YAML.load(ERB.new(IO.read(cfile)).result) || opts
|
13
|
+
opts = opts.merge(opts.delete(environment) || {})
|
14
|
+
|
15
|
+
publisher_opts = opts.merge(opts.delete('publisher') || {})
|
16
|
+
subscriber_opts = opts.merge(opts.delete('subscriber') || {})
|
17
|
+
|
18
|
+
Circuitry.subscriber_config = subscriber_opts
|
19
|
+
Circuitry.publisher_config = publisher_opts
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'circuitry/config/shared_settings'
|
3
|
+
|
4
|
+
module Circuitry
|
5
|
+
module Config
|
6
|
+
class PublisherSettings
|
7
|
+
include Virtus::Model
|
8
|
+
include SharedSettings
|
9
|
+
|
10
|
+
def async_strategy=(value)
|
11
|
+
validate_setting(value, Publisher.async_strategies)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Circuitry
|
4
|
+
class ConfigError < StandardError; end
|
5
|
+
|
6
|
+
module Config
|
7
|
+
module SharedSettings
|
8
|
+
def self.included(base)
|
9
|
+
base.attribute :access_key, String
|
10
|
+
base.attribute :secret_key, String
|
11
|
+
base.attribute :region, String, default: 'us-east-1'
|
12
|
+
base.attribute :logger, Logger, default: Logger.new(STDERR)
|
13
|
+
base.attribute :error_handler
|
14
|
+
base.attribute :topic_names, Array[String], default: []
|
15
|
+
base.attribute :on_async_exit
|
16
|
+
base.attribute :async_strategy, Symbol, default: ->(_page, _att) { :fork }
|
17
|
+
end
|
18
|
+
|
19
|
+
def middleware
|
20
|
+
@_middleware ||= Circuitry::Middleware::Chain.new
|
21
|
+
yield @_middleware if block_given?
|
22
|
+
@_middleware
|
23
|
+
end
|
24
|
+
|
25
|
+
def aws_options
|
26
|
+
{
|
27
|
+
access_key_id: access_key,
|
28
|
+
secret_access_key: secret_key,
|
29
|
+
region: region
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_setting(value, permitted_values)
|
34
|
+
return if permitted_values.include?(value)
|
35
|
+
raise ConfigError, "invalid value `#{value}`, must be one of #{permitted_values.inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'circuitry/config/shared_settings'
|
3
|
+
|
4
|
+
module Circuitry
|
5
|
+
module Config
|
6
|
+
class SubscriberSettings
|
7
|
+
include Virtus::Model
|
8
|
+
include SharedSettings
|
9
|
+
|
10
|
+
attribute :queue_name, String
|
11
|
+
attribute :dead_letter_queue_name, String
|
12
|
+
attribute :visibility_timeout, Integer, default: 30 * 60
|
13
|
+
attribute :max_receive_count, Integer, default: 8
|
14
|
+
attribute :lock_strategy, Object, default: ->(_page, _att) { Circuitry::Locks::Memory.new }
|
15
|
+
|
16
|
+
def dead_letter_queue_name
|
17
|
+
super || "#{queue_name}-failures"
|
18
|
+
end
|
19
|
+
|
20
|
+
def async_strategy=(value)
|
21
|
+
validate_setting(value, Subscriber.async_strategies)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def lock_strategy=(value)
|
26
|
+
unless value.is_a?(Circuitry::Locks::Base)
|
27
|
+
raise ConfigError, "invalid lock strategy \"#{value.inspect}\""
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/circuitry/locks/base.rb
CHANGED
@@ -25,15 +25,15 @@ module Circuitry
|
|
25
25
|
|
26
26
|
protected
|
27
27
|
|
28
|
-
def lock(
|
28
|
+
def lock(_key, _ttl)
|
29
29
|
raise NotImplementedError
|
30
30
|
end
|
31
31
|
|
32
|
-
def lock!(
|
32
|
+
def lock!(_key, _ttl)
|
33
33
|
raise NotImplementedError
|
34
34
|
end
|
35
35
|
|
36
|
-
def unlock!(
|
36
|
+
def unlock!(_key)
|
37
37
|
raise NotImplementedError
|
38
38
|
end
|
39
39
|
|
@@ -19,7 +19,7 @@ module Circuitry
|
|
19
19
|
reap
|
20
20
|
|
21
21
|
store do |store|
|
22
|
-
if store.
|
22
|
+
if store.key?(key)
|
23
23
|
false
|
24
24
|
else
|
25
25
|
store[key] = Time.now + ttl
|
@@ -44,16 +44,16 @@ module Circuitry
|
|
44
44
|
|
45
45
|
private
|
46
46
|
|
47
|
-
def store
|
47
|
+
def store
|
48
48
|
semaphore.synchronize do
|
49
|
-
|
49
|
+
yield self.class.store
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
53
|
def reap
|
54
54
|
store do |store|
|
55
55
|
now = Time.now
|
56
|
-
store.delete_if { |
|
56
|
+
store.delete_if { |_, expires_at| expires_at <= now }
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
data/lib/circuitry/locks/noop.rb
CHANGED
data/lib/circuitry/processor.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Circuitry
|
2
2
|
module Processor
|
3
|
-
def process(&
|
3
|
+
def process(&_block)
|
4
4
|
raise NotImplementedError, "#{self} must implement class method `process`"
|
5
5
|
end
|
6
6
|
|
@@ -8,15 +8,17 @@ module Circuitry
|
|
8
8
|
raise NotImplementedError, "#{self} must implement class method `flush`"
|
9
9
|
end
|
10
10
|
|
11
|
+
def on_exit
|
12
|
+
Circuitry.subscriber_config.on_async_exit
|
13
|
+
end
|
14
|
+
|
11
15
|
protected
|
12
16
|
|
13
|
-
def safely_process
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
error_handler.call(e) if error_handler
|
19
|
-
end
|
17
|
+
def safely_process
|
18
|
+
yield
|
19
|
+
rescue => e
|
20
|
+
logger.error("Error handling message: #{e}")
|
21
|
+
error_handler.call(e) if error_handler
|
20
22
|
end
|
21
23
|
|
22
24
|
def pool
|
@@ -26,11 +28,11 @@ module Circuitry
|
|
26
28
|
private
|
27
29
|
|
28
30
|
def logger
|
29
|
-
Circuitry.
|
31
|
+
Circuitry.subscriber_config.logger
|
30
32
|
end
|
31
33
|
|
32
34
|
def error_handler
|
33
|
-
Circuitry.
|
35
|
+
Circuitry.subscriber_config.error_handler
|
34
36
|
end
|
35
37
|
end
|
36
38
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'circuitry/provisioning/queue_creator'
|
2
|
+
require 'circuitry/provisioning/topic_creator'
|
3
|
+
require 'circuitry/provisioning/subscription_creator'
|
4
|
+
|
5
|
+
module Circuitry
|
6
|
+
module Provisioning
|
7
|
+
class Provisioner
|
8
|
+
def initialize(logger)
|
9
|
+
self.logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
queue = create_queue
|
14
|
+
return unless queue
|
15
|
+
|
16
|
+
create_topics(:publisher, publisher_config.topic_names)
|
17
|
+
subscribe_topics(queue, create_topics(:subscriber, subscriber_config.topic_names))
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_accessor :logger
|
23
|
+
|
24
|
+
def publisher_config
|
25
|
+
Circuitry.publisher_config
|
26
|
+
end
|
27
|
+
|
28
|
+
def subscriber_config
|
29
|
+
Circuitry.subscriber_config
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_queue
|
33
|
+
safe_aws('Create Queue') do
|
34
|
+
queue = QueueCreator.find_or_create(
|
35
|
+
subscriber_config.queue_name,
|
36
|
+
dead_letter_queue_name: subscriber_config.dead_letter_queue_name,
|
37
|
+
max_receive_count: subscriber_config.max_receive_count,
|
38
|
+
visibility_timeout: subscriber_config.visibility_timeout
|
39
|
+
)
|
40
|
+
logger.info "Created queue #{queue.url}"
|
41
|
+
queue
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_topics(type, topics)
|
46
|
+
safe_aws("Create #{type.to_s.capitalize} Topics") do
|
47
|
+
topics.map do |topic_name|
|
48
|
+
topic = TopicCreator.find_or_create(topic_name)
|
49
|
+
logger.info "Created topic #{topic.name}"
|
50
|
+
topic
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def subscribe_topics(queue, topics)
|
56
|
+
safe_aws('Subscribe Topics') do
|
57
|
+
SubscriptionCreator.subscribe_all(queue, topics)
|
58
|
+
logger.info "Subscribed all topics to #{queue.name}"
|
59
|
+
true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def safe_aws(desc)
|
64
|
+
yield
|
65
|
+
rescue Aws::SQS::Errors::AccessDenied
|
66
|
+
logger.fatal("#{desc}: Access denied. Check your configured credentials.")
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'circuitry/services/sqs'
|
2
|
+
require 'circuitry/queue'
|
3
|
+
|
4
|
+
module Circuitry
|
5
|
+
module Provisioning
|
6
|
+
class QueueCreator
|
7
|
+
include Services::SQS
|
8
|
+
|
9
|
+
attr_reader :queue_name
|
10
|
+
attr_reader :visibility_timeout
|
11
|
+
|
12
|
+
def self.find_or_create(queue_name,
|
13
|
+
dead_letter_queue_name: nil,
|
14
|
+
max_receive_count: 8,
|
15
|
+
visibility_timeout: 30 * 60)
|
16
|
+
creator = new(queue_name, visibility_timeout)
|
17
|
+
result = creator.create_queue
|
18
|
+
creator.create_dead_letter_queue(dead_letter_queue_name, max_receive_count) if dead_letter_queue_name
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(queue_name, visibility_timeout)
|
23
|
+
self.queue_name = queue_name
|
24
|
+
self.visibility_timeout = visibility_timeout
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_queue
|
28
|
+
@_queue ||= Queue.new(create_primary_queue_internal)
|
29
|
+
end
|
30
|
+
|
31
|
+
def create_dead_letter_queue(name, max_receive_count)
|
32
|
+
@_dl_queue ||= Queue.new(create_dl_queue_internal(name, max_receive_count))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_writer :queue_name
|
38
|
+
attr_writer :visibility_timeout
|
39
|
+
|
40
|
+
def create_dl_queue_internal(name, max_receive_count)
|
41
|
+
dl_url = sqs.create_queue(queue_name: name).queue_url
|
42
|
+
dl_arn = sqs.get_queue_attributes(
|
43
|
+
queue_url: dl_url,
|
44
|
+
attribute_names: ['QueueArn']
|
45
|
+
).attributes['QueueArn']
|
46
|
+
|
47
|
+
policy = build_redrive_policy(dl_arn, max_receive_count)
|
48
|
+
sqs.set_queue_attributes(queue_url: create_queue.url, attributes: policy)
|
49
|
+
dl_url
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_redrive_policy(deadletter_arn, max_receive_count)
|
53
|
+
{
|
54
|
+
'RedrivePolicy' => %({"maxReceiveCount":"#{max_receive_count}", "deadLetterTargetArn":"#{deadletter_arn}"})
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_primary_queue_internal
|
59
|
+
attributes = { 'VisibilityTimeout' => visibility_timeout.to_s }
|
60
|
+
sqs.create_queue(queue_name: queue_name, attributes: attributes).queue_url
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'circuitry/services/sns'
|
2
|
+
require 'circuitry/topic'
|
3
|
+
|
4
|
+
module Circuitry
|
5
|
+
module Provisioning
|
6
|
+
class SubscriptionCreator
|
7
|
+
include Services::SNS
|
8
|
+
include Services::SQS
|
9
|
+
|
10
|
+
attr_reader :queue
|
11
|
+
attr_reader :topics
|
12
|
+
|
13
|
+
def self.subscribe_all(queue, topics)
|
14
|
+
new(queue, topics).subscribe_all
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(queue, topics)
|
18
|
+
raise ArgumentError, 'queue must be a Circuitry::Queue' unless queue.is_a?(Circuitry::Queue)
|
19
|
+
raise ArgumentError, 'topics must be an array' unless topics.is_a?(Array)
|
20
|
+
|
21
|
+
self.queue = queue
|
22
|
+
self.topics = topics
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscribe_all
|
26
|
+
topics.each do |topic|
|
27
|
+
sns.subscribe(topic_arn: topic.arn, endpoint: queue.arn, protocol: 'sqs')
|
28
|
+
end
|
29
|
+
sqs.set_queue_attributes(
|
30
|
+
queue_url: queue.url,
|
31
|
+
attributes: build_policy
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_writer :queue
|
38
|
+
attr_writer :topics
|
39
|
+
|
40
|
+
def build_policy
|
41
|
+
# The aws ruby SDK doesn't have a policy builder :{
|
42
|
+
{
|
43
|
+
'Policy' => {
|
44
|
+
'Version' => '2012-10-17',
|
45
|
+
'Id' => "#{queue.arn}/SNSPolicy",
|
46
|
+
'Statement' => topics.map { |t| build_policy_statement(t) }
|
47
|
+
}.to_json
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def build_policy_statement(topic)
|
52
|
+
{
|
53
|
+
'Sid' => "Sid#{topic.name}",
|
54
|
+
'Effect' => 'Allow',
|
55
|
+
'Principal' => { 'AWS' => '*' },
|
56
|
+
'Action' => 'SQS:SendMessage',
|
57
|
+
'Resource' => queue.arn,
|
58
|
+
'Condition' => {
|
59
|
+
'ArnEquals' => { 'aws:SourceArn' => topic.arn }
|
60
|
+
}
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|