circuitry 2.1.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|