realm-sns 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aed94ae9cc516c1b6288aa772cc161c26cb1e15653844484a8bdb3a746a23192
4
+ data.tar.gz: 2e199ef3883732d74df1f2695262ac8d3194dd695c9d039bf5cc3a032f91f04a
5
+ SHA512:
6
+ metadata.gz: 1af4c9e553f23fbd7ac3284e059203767a457314d9e2b2c7cdea0c2f223c78b1ae6071b9c8769f960a6ba21d2c65f472a35ef74043f02f99dfd26770d4d392ee
7
+ data.tar.gz: 1e35141ccba9f78085e1f509b0522f45fec26b2a240ae6d519882b16553f69ae62dc23c4d78713afa0cd112bca364fd6039650d8f052e7d59d001508340fa71b
data/lib/realm-sns.rb ADDED
@@ -0,0 +1,8 @@
1
+ # rubocop:disable Naming/FileName
2
+ # frozen_string_literal: true
3
+
4
+ Dir[File.join(File.dirname(__FILE__), 'realm', '**', '*.rb')].sort.each do |f|
5
+ require f
6
+ end
7
+
8
+ # rubocop:enable Naming/FileName
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'active_support/core_ext/string'
5
+
6
+ require 'realm/event_handler'
7
+ require 'realm/event_router/gateway'
8
+ require_relative './gateway/queue_manager'
9
+ require_relative './gateway/topic_adapter'
10
+ require_relative './gateway/worker'
11
+
12
+ module Realm
13
+ module SNS
14
+ class Gateway < Realm::EventRouter::Gateway
15
+ def initialize(topic_arn:, queue_prefix: nil, event_processing_attempts: 3, **)
16
+ super
17
+ @topic = TopicAdapter.new(topic_arn)
18
+ @queue_prefix = queue_prefix
19
+ @event_processing_attempts = event_processing_attempts
20
+ @queue_map = {}
21
+ end
22
+
23
+ def add_listener(event_type, listener, queue_arn: nil)
24
+ queue = queue_arn ? queue_manager.get(arn: queue_arn) : provide_queue(event_type, listener)
25
+ @queue_map[queue] = listener
26
+ end
27
+
28
+ def trigger(event_type, attributes = {})
29
+ create_event(event_type, attributes).tap { |event| @topic.publish(event_type, event.to_json) }
30
+ end
31
+
32
+ def worker(**options)
33
+ @worker ||= Worker.new(
34
+ @queue_map,
35
+ event_factory: @event_factory,
36
+ logger: @runtime && @runtime.context[:logger],
37
+ event_processing_attempts: @event_processing_attempts,
38
+ **options,
39
+ )
40
+ end
41
+
42
+ def queues
43
+ @queue_map.keys
44
+ end
45
+
46
+ private
47
+
48
+ def provide_queue(event_type, listener)
49
+ queue_name = [event_type, queue_suffix(listener)].join('-')
50
+ queue = queue_manager.provide(queue_name)
51
+ @topic.subscribe(event_type, queue)
52
+ queue
53
+ end
54
+
55
+ def queue_manager
56
+ @queue_manager ||= QueueManager.new(prefix: @queue_prefix)
57
+ end
58
+
59
+ def queue_suffix(listener)
60
+ listener.try(:identifier) || SecureRandom.alphanumeric(16)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'realm/event_router/gateway'
4
+ require 'realm/mixins/decorator'
5
+
6
+ module Realm
7
+ module SNS
8
+ class Gateway < Realm::EventRouter::Gateway
9
+ # Provides cleaner SDK over Aws::SQS::Queue
10
+ class QueueAdapter
11
+ include Mixins::Decorator[:@queue]
12
+
13
+ def arn
14
+ @queue.attributes['QueueArn']
15
+ end
16
+
17
+ def allow_send_messages(source_arn)
18
+ @queue.set_attributes(attributes: {
19
+ 'Policy' => {
20
+ 'Version' => '2012-10-17',
21
+ 'Statement' => policy_statement(source_arn),
22
+ }.to_json,
23
+ })
24
+ end
25
+
26
+ def publish(event_type, message)
27
+ @queue.send_message(
28
+ message_body: message,
29
+ message_attributes: { 'event_type' => { data_type: 'String', string_value: event_type.to_s } },
30
+ )
31
+ end
32
+
33
+ def empty?
34
+ attributes.slice(
35
+ 'ApproximateNumberOfMessages',
36
+ 'ApproximateNumberOfMessagesDelayed',
37
+ 'ApproximateNumberOfMessagesNotVisible',
38
+ ).all? { |_, val| val.to_i.zero? }
39
+ end
40
+
41
+ private
42
+
43
+ def policy_statement(source_arn)
44
+ {
45
+ 'Effect' => 'Allow',
46
+ 'Principal' => { 'AWS' => '*' },
47
+ 'Action' => 'sqs:SendMessage',
48
+ 'Resource' => arn,
49
+ 'Condition' => {
50
+ 'ArnEquals' => { 'aws:SourceArn' => source_arn },
51
+ },
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+ require 'realm/error'
5
+ require_relative './queue_adapter'
6
+
7
+ module Realm
8
+ module SNS
9
+ class Gateway < Realm::EventRouter::Gateway
10
+ class QueueManager
11
+ class QueueNameTooLong < Realm::Error
12
+ def initialize(name, msg: "Queue name '#{name}' cannot be longer than 80 characters, " \
13
+ "please provide custom EventHandler identifier if it's auto generated")
14
+ super(msg)
15
+ end
16
+ end
17
+
18
+ CleanupWithoutPrefix = Realm::Error[
19
+ 'Cleaning up queues without prefix is not allowed, it can lead to deleting queues from other apps']
20
+
21
+ def initialize(prefix: nil, sqs: Aws::SQS::Resource.new)
22
+ @prefix = prefix
23
+ @sqs = sqs
24
+ end
25
+
26
+ def get(name: nil, arn: nil)
27
+ throw ArgumentError, 'You have to provide name or arn of the queue' unless name || arn
28
+
29
+ QueueAdapter.new(@sqs.get_queue_by_name(queue_name: name ? prefix_name(name) : arn.split(':')[-1]))
30
+ end
31
+
32
+ def create(queue_name)
33
+ name = prefix_name(queue_name)
34
+ raise QueueNameTooLong, name if name.size > 80
35
+
36
+ QueueAdapter.new(@sqs.create_queue(queue_name: name))
37
+ end
38
+
39
+ def provide(queue_name)
40
+ get(name: queue_name)
41
+ rescue Aws::SQS::Errors::NonExistentQueue
42
+ create(queue_name)
43
+ end
44
+
45
+ def cleanup(except: [])
46
+ raise CleanupWithoutPrefix unless @prefix
47
+
48
+ except_urls = Array(except).map(&:url)
49
+ @sqs.queues(queue_name_prefix: @prefix).each do |queue|
50
+ next if except_urls.include?(queue.url)
51
+
52
+ queue.delete if QueueAdapter.new(queue).empty?
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def prefix_name(name)
59
+ [@prefix, name].compact.join('-')
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+
5
+ module Realm
6
+ module SNS
7
+ class Gateway < Realm::EventRouter::Gateway
8
+ # Provides cleaner SDK over Aws::SNS::Topic
9
+ class TopicAdapter
10
+ def initialize(topic_or_arn)
11
+ @topic = topic_or_arn.is_a?(Aws::SNS::Topic) ? topic_or_arn : Aws::SNS::Resource.new.topic(topic_or_arn)
12
+ end
13
+
14
+ def publish(event_type, message)
15
+ @topic.publish(
16
+ message: message,
17
+ message_attributes: { 'event_type' => { data_type: 'String', string_value: event_type.to_s } },
18
+ )
19
+ end
20
+
21
+ def subscribe(event_type, queue)
22
+ queue.allow_send_messages(@topic.arn)
23
+ @topic.subscribe(
24
+ protocol: 'sqs',
25
+ endpoint: queue.arn,
26
+ attributes: subscribe_attributes(event_type),
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def subscribe_attributes(event_type)
33
+ attrs = { 'RawMessageDelivery' => true }
34
+ attrs['FilterPolicy'] = { 'event_type' => [event_type] } unless event_type == :any
35
+ attrs.transform_values(&:to_json)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/hash'
4
+ require 'aws-sdk-sqs'
5
+
6
+ module Realm
7
+ module SNS
8
+ class Gateway < Realm::EventRouter::Gateway
9
+ class Worker
10
+ def initialize(queue_map, event_factory:, event_processing_attempts: 3, logger: nil)
11
+ @queue_map = queue_map
12
+ @event_factory = event_factory
13
+ @event_processing_attempts = event_processing_attempts
14
+ @logger = logger || Logger.new($stdout)
15
+ @threads = []
16
+ end
17
+
18
+ def start(poller_options: {})
19
+ @signaler = { exiting: false }
20
+ @queue_map.each_pair do |queue, listener|
21
+ @threads << Thread.new { run_poller(queue, listener, @signaler, poller_options) }
22
+ end
23
+ self
24
+ end
25
+
26
+ def stop(timeout: 30)
27
+ Thread.new { @logger.info("Stopping worker (timeout: #{timeout}s)") }.join # Cannot log from trap context
28
+ @signaler[:exiting] = true
29
+ join(timeout)
30
+ @threads.clear
31
+ self
32
+ end
33
+
34
+ def join(timeout = nil)
35
+ @threads.each { |thread| thread.join(timeout) }
36
+ end
37
+
38
+ private
39
+
40
+ def run_poller(queue, listener, signaler, options)
41
+ @logger.info("Start polling #{queue.arn}")
42
+ init_poller(queue, signaler, options).poll do |messages, stats|
43
+ log_poller_stats(queue, stats)
44
+ messages.each { |msg| handle_message(listener, msg) }
45
+ end
46
+ @logger.info("Polling stopped #{queue.arn}")
47
+ end
48
+
49
+ def init_poller(queue, signaler, options = {})
50
+ Aws::SQS::QueuePoller.new(
51
+ queue.url,
52
+ max_number_of_messages: 10,
53
+ visibility_timeout: 60,
54
+ attribute_names: ['ApproximateReceiveCount'],
55
+ message_attribute_names: ['event_type'],
56
+ before_request: before_request_proc(queue, signaler),
57
+ **options,
58
+ )
59
+ end
60
+
61
+ def before_request_proc(queue, signaler)
62
+ proc {
63
+ if signaler[:exiting]
64
+ @logger.info("Stopping polling #{queue.arn}")
65
+ throw :stop_polling
66
+ end
67
+ }
68
+ end
69
+
70
+ def log_poller_stats(queue, stats)
71
+ @logger.info(
72
+ message: "Poller #{queue.arn} stats",
73
+ request_count: stats.request_count,
74
+ message_count: stats.received_message_count,
75
+ last_message_received_at: stats.last_message_received_at,
76
+ )
77
+ end
78
+
79
+ def handle_message(listener, msg)
80
+ event = message_to_event(msg)
81
+ listener.(event)
82
+ rescue StandardError => e
83
+ log_error(e, event, msg)
84
+ # Picks up the message again after visibility_timeout runs out:
85
+ throw :skip_delete if event && message_receive_count(msg) < @event_processing_attempts
86
+ end
87
+
88
+ def message_to_event(msg)
89
+ event_type = msg.message_attributes['event_type'].string_value
90
+ raise 'Message is missing event type' unless event_type
91
+
92
+ payload = JSON.parse(msg.body).deep_symbolize_keys
93
+ @event_factory.create_event(event_type, payload)
94
+ end
95
+
96
+ def message_receive_count(msg)
97
+ msg.attributes['ApproximateReceiveCount'].to_i
98
+ end
99
+
100
+ def log_error(error, event, msg)
101
+ return @logger.fatal("Unexpected message in queue: #{msg}; error: #{error.full_message}") unless event
102
+
103
+ attempt = message_receive_count(msg)
104
+ will_retry = attempt < @event_processing_attempts
105
+ log_line = [
106
+ "Processing of event failed type=#{event.type} id=#{event.head.id} attempt=#{attempt},",
107
+ "#{will_retry ? 'will retry' : 'final'}):\n#{error.full_message}",
108
+ ].join(' ')
109
+ @logger.send(will_retry ? :warn : :error, log_line)
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'realm/plugin'
4
+ require_relative 'gateway'
5
+
6
+ module Realm
7
+ module SNS
8
+ class Plugin < Realm::Plugin
9
+ def self.setup(_config, container)
10
+ container.register('event_router.gateway_classes.sns', SNS::Gateway)
11
+ end
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: realm-sns
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - developers@reevoo.com
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-sns
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.36'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.36'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-sqs
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.34'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.34'
41
+ - !ruby/object:Gem::Dependency
42
+ name: realm-core
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - lib/realm-sns.rb
104
+ - lib/realm/sns/gateway.rb
105
+ - lib/realm/sns/gateway/queue_adapter.rb
106
+ - lib/realm/sns/gateway/queue_manager.rb
107
+ - lib/realm/sns/gateway/topic_adapter.rb
108
+ - lib/realm/sns/gateway/worker.rb
109
+ - lib/realm/sns/plugin.rb
110
+ homepage:
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 2.7.0
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubygems_version: 3.1.6
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: SNS/SQS plugin for Realm
133
+ test_files: []