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.
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