cyclone_lariat 0.2.1

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: b29f194e1ee444b5621fee3ad9be7547ad612c34acf8cf451d5be0f20976cc4e
4
+ data.tar.gz: efdedb4530a004672b9c671c02b899fddedaa1d45b9876d28bce672f86f88812
5
+ SHA512:
6
+ metadata.gz: 4c784a8c6ec54fef60a529a09d6a45f9dd32bab493e2acabdabed7e2039e743cb547e132a38522ee1b8473a1e2f21644b8d7a25d8bd1daca498e2bef5cd390ee
7
+ data.tar.gz: 85628b6886b4a82b639aa0f271b199e9cd85db4cbb316d4621637ca062ad996892f8c171ad32367a64f213b1ebcdd5e495e6f7cdb00f518923e87ecfe41a2610
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ /.idea/
2
+ /.bundle/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /.tmp/
11
+ .byebug_history
12
+ config/database.yml
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
16
+
17
+ # gem builds
18
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,48 @@
1
+ ##
2
+ # Bug with Travis rubocop and rainbow gem
3
+ # resolve:
4
+ # https://github.com/rubocop-hq/rubocop/issues/6398#issuecomment-431898694
5
+ inherit_mode:
6
+ merge:
7
+ - Exclude
8
+
9
+ AllCops:
10
+ TargetRubyVersion: 2.6
11
+ Exclude:
12
+ - Rakefile
13
+ UseCache: true
14
+ NewCops: enable
15
+
16
+ Metrics/LineLength:
17
+ Max: 120
18
+ IgnoredPatterns: ['\s*\#\s.*$']
19
+ Exclude:
20
+ - 'spec/**/*'
21
+
22
+ Style/Documentation:
23
+ Exclude:
24
+ - '**/*'
25
+
26
+ Metrics/ParameterLists:
27
+ Exclude:
28
+ - '**/*'
29
+
30
+ Style/AccessorGrouping:
31
+ Exclude:
32
+ - 'lib/cyclone_lariat/event.rb'
33
+
34
+ Metrics/CyclomaticComplexity:
35
+ Exclude:
36
+ - 'lib/cyclone_lariat/event.rb'
37
+ -
38
+ Metrics/PerceivedComplexity:
39
+ Exclude:
40
+ - 'lib/cyclone_lariat/event.rb'
41
+
42
+ Metrics/AbcSize:
43
+ Exclude:
44
+ - '**/*'
45
+
46
+ Metrics/BlockLength:
47
+ Exclude:
48
+ - '**/*'
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ cyclone-lariat
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.6.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.2.0] - 2021-06-2
8
+ Changed
9
+ - Fix can load from database if error_details is nil
10
+
11
+ ## [0.2.0] - 2021-06-2
12
+ Added
13
+ - Complete tests
14
+ - Production ready
15
+
16
+ ## [0.1.0] - 2021-05-24
17
+ - Init project
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in luna_park.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,151 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cyclone_lariat (0.2.1)
5
+ aws-sdk-sns
6
+ luna_park (~> 0.11)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.7.0)
12
+ public_suffix (>= 2.0.2, < 5.0)
13
+ ast (2.4.2)
14
+ aws-eventstream (1.1.1)
15
+ aws-partitions (1.462.0)
16
+ aws-sdk-core (3.114.0)
17
+ aws-eventstream (~> 1, >= 1.0.2)
18
+ aws-partitions (~> 1, >= 1.239.0)
19
+ aws-sigv4 (~> 1.1)
20
+ jmespath (~> 1.0)
21
+ aws-sdk-sns (1.40.0)
22
+ aws-sdk-core (~> 3, >= 3.112.0)
23
+ aws-sigv4 (~> 1.1)
24
+ aws-sigv4 (1.2.3)
25
+ aws-eventstream (~> 1, >= 1.0.2)
26
+ byebug (11.1.3)
27
+ coderay (1.1.3)
28
+ concurrent-ruby (1.1.8)
29
+ crack (0.4.5)
30
+ rexml
31
+ database_cleaner-core (2.0.1)
32
+ database_cleaner-sequel (2.0.0)
33
+ database_cleaner-core (~> 2.0.0)
34
+ sequel
35
+ diff-lcs (1.4.4)
36
+ docile (1.4.0)
37
+ dry-configurable (0.12.1)
38
+ concurrent-ruby (~> 1.0)
39
+ dry-core (~> 0.5, >= 0.5.0)
40
+ dry-container (0.7.2)
41
+ concurrent-ruby (~> 1.0)
42
+ dry-configurable (~> 0.1, >= 0.1.3)
43
+ dry-core (0.5.0)
44
+ concurrent-ruby (~> 1.0)
45
+ dry-equalizer (0.3.0)
46
+ dry-inflector (0.2.0)
47
+ dry-initializer (3.0.4)
48
+ dry-logic (1.2.0)
49
+ concurrent-ruby (~> 1.0)
50
+ dry-core (~> 0.5, >= 0.5)
51
+ dry-schema (1.6.2)
52
+ concurrent-ruby (~> 1.0)
53
+ dry-configurable (~> 0.8, >= 0.8.3)
54
+ dry-core (~> 0.5, >= 0.5)
55
+ dry-initializer (~> 3.0)
56
+ dry-logic (~> 1.0)
57
+ dry-types (~> 1.5)
58
+ dry-types (1.5.1)
59
+ concurrent-ruby (~> 1.0)
60
+ dry-container (~> 0.3)
61
+ dry-core (~> 0.5, >= 0.5)
62
+ dry-inflector (~> 0.1, >= 0.1.2)
63
+ dry-logic (~> 1.0, >= 1.0.2)
64
+ dry-validation (1.6.0)
65
+ concurrent-ruby (~> 1.0)
66
+ dry-container (~> 0.7, >= 0.7.1)
67
+ dry-core (~> 0.4)
68
+ dry-equalizer (~> 0.2)
69
+ dry-initializer (~> 3.0)
70
+ dry-schema (~> 1.5, >= 1.5.2)
71
+ hashdiff (1.0.1)
72
+ jmespath (1.4.0)
73
+ luna_park (0.11.1)
74
+ method_source (1.0.0)
75
+ parallel (1.20.1)
76
+ parser (3.0.1.1)
77
+ ast (~> 2.4.1)
78
+ pg (1.2.3)
79
+ pry (0.13.1)
80
+ coderay (~> 1.1)
81
+ method_source (~> 1.0)
82
+ pry-byebug (3.9.0)
83
+ byebug (~> 11.0)
84
+ pry (~> 0.13.0)
85
+ public_suffix (4.0.6)
86
+ rainbow (3.0.0)
87
+ rake (13.0.3)
88
+ regexp_parser (2.1.1)
89
+ rexml (3.2.5)
90
+ rspec (3.10.0)
91
+ rspec-core (~> 3.10.0)
92
+ rspec-expectations (~> 3.10.0)
93
+ rspec-mocks (~> 3.10.0)
94
+ rspec-core (3.10.1)
95
+ rspec-support (~> 3.10.0)
96
+ rspec-expectations (3.10.1)
97
+ diff-lcs (>= 1.2.0, < 2.0)
98
+ rspec-support (~> 3.10.0)
99
+ rspec-mocks (3.10.2)
100
+ diff-lcs (>= 1.2.0, < 2.0)
101
+ rspec-support (~> 3.10.0)
102
+ rspec-support (3.10.2)
103
+ rubocop (0.93.1)
104
+ parallel (~> 1.10)
105
+ parser (>= 2.7.1.5)
106
+ rainbow (>= 2.2.2, < 4.0)
107
+ regexp_parser (>= 1.8)
108
+ rexml
109
+ rubocop-ast (>= 0.6.0)
110
+ ruby-progressbar (~> 1.7)
111
+ unicode-display_width (>= 1.4.0, < 2.0)
112
+ rubocop-ast (1.5.0)
113
+ parser (>= 3.0.1.1)
114
+ ruby-progressbar (1.11.0)
115
+ sequel (5.44.0)
116
+ simplecov (0.21.2)
117
+ docile (~> 1.1)
118
+ simplecov-html (~> 0.11)
119
+ simplecov_json_formatter (~> 0.1)
120
+ simplecov-html (0.12.3)
121
+ simplecov_json_formatter (0.1.3)
122
+ timecop (0.9.4)
123
+ unicode-display_width (1.7.0)
124
+ webmock (3.7.6)
125
+ addressable (>= 2.3.6)
126
+ crack (>= 0.3.2)
127
+ hashdiff (>= 0.4.0, < 2.0.0)
128
+ yard (0.9.26)
129
+
130
+ PLATFORMS
131
+ x86_64-linux
132
+
133
+ DEPENDENCIES
134
+ bundler (~> 2.1)
135
+ byebug (~> 11.1)
136
+ cyclone_lariat!
137
+ database_cleaner-sequel (~> 2.0)
138
+ dry-validation (~> 1.1)
139
+ pg (~> 1.2)
140
+ pry (~> 0.13)
141
+ pry-byebug (~> 3.9)
142
+ rake (~> 13.0)
143
+ rspec (~> 3.0)
144
+ rubocop (~> 0.87)
145
+ simplecov (~> 0.18)
146
+ timecop (~> 0.9)
147
+ webmock (~> 3.7.0)
148
+ yard (~> 0.9)
149
+
150
+ BUNDLED WITH
151
+ 2.2.19
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Cyclone lariat
2
+
3
+ This is gem work like middleware for [shoryuken](https://github.com/ruby-shoryuken/shoryuken). It save all events to database. And catch and produce all exceptions.
4
+
5
+ ![Luna Park](docs/_imgs/lariat.jpg)
6
+
7
+
8
+ ```ruby
9
+ # Gemfile
10
+
11
+ # If use client or middleware
12
+ gem 'cyclone_lariat', require: false
13
+
14
+ # If use client
15
+ gem 'cyclone_lariat'
16
+ ```
17
+
18
+
19
+ ## Client
20
+
21
+ You can use client directly
22
+
23
+ ```ruby
24
+ require 'cyclone_lariat/client' # If require: false in Gemfile
25
+
26
+ client = CycloneLariat::Client.new(
27
+ key: APP_CONF.aws.key,
28
+ secret_key: APP_CONF.aws.secret_key,
29
+ region: APP_CONF.aws.region,
30
+ version: 1, # at default 1
31
+ publisher: 'pilot',
32
+ instance: INSTANCE # at default :prod
33
+ )
34
+ ```
35
+
36
+ You can don't define topic, and it's name will be defined automatically
37
+ ```ruby
38
+ # event_type data topic
39
+ client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' } # prod-event-fanout-pilot-email_is_created
40
+ client.publish_event 'email_is_removed', data: { mail: 'john.doe@example.com' } # prod-event-fanout-pilot-email_is_removed
41
+ ```
42
+ Or you can define it by handle. For example, if you want to send different events to same channel.
43
+ ```ruby
44
+ # event_type data topic
45
+ client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, to: 'prod-event-fanout-pilot-emails'
46
+ client.publish_event 'email_is_removed', data: { mail: 'john.doe@example.com' }, to: 'prod-event-fanout-pilot-emails'
47
+ ```
48
+
49
+ Or you can use client as Repo.
50
+
51
+ ```ruby
52
+ require 'cyclone_lariat/client' # If require: false in Gemfile
53
+
54
+ class YourClient < CycloneLariat::Client
55
+ version 1
56
+ publisher 'pilot'
57
+ instance 'stage'
58
+
59
+ def email_is_created(mail)
60
+ publish event( 'email_is_created',
61
+ data: { mail: mail }
62
+ ),
63
+ to: APP_CONF.aws.fanout.emails
64
+ end
65
+
66
+ def email_is_removed(mail)
67
+ publish event( 'email_is_removed',
68
+ data: { mail: mail }
69
+ ),
70
+ to: APP_CONF.aws.fanout.email
71
+ end
72
+ end
73
+
74
+ # Init repo
75
+ client = YourClient.new(key: APP_CONF.aws.key, secret_key: APP_CONF.aws.secret_key, region: APP_CONF.aws.region)
76
+
77
+ # And send topics
78
+ client.email_is_created 'john.doe@example.com'
79
+ client.email_is_removed 'john.doe@example.com'
80
+ ```
81
+
82
+ # Middleware
83
+ If you use middleware:
84
+ - Store all events to dataset
85
+ - Notify every input sqs message
86
+ - Notify every error
87
+
88
+ ```ruby
89
+ require 'cyclone_lariat/middleware' # If require: false in Gemfile
90
+
91
+ class Receiver
92
+ include Shoryuken::Worker
93
+
94
+ DB = Sequel.connect(host: 'localhost', user: 'ruby')
95
+
96
+ shoryuken_options auto_delete: true,
97
+ body_parser: ->(sqs_msg) {
98
+ JSON.parse(sqs_msg.body, symbolize_names: true)
99
+ },
100
+ queue: 'your_sqs_queue_name'
101
+
102
+ server_middleware do |chain|
103
+
104
+ # Options dataset, errors_notifier and message_notifier is optionals.
105
+ # If you dont define notifiers - middleware does not notify
106
+ # If you dont define dataset - middleware does store events in db
107
+ chain.add CycloneLariat::Middleware,
108
+ dataset: DB[:events],
109
+ errors_notifier: LunaPark::Notifiers::Sentry.new,
110
+ message_notifier: LunaPark::Notifiers::Log.new(min_lvl: :debug, format: :pretty_json)
111
+ end
112
+
113
+ def perform(sqs_message, sqs_message_body)
114
+ # Your logic here
115
+ end
116
+ end
117
+ ```
118
+
119
+ ## Migrations
120
+ Before use events storage add and apply this two migrations
121
+
122
+ ```ruby
123
+
124
+ # First one
125
+
126
+ Sequel.migration do
127
+ up do
128
+ run <<-SQL
129
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
130
+ SQL
131
+ end
132
+
133
+ down do
134
+ run <<-SQL
135
+ DROP EXTENSION IF EXISTS "uuid-ossp";
136
+ SQL
137
+ end
138
+ end
139
+
140
+ # The second one:
141
+ Sequel.migration do
142
+ change do
143
+ create_table :events do
144
+ column :uuid, :uuid, primary_key: true
145
+ String :type, null: false
146
+ Integer :version, null: false
147
+ String :publisher, null: false
148
+ column :data, :json, null: false
149
+ String :error_message, null: true, default: nil
150
+ column :error_details, :json, null: true, default: nil
151
+ DateTime :sent_at, null: true, default: nil
152
+ DateTime :received_at, null: false, default: Sequel::CURRENT_TIMESTAMP
153
+ DateTime :processed_at, null: true, default: nil
154
+ end
155
+ end
156
+ end
157
+ ```
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+
5
+ # tasks from lib directory
6
+ Dir[File.expand_path('lib/tasks/**/*.rake', __dir__)].each do |entity|
7
+ puts entity
8
+ load entity
9
+ end
10
+
11
+ task default: %i[spec] # rubocop]
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ DB_CONF = {
4
+ adapter: 'postgresql',
5
+ host: 'host',
6
+ username: 'postgres',
7
+ password: 'password',
8
+ database: 'cyclone-lariat-test'
9
+ }.freeze
data/config/db.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ DB_CONF = {
4
+ adapter: 'postgresql',
5
+ host: 'localhost',
6
+ username: 'ruby',
7
+ password: 'ruby',
8
+ database: 'cyclone-lariat-test'
9
+ }.freeze
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'cyclone_lariat/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'cyclone_lariat'
9
+ spec.version = CycloneLariat::VERSION
10
+ spec.authors = ['Alexander Kudrin', 'Philip Sorokin']
11
+ spec.email = ['kudrin.alexander@gmail.com']
12
+
13
+ spec.summary = 'Shoryuken middleware for LunaPark based application.'
14
+ spec.homepage = 'https://am-team.github.io/cyclone_lariat/#/'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 2.6.0'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
22
+ else
23
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
24
+ 'public gem pushes.'
25
+ end
26
+ spec.metadata['yard.run'] = 'yri'
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.require_paths = ['lib']
34
+
35
+ spec.add_dependency 'aws-sdk-sns'
36
+ spec.add_dependency 'luna_park', '~> 0.11'
37
+
38
+ spec.add_development_dependency 'bundler', '~> 2.1'
39
+ spec.add_development_dependency 'byebug', '~> 11.1'
40
+ spec.add_development_dependency 'database_cleaner-sequel', '~> 2.0'
41
+ spec.add_development_dependency 'dry-validation', '~> 1.1'
42
+ spec.add_development_dependency 'pg', '~> 1.2'
43
+ spec.add_development_dependency 'pry', '~> 0.13'
44
+ spec.add_development_dependency 'pry-byebug', '~> 3.9'
45
+ spec.add_development_dependency 'rake', '~> 13.0'
46
+ spec.add_development_dependency 'rspec', '~> 3.0'
47
+ spec.add_development_dependency 'rubocop', '~> 0.87'
48
+ spec.add_development_dependency 'simplecov', '~> 0.18'
49
+ spec.add_development_dependency 'timecop', '~> 0.9'
50
+ spec.add_development_dependency 'webmock', '~> 3.7.0'
51
+ spec.add_development_dependency 'yard', '~> 0.9'
52
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ run <<-SQL
6
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
7
+ SQL
8
+ end
9
+
10
+ down do
11
+ run <<-SQL
12
+ DROP EXTENSION IF EXISTS "uuid-ossp";
13
+ SQL
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table :events do
6
+ column :uuid, :uuid, primary_key: true
7
+ String :type, null: false
8
+ Integer :version, null: false
9
+ String :publisher, null: false
10
+ column :data, :json, null: false
11
+ String :error_message, null: true, default: nil
12
+ column :error_details, :json, null: true, default: nil
13
+ DateTime :sent_at, null: true, default: nil
14
+ DateTime :received_at, null: false, default: Sequel::CURRENT_TIMESTAMP
15
+ DateTime :processed_at, null: true, default: nil
16
+ end
17
+ end
18
+ end
Binary file
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cyclone_lariat/client'
4
+ require_relative 'cyclone_lariat/errors'
5
+ require_relative 'cyclone_lariat/event'
6
+ require_relative 'cyclone_lariat/events_repo'
7
+ require_relative 'cyclone_lariat/middleware'
8
+ require_relative 'cyclone_lariat/version'
9
+
10
+ module CycloneLariat; end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+ require 'luna_park/extensions/injector'
5
+ require_relative 'event'
6
+ require_relative 'errors'
7
+
8
+ module CycloneLariat
9
+ class Client
10
+ include LunaPark::Extensions::Injector
11
+
12
+ dependency(:aws_sns_client_class) { Aws::SNS::Client }
13
+ dependency(:aws_credentials_class) { Aws::Credentials }
14
+
15
+ DEFAULT_VERSION = 1
16
+ DEFAULT_INSTANCE = :prod
17
+ SNS_SUFFIX = :fanout
18
+
19
+ def initialize(key:, secret_key:, region:, version: nil, publisher: nil, instance: nil)
20
+ @key = key
21
+ @secret_key = secret_key
22
+ @region = region
23
+ @version = version
24
+ @publisher = publisher
25
+ @instance = instance
26
+ end
27
+
28
+ def event(type, data: {}, version: self.version, uuid: SecureRandom.uuid)
29
+ Event.wrap(
30
+ uuid: uuid,
31
+ type: type,
32
+ sent_at: Time.now.iso8601,
33
+ version: version,
34
+ publisher: publisher,
35
+ data: data
36
+ )
37
+ end
38
+
39
+ def publish(msg, to: nil)
40
+ topic = to || [instance, msg.kind, SNS_SUFFIX, publisher, msg.type].join('-')
41
+
42
+ aws_client.publish(
43
+ topic_arn: topic_arn(topic),
44
+ message: msg.to_json
45
+ )
46
+ end
47
+
48
+ def publish_event(type, data: {}, version: self.version, uuid: SecureRandom.uuid, to: nil)
49
+ publish event(type, data: data, version: version, uuid: uuid), to: to
50
+ end
51
+
52
+ class << self
53
+ def version(version = nil)
54
+ version.nil? ? @version || DEFAULT_VERSION : @version = version
55
+ end
56
+
57
+ def instance(instance = nil)
58
+ instance.nil? ? @instance || DEFAULT_INSTANCE : @instance = instance
59
+ end
60
+
61
+ def publisher(publisher = nil)
62
+ publisher.nil? ? @publisher || (raise 'You should define publisher') : @publisher = publisher
63
+ end
64
+ end
65
+
66
+ def version
67
+ @version ||= self.class.version
68
+ end
69
+
70
+ def publisher
71
+ @publisher ||= self.class.publisher
72
+ end
73
+
74
+ def instance
75
+ @instance ||= self.class.instance
76
+ end
77
+
78
+ private
79
+
80
+ attr_reader :key, :secret_key, :region
81
+
82
+ def topic_arn(topic_name)
83
+ list = aws_client.list_topics.topics
84
+ topic = list.find { |t| t.topic_arn.match?(topic_name) }
85
+ raise Errors::TopicNotFound.new(expected_topic: topic_name, existed_topics: list.map(&:topic_arn)) if topic.nil?
86
+
87
+ topic.topic_arn
88
+ end
89
+
90
+ def aws_client
91
+ @aws_client ||= aws_sns_client_class.new(credentials: aws_credentials, region: region)
92
+ end
93
+
94
+ def aws_credentials
95
+ @aws_credentials ||= aws_credentials_class.new(key, secret_key)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/errors/system'
4
+ require 'luna_park/errors/business'
5
+
6
+ module CycloneLariat
7
+ module Errors
8
+ class TopicNotFound < LunaPark::Errors::System
9
+ message { |d| "Could not found topic: `#{d[:expected_topic]}`" }
10
+ end
11
+
12
+ class ProcessingEventLogic < LunaPark::Errors::Business
13
+ attr_writer :message, :details
14
+
15
+ def ==(other)
16
+ other.is_a?(LunaPark::Errors::Business) &&
17
+ other.message == message &&
18
+ other.details == details
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/entities/attributable'
4
+ require_relative 'errors'
5
+
6
+ module CycloneLariat
7
+ class Event < LunaPark::Entities::Attributable
8
+ KIND = 'event'
9
+
10
+ attr :uuid, String, :new
11
+ attr :publisher, String, :new
12
+ attr :type, String, :new
13
+ attr :error
14
+ attr :version
15
+ attr :data
16
+
17
+ attr_reader :sent_at,
18
+ :processed_at,
19
+ :received_at
20
+
21
+ def kind
22
+ KIND
23
+ end
24
+
25
+ def version=(value)
26
+ @version = Integer(value)
27
+ end
28
+
29
+ def sent_at=(value)
30
+ @sent_at = wrap_time(value)
31
+ end
32
+
33
+ def received_at=(value)
34
+ @received_at = wrap_time(value)
35
+ end
36
+
37
+ def processed_at=(value)
38
+ @processed_at = wrap_time(value)
39
+ end
40
+
41
+ def error_message=(txt)
42
+ @error ||= Errors::ProcessingEventLogic.new
43
+ @error.message = txt
44
+ end
45
+
46
+ def error_details=(details)
47
+ @error ||= Errors::ProcessingEventLogic.new
48
+ @error.details = details
49
+ end
50
+
51
+ def ==(other)
52
+ kind == other.kind &&
53
+ uuid == other.uuid &&
54
+ publisher == other.publisher &&
55
+ type == other.type &&
56
+ error&.message == other.error&.message &&
57
+ error&.details == other.error&.details &&
58
+ version == other.version &&
59
+ sent_at.to_i == other.sent_at.to_i &&
60
+ received_at.to_i == other.received_at.to_i
61
+ processed_at.to_i == other.processed_at.to_i
62
+ end
63
+
64
+ def to_json(*args)
65
+ hash = serialize
66
+ hash[:type] = [kind, hash[:type]].join '_'
67
+ hash.to_json(*args)
68
+ end
69
+
70
+ private
71
+
72
+ def wrap_time(value)
73
+ case value
74
+ when String then Time.parse(value)
75
+ when Time then value
76
+ when NilClass then nil
77
+ else raise ArgumentError, "Unknown type `#{value.class}`"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+
5
+ module CycloneLariat
6
+ class EventsRepo
7
+ attr_reader :dataset
8
+
9
+ def initialize(dataset)
10
+ @dataset = dataset
11
+ end
12
+
13
+ def create(event)
14
+ dataset.insert(
15
+ uuid: event.uuid,
16
+ type: event.type,
17
+ publisher: event.publisher,
18
+ data: JSON.generate(event.data),
19
+ error_message: event.error&.message,
20
+ error_details: JSON.generate(event.error&.details),
21
+ version: event.version,
22
+ sent_at: event.sent_at
23
+ )
24
+ end
25
+
26
+ def exists?(uuid:)
27
+ dataset.where(uuid: uuid).limit(1).any?
28
+ end
29
+
30
+ def processed!(uuid:)
31
+ !dataset.where(uuid: uuid).update(processed_at: Sequel.function(:NOW)).zero?
32
+ end
33
+
34
+ def find(uuid:)
35
+ raw = dataset.where(uuid: uuid).first
36
+ raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
37
+ raw[:error_details] = JSON.parse(raw[:error_details], symbolize_names: true) if raw[:error_details]
38
+ Event.wrap raw
39
+ end
40
+
41
+ def each_unprocessed
42
+ dataset.where(processed_at: nil).each do |raw|
43
+ raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
44
+ raw[:error_details] = JSON.parse(raw[:error_details], symbolize_names: true) if raw[:error_details]
45
+ event = Event.wrap(raw)
46
+ yield(event)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'events_repo'
4
+ require 'luna_park/errors'
5
+ require 'json'
6
+
7
+ module CycloneLariat
8
+ class Middleware
9
+ def initialize(dataset: nil, errors_notifier: nil, message_notifier: nil, repo: EventsRepo)
10
+ @events_repo = repo.new(dataset) if dataset
11
+ @message_notifier = message_notifier
12
+ @errors_notifier = errors_notifier
13
+ end
14
+
15
+ def call(_worker_instance, queue, _sqs_msg, body, &block)
16
+ log_received_message queue, body
17
+
18
+ catch_standard_error(queue, body) do
19
+ event = Event.wrap(JSON.parse(body[:Message]))
20
+
21
+ catch_business_error(event) do
22
+ store_in_dataset(event, &block)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :errors_notifier, :message_notifier, :events_repo
30
+
31
+ def log_received_message(queue, body)
32
+ message_notifier&.info 'Receive message', queue: queue, aws_message_id: body[:MessageId], message: body[:Message]
33
+ end
34
+
35
+ def store_in_dataset(event)
36
+ return yield if events_repo.nil?
37
+ return true if events_repo.exists?(uuid: event.uuid)
38
+
39
+ events_repo.create(event)
40
+ yield
41
+ events_repo.processed! uuid: event.uuid
42
+ end
43
+
44
+ def catch_business_error(event)
45
+ yield
46
+ rescue LunaPark::Errors::Business => e
47
+ errors_notifier&.warning(e, event: event)
48
+ end
49
+
50
+ def catch_standard_error(queue, body)
51
+ yield
52
+ rescue StandardError => e
53
+ errors_notifier&.error(e, queue: queue, aws_message_id: body[:MessageId], message: body[:Message])
54
+ raise e
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ VERSION = '0.2.1'
5
+ end
data/lib/tasks/db.rake ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sequel'
4
+ require_relative '../../config/db'
5
+
6
+ namespace :db do
7
+ desc 'Create database'
8
+ task create: :config do
9
+ cmd = "PGPASSWORD=#{DB_CONF[:password]} createdb" \
10
+ " --username=#{DB_CONF[:username]}" \
11
+ " --host=#{DB_CONF[:host]}" \
12
+ " #{DB_CONF[:database]}"
13
+ puts "Database `#{DB_CONF[:database]}` successfully created" if system(cmd)
14
+ end
15
+
16
+ desc 'Drop database'
17
+ task drop: :config do
18
+ cmd = "PGPASSWORD=#{DB_CONF[:password]} dropdb" \
19
+ " --username=#{DB_CONF[:username]}" \
20
+ " --host=#{DB_CONF[:host]}" \
21
+ " #{DB_CONF[:database]}"
22
+ puts "Database `#{DB_CONF[:database]}` successfully dropped" if system(cmd)
23
+ end
24
+
25
+ desc 'Apply migrations'
26
+ task :migrate, [:version] => :config do |_, args|
27
+ require 'logger'
28
+ require 'sequel/core'
29
+
30
+ Sequel.extension :migration
31
+ version = args[:version] ? args[:version].to_i : nil
32
+ migrations_path = "#{__dir__}/../../db/migrate/"
33
+
34
+ Sequel.connect(**DB_CONF, logger: Logger.new($stdout)) do |db|
35
+ Sequel::Migrator.run(db, migrations_path, target: version)
36
+ end
37
+ end
38
+
39
+ desc 'Database console'
40
+ task console: :config do
41
+ cmd = "PGPASSWORD=#{DB_CONF[:password]} psql" \
42
+ " --username=#{DB_CONF[:username]}" \
43
+ " --host=#{DB_CONF[:host]}" \
44
+ " --port=#{DB_CONF[:port]}" \
45
+ " #{DB_CONF[:database]}"
46
+ puts "Database `#{DB_CONF[:database]}` says 'bye-bye'" if system(cmd)
47
+ end
48
+
49
+ desc 'Reset database - drop, create, & migrate'
50
+ task :reset do
51
+ Rake::Task['db:drop'].invoke
52
+ Rake::Task['db:create'].invoke
53
+ Rake::Task['db:migrate'].invoke
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,294 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cyclone_lariat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Kudrin
8
+ - Philip Sorokin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2021-06-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk-sns
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: luna_park
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.11'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.11'
42
+ - !ruby/object:Gem::Dependency
43
+ name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.1'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.1'
56
+ - !ruby/object:Gem::Dependency
57
+ name: byebug
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '11.1'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '11.1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: database_cleaner-sequel
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '2.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '2.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: dry-validation
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.1'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.1'
98
+ - !ruby/object:Gem::Dependency
99
+ name: pg
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '1.2'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '1.2'
112
+ - !ruby/object:Gem::Dependency
113
+ name: pry
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '0.13'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '0.13'
126
+ - !ruby/object:Gem::Dependency
127
+ name: pry-byebug
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '3.9'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '3.9'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rake
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '13.0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '13.0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: rspec
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '3.0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '3.0'
168
+ - !ruby/object:Gem::Dependency
169
+ name: rubocop
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '0.87'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - "~>"
180
+ - !ruby/object:Gem::Version
181
+ version: '0.87'
182
+ - !ruby/object:Gem::Dependency
183
+ name: simplecov
184
+ requirement: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - "~>"
187
+ - !ruby/object:Gem::Version
188
+ version: '0.18'
189
+ type: :development
190
+ prerelease: false
191
+ version_requirements: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - "~>"
194
+ - !ruby/object:Gem::Version
195
+ version: '0.18'
196
+ - !ruby/object:Gem::Dependency
197
+ name: timecop
198
+ requirement: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - "~>"
201
+ - !ruby/object:Gem::Version
202
+ version: '0.9'
203
+ type: :development
204
+ prerelease: false
205
+ version_requirements: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - "~>"
208
+ - !ruby/object:Gem::Version
209
+ version: '0.9'
210
+ - !ruby/object:Gem::Dependency
211
+ name: webmock
212
+ requirement: !ruby/object:Gem::Requirement
213
+ requirements:
214
+ - - "~>"
215
+ - !ruby/object:Gem::Version
216
+ version: 3.7.0
217
+ type: :development
218
+ prerelease: false
219
+ version_requirements: !ruby/object:Gem::Requirement
220
+ requirements:
221
+ - - "~>"
222
+ - !ruby/object:Gem::Version
223
+ version: 3.7.0
224
+ - !ruby/object:Gem::Dependency
225
+ name: yard
226
+ requirement: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - "~>"
229
+ - !ruby/object:Gem::Version
230
+ version: '0.9'
231
+ type: :development
232
+ prerelease: false
233
+ version_requirements: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - "~>"
236
+ - !ruby/object:Gem::Version
237
+ version: '0.9'
238
+ description:
239
+ email:
240
+ - kudrin.alexander@gmail.com
241
+ executables: []
242
+ extensions: []
243
+ extra_rdoc_files: []
244
+ files:
245
+ - ".gitignore"
246
+ - ".rspec"
247
+ - ".rubocop.yml"
248
+ - ".ruby-gemset"
249
+ - ".ruby-version"
250
+ - CHANGELOG.md
251
+ - Gemfile
252
+ - Gemfile.lock
253
+ - README.md
254
+ - Rakefile
255
+ - config/db.example.rb
256
+ - config/db.rb
257
+ - cyclone_lariat.gemspec
258
+ - db/migrate/01_add_uuid_extensions.rb
259
+ - db/migrate/02_add_events.rb
260
+ - docs/_imgs/lariat.jpg
261
+ - lib/cyclone_lariat.rb
262
+ - lib/cyclone_lariat/client.rb
263
+ - lib/cyclone_lariat/errors.rb
264
+ - lib/cyclone_lariat/event.rb
265
+ - lib/cyclone_lariat/events_repo.rb
266
+ - lib/cyclone_lariat/middleware.rb
267
+ - lib/cyclone_lariat/version.rb
268
+ - lib/tasks/db.rake
269
+ homepage: https://am-team.github.io/cyclone_lariat/#/
270
+ licenses:
271
+ - MIT
272
+ metadata:
273
+ allowed_push_host: https://rubygems.org
274
+ yard.run: yri
275
+ post_install_message:
276
+ rdoc_options: []
277
+ require_paths:
278
+ - lib
279
+ required_ruby_version: !ruby/object:Gem::Requirement
280
+ requirements:
281
+ - - ">="
282
+ - !ruby/object:Gem::Version
283
+ version: 2.6.0
284
+ required_rubygems_version: !ruby/object:Gem::Requirement
285
+ requirements:
286
+ - - ">="
287
+ - !ruby/object:Gem::Version
288
+ version: '0'
289
+ requirements: []
290
+ rubygems_version: 3.0.6
291
+ signing_key:
292
+ specification_version: 4
293
+ summary: Shoryuken middleware for LunaPark based application.
294
+ test_files: []