circuitry 2.1.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +107 -59
  4. data/circuitry.gemspec +2 -2
  5. data/lib/circuitry.rb +32 -15
  6. data/lib/circuitry/cli.rb +32 -32
  7. data/lib/circuitry/config/file_loader.rb +24 -0
  8. data/lib/circuitry/config/publisher_settings.rb +16 -0
  9. data/lib/circuitry/config/shared_settings.rb +39 -0
  10. data/lib/circuitry/config/subscriber_settings.rb +32 -0
  11. data/lib/circuitry/locks/base.rb +3 -3
  12. data/lib/circuitry/locks/memory.rb +4 -4
  13. data/lib/circuitry/locks/noop.rb +1 -1
  14. data/lib/circuitry/locks/redis.rb +1 -1
  15. data/lib/circuitry/middleware/chain.rb +1 -1
  16. data/lib/circuitry/processor.rb +12 -10
  17. data/lib/circuitry/processors/forker.rb +1 -1
  18. data/lib/circuitry/processors/threader.rb +1 -1
  19. data/lib/circuitry/provisioning.rb +9 -0
  20. data/lib/circuitry/provisioning/provisioner.rb +71 -0
  21. data/lib/circuitry/provisioning/queue_creator.rb +64 -0
  22. data/lib/circuitry/provisioning/subscription_creator.rb +65 -0
  23. data/lib/circuitry/provisioning/topic_creator.rb +31 -0
  24. data/lib/circuitry/publisher.rb +5 -6
  25. data/lib/circuitry/queue.rb +25 -3
  26. data/lib/circuitry/railtie.rb +9 -0
  27. data/lib/circuitry/services/sns.rb +1 -1
  28. data/lib/circuitry/services/sqs.rb +1 -1
  29. data/lib/circuitry/subscriber.rb +8 -8
  30. data/lib/circuitry/tasks.rb +3 -3
  31. data/lib/circuitry/topic.rb +24 -2
  32. data/lib/circuitry/version.rb +1 -1
  33. metadata +11 -7
  34. data/lib/circuitry/configuration.rb +0 -64
  35. data/lib/circuitry/provisioner.rb +0 -60
  36. data/lib/circuitry/queue_creator.rb +0 -50
  37. data/lib/circuitry/subscription_creator.rb +0 -60
  38. 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
@@ -25,15 +25,15 @@ module Circuitry
25
25
 
26
26
  protected
27
27
 
28
- def lock(key, ttl)
28
+ def lock(_key, _ttl)
29
29
  raise NotImplementedError
30
30
  end
31
31
 
32
- def lock!(key, ttl)
32
+ def lock!(_key, _ttl)
33
33
  raise NotImplementedError
34
34
  end
35
35
 
36
- def unlock!(key)
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.has_key?(key)
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(&block)
47
+ def store
48
48
  semaphore.synchronize do
49
- block.call(self.class.store)
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 { |key, expires_at| expires_at <= now }
56
+ store.delete_if { |_, expires_at| expires_at <= now }
57
57
  end
58
58
  end
59
59
 
@@ -5,7 +5,7 @@ module Circuitry
5
5
 
6
6
  protected
7
7
 
8
- def lock(key, ttl)
8
+ def lock(_key, _ttl)
9
9
  true
10
10
  end
11
11
 
@@ -40,7 +40,7 @@ module Circuitry
40
40
  if pool?
41
41
  client.with(&block)
42
42
  else
43
- block.call(client)
43
+ yield client
44
44
  end
45
45
  end
46
46
 
@@ -54,7 +54,7 @@ module Circuitry
54
54
  def invoke(*args)
55
55
  chain = build.dup
56
56
 
57
- traverse_chain = -> do
57
+ traverse_chain = lambda do
58
58
  if chain.empty?
59
59
  yield
60
60
  else
@@ -1,6 +1,6 @@
1
1
  module Circuitry
2
2
  module Processor
3
- def process(&block)
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(&block)
14
- begin
15
- block.call
16
- rescue => e
17
- logger.error("Error handling message: #{e}")
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.config.logger
31
+ Circuitry.subscriber_config.logger
30
32
  end
31
33
 
32
34
  def error_handler
33
- Circuitry.config.error_handler
35
+ Circuitry.subscriber_config.error_handler
34
36
  end
35
37
  end
36
38
  end
@@ -9,7 +9,7 @@ module Circuitry
9
9
  def process(&block)
10
10
  pid = fork do
11
11
  safely_process(&block)
12
- Circuitry.config.on_fork_exit.call if Circuitry.config.on_fork_exit
12
+ on_exit.call if on_exit
13
13
  end
14
14
 
15
15
  Process.detach(pid)
@@ -11,7 +11,7 @@ module Circuitry
11
11
 
12
12
  pool << Thread.new do
13
13
  safely_process(&block)
14
- Circuitry.config.on_thread_exit.call if Circuitry.config.on_thread_exit
14
+ on_exit.call if on_exit
15
15
  end
16
16
  end
17
17
 
@@ -0,0 +1,9 @@
1
+ require 'circuitry/provisioning/provisioner'
2
+
3
+ module Circuitry
4
+ module Provisioning
5
+ def self.provision(logger: Logger.new(STDOUT))
6
+ Provisioning::Provisioner.new(logger).run
7
+ end
8
+ end
9
+ 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