realm-sns 0.7.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 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: []