cyclone_lariat 0.2.1 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b29f194e1ee444b5621fee3ad9be7547ad612c34acf8cf451d5be0f20976cc4e
4
- data.tar.gz: efdedb4530a004672b9c671c02b899fddedaa1d45b9876d28bce672f86f88812
3
+ metadata.gz: 0cebf79d1deb7a107446827a619897bf59364b89e8745fd767ed001bea3d6272
4
+ data.tar.gz: 6ff840519d31a46ab2f2ae6615176489041154213093052d5f924495dd537a20
5
5
  SHA512:
6
- metadata.gz: 4c784a8c6ec54fef60a529a09d6a45f9dd32bab493e2acabdabed7e2039e743cb547e132a38522ee1b8473a1e2f21644b8d7a25d8bd1daca498e2bef5cd390ee
7
- data.tar.gz: 85628b6886b4a82b639aa0f271b199e9cd85db4cbb316d4621637ca062ad996892f8c171ad32367a64f213b1ebcdd5e495e6f7cdb00f518923e87ecfe41a2610
6
+ metadata.gz: 45eab91fdef2c3c97d6638bf2ec0bf5d83dbaa54b18037a8245d25f2142b4582d60cd771c758f6d9300b2ed51977ce430864606f9155b2a235592470e92db539
7
+ data.tar.gz: 2f58eef796c7e0c52b6f3b8e4341466f7c8baf77ca3a6ff482e4435bd4aad73c04e1a667f39a11d820e774421c58b0519ce8401bdd1984ad872178a85d92a034
@@ -0,0 +1,43 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build + Publish
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ packages: write
14
+
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - name: Set up Ruby 2.6
18
+ uses: actions/setup-ruby@v1
19
+ with:
20
+ ruby-version: 2.6.x
21
+
22
+ - name: Publish to GPR
23
+ run: |
24
+ mkdir -p $HOME/.gem
25
+ touch $HOME/.gem/credentials
26
+ chmod 0600 $HOME/.gem/credentials
27
+ printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
28
+ gem build *.gemspec
29
+ gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
30
+ env:
31
+ GEM_HOST_API_KEY: "Bearer ${{secrets.GITHUB_TOKEN}}"
32
+ OWNER: ${{ github.repository_owner }}
33
+
34
+ - name: Publish to RubyGems
35
+ run: |
36
+ mkdir -p $HOME/.gem
37
+ touch $HOME/.gem/credentials
38
+ chmod 0600 $HOME/.gem/credentials
39
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
40
+ gem build *.gemspec
41
+ gem push *.gem
42
+ env:
43
+ GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
data/.gitignore CHANGED
@@ -9,7 +9,7 @@
9
9
  /tmp/
10
10
  /.tmp/
11
11
  .byebug_history
12
- config/database.yml
12
+ config/db.rb
13
13
 
14
14
  # rspec failure tracking
15
15
  .rspec_status
data/CHANGELOG.md CHANGED
@@ -4,11 +4,37 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.2.0] - 2021-06-2
7
+ ## [0.3.2] - 2021-06-11
8
8
  Changed
9
+ - Bugfix
10
+
11
+ ## [0.3.1] - 2021-06-11
12
+ Changed
13
+ - Command
14
+ - SqsClient
15
+
16
+ ## [0.3.0] - 2021-06-09
17
+ Added
18
+ - Command
19
+ - SqsClient
20
+
21
+ Changed:
22
+ - Client renamed to SnsClient
23
+ - `to:` renamed to `topic:`
24
+
25
+ ## [0.2.3] - 2021-06-09
26
+ Added
27
+ - Skip on empty message with error notify
28
+
29
+ ## [0.2.2] - 2021-06-08
30
+ Changed
31
+ - Fix save to database
32
+ - Rename error to client error
33
+
34
+ ## [0.2.1] - 2021-06-02
9
35
  - Fix can load from database if error_details is nil
10
36
 
11
- ## [0.2.0] - 2021-06-2
37
+ ## [0.2.0] - 2021-06-02
12
38
  Added
13
39
  - Complete tests
14
40
  - Production ready
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- cyclone_lariat (0.2.1)
4
+ cyclone_lariat (0.3.2)
5
5
  aws-sdk-sns
6
+ aws-sdk-sqs
6
7
  luna_park (~> 0.11)
7
8
 
8
9
  GEM
@@ -21,6 +22,9 @@ GEM
21
22
  aws-sdk-sns (1.40.0)
22
23
  aws-sdk-core (~> 3, >= 3.112.0)
23
24
  aws-sigv4 (~> 1.1)
25
+ aws-sdk-sqs (1.39.0)
26
+ aws-sdk-core (~> 3, >= 3.112.0)
27
+ aws-sigv4 (~> 1.1)
24
28
  aws-sigv4 (1.2.3)
25
29
  aws-eventstream (~> 1, >= 1.0.2)
26
30
  byebug (11.1.3)
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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
4
 
5
- ![Luna Park](docs/_imgs/lariat.jpg)
5
+ ![Cyclone lariat](docs/_imgs/lariat.jpg)
6
6
 
7
7
 
8
8
  ```ruby
@@ -15,59 +15,86 @@ gem 'cyclone_lariat', require: false
15
15
  gem 'cyclone_lariat'
16
16
  ```
17
17
 
18
+ ## Logic
18
19
 
19
- ## Client
20
+ ![diagram](docs/_imgs/diagram.png)
20
21
 
22
+ ## Command vs Event
23
+ Commands and events are both simple domain structures that contain solely data for reading. That means they contain no
24
+ behaviour or business logic.
25
+
26
+ A command is an object that is sent to the domain for a state change which is handled by a command handler. They should
27
+ be named with a verb in an imperative mood plus the aggregate name which it operates on. Such request can be rejected
28
+ due to the data the command holds being invalid/inconsistent. There should be exactly 1 handler for each command.
29
+ Once the command has been executed, the consumer can then carry out whatever the task is depending on the output of the
30
+ command.
31
+
32
+ An event is a statement of fact about what change has been made to the domain state. They are named with the aggregate
33
+ name where the change took place plus the verb past-participle. An event happens off the back of a command.
34
+ A command can emit any number of events. The sender of the event does not care who receives it or whether it has been
35
+ received at all.
36
+
37
+ ## SnsClient
21
38
  You can use client directly
22
39
 
23
40
  ```ruby
24
- require 'cyclone_lariat/client' # If require: false in Gemfile
41
+ require 'cyclone_lariat/sns_client' # If require: false in Gemfile
25
42
 
26
- client = CycloneLariat::Client.new(
27
- key: APP_CONF.aws.key,
43
+ client = CycloneLariat::SnsClient.new(
44
+ key: APP_CONF.aws.key,
28
45
  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
46
+ region: APP_CONF.aws.region,
47
+ version: 1, # at default 1
48
+ publisher: 'pilot',
49
+ instance: INSTANCE # at default :prod
33
50
  )
34
51
  ```
35
52
 
36
53
  You can don't define topic, and it's name will be defined automatically
37
54
  ```ruby
38
55
  # 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
56
+ client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' } # prod-event-fanout-pilot-email_is_created
57
+ client.publish_event 'email_is_removed', data: { mail: 'john.doe@example.com' } # prod-event-fanout-pilot-email_is_removed
58
+ client.publish_command 'delete_user', data: { mail: 'john.doe@example.com' } # prod-command-fanout-pilot-delete_user
41
59
  ```
42
60
  Or you can define it by handle. For example, if you want to send different events to same channel.
43
61
  ```ruby
44
62
  # 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'
63
+ client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, topic: 'prod-event-fanout-pilot-emails'
64
+ client.publish_event 'email_is_removed', data: { mail: 'john.doe@example.com' }, topic: 'prod-event-fanout-pilot-emails'
65
+ client.publish_command 'delete_user', data: { mail: 'john.doe@example.com' }, topic: 'prod-command-fanout-pilot-emails'
47
66
  ```
48
67
 
49
68
  Or you can use client as Repo.
50
69
 
51
70
  ```ruby
52
- require 'cyclone_lariat/client' # If require: false in Gemfile
71
+ require 'cyclone_lariat/sns_client' # If require: false in Gemfile
53
72
 
54
- class YourClient < CycloneLariat::Client
55
- version 1
73
+ class YourClient < CycloneLariat::SnsClient
74
+ version 1
56
75
  publisher 'pilot'
57
- instance 'stage'
58
-
76
+ instance 'stage'
77
+
59
78
  def email_is_created(mail)
60
- publish event( 'email_is_created',
61
- data: { mail: mail }
62
- ),
63
- to: APP_CONF.aws.fanout.emails
79
+ publish event('email_is_created',
80
+ data: { mail: mail }
81
+ ),
82
+ to: APP_CONF.aws.fanout.emails
64
83
  end
65
-
84
+
66
85
  def email_is_removed(mail)
67
- publish event( 'email_is_removed',
68
- data: { mail: mail }
69
- ),
70
- to: APP_CONF.aws.fanout.email
86
+ publish event('email_is_removed',
87
+ data: { mail: mail }
88
+ ),
89
+ to: APP_CONF.aws.fanout.email
90
+ end
91
+
92
+
93
+ def delete_user(mail)
94
+ publish command('delete_user',
95
+ data: { mail: mail }
96
+ ),
97
+ to: APP_CONF.aws.fanout.email
71
98
  end
72
99
  end
73
100
 
@@ -77,8 +104,40 @@ client = YourClient.new(key: APP_CONF.aws.key, secret_key: APP_CONF.aws.secret_k
77
104
  # And send topics
78
105
  client.email_is_created 'john.doe@example.com'
79
106
  client.email_is_removed 'john.doe@example.com'
107
+ client.delete_user 'john.doe@example.com'
108
+ ```
109
+
110
+
111
+ # SqsClient
112
+ SqsClient is really similar to SnsClient. It can be initialized in same way:
113
+
114
+ ```ruby
115
+ require 'cyclone_lariat/sns_client' # If require: false in Gemfile
116
+
117
+ client = CycloneLariat::SqsClient.new(
118
+ key: APP_CONF.aws.key,
119
+ secret_key: APP_CONF.aws.secret_key,
120
+ region: APP_CONF.aws.region,
121
+ version: 1, # at default 1
122
+ publisher: 'pilot',
123
+ instance: INSTANCE # at default :prod
124
+ )
80
125
  ```
81
126
 
127
+ As you see all params identity. And you can easily change your sqs-queue to sns-topic when you start work with more
128
+ subscribes. But you should define destination.
129
+
130
+ ```ruby
131
+ client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, dest: 'notify_service'
132
+ # prod-event-queue-pilot-email_is_created-notify_service
133
+ ```
134
+
135
+ Or you can define topic directly:
136
+ ```ruby
137
+ client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, topic: 'prod-event-fanout-pilot-emails'
138
+ ```
139
+
140
+
82
141
  # Middleware
83
142
  If you use middleware:
84
143
  - Store all events to dataset
@@ -100,7 +159,6 @@ class Receiver
100
159
  queue: 'your_sqs_queue_name'
101
160
 
102
161
  server_middleware do |chain|
103
-
104
162
  # Options dataset, errors_notifier and message_notifier is optionals.
105
163
  # If you dont define notifiers - middleware does not notify
106
164
  # If you dont define dataset - middleware does store events in db
@@ -140,18 +198,36 @@ end
140
198
  # The second one:
141
199
  Sequel.migration do
142
200
  change do
143
- create_table :events do
201
+ create_table :async_messages do
144
202
  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
203
+ String :type, null: false
204
+ Integer :version, null: false
205
+ String :publisher, null: false
206
+ column :data, :json, null: false
207
+ String :client_error_message, null: true, default: nil
208
+ column :client_error_details, :json, null: true, default: nil
209
+ DateTime :sent_at, null: true, default: nil
210
+ DateTime :received_at, null: false, default: Sequel::CURRENT_TIMESTAMP
211
+ DateTime :processed_at, null: true, default: nil
154
212
  end
155
213
  end
156
214
  end
157
215
  ```
216
+
217
+ ### Rake tasks
218
+
219
+ For simplify write some Rake tasks you can use CycloneLariat::Repo.
220
+
221
+ ```ruby
222
+ # For retry all unprocessed
223
+
224
+ CycloneLariat.new(DB[:events]).each_unprocessed do |event|
225
+ # Your logic here
226
+ end
227
+
228
+ # For retry all events with client errors
229
+
230
+ CycloneLariat.new(DB[:events]).each_with_client_errors do |event|
231
+ # Your logic here
232
+ end
233
+ ```
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
19
  # to allow pushing to a single host or delete this section to allow pushing to any host.
20
20
  if spec.respond_to?(:metadata)
21
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+ # spec.metadata['allowed_push_host'] = 'https://rubygems.org'
22
22
  else
23
23
  raise 'RubyGems 2.0 or newer is required to protect against ' \
24
24
  'public gem pushes.'
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.require_paths = ['lib']
34
34
 
35
35
  spec.add_dependency 'aws-sdk-sns'
36
+ spec.add_dependency 'aws-sdk-sqs'
36
37
  spec.add_dependency 'luna_park', '~> 0.11'
37
38
 
38
39
  spec.add_development_dependency 'bundler', '~> 2.1'
@@ -2,17 +2,18 @@
2
2
 
3
3
  Sequel.migration do
4
4
  change do
5
- create_table :events do
5
+ create_table :async_messages do
6
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
7
+ String :kind, null: false
8
+ String :type, null: false
9
+ Integer :version, null: false
10
+ String :publisher, null: false
11
+ column :data, :json, null: false
12
+ String :client_error_message, null: true, default: nil
13
+ column :client_error_details, :json, null: true, default: nil
14
+ DateTime :sent_at, null: true, default: nil
15
+ DateTime :received_at, null: false, default: Sequel::CURRENT_TIMESTAMP
16
+ DateTime :processed_at, null: true, default: nil
16
17
  end
17
18
  end
18
19
  end
Binary file
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'cyclone_lariat/client'
3
+ require_relative 'cyclone_lariat/sns_client'
4
4
  require_relative 'cyclone_lariat/errors'
5
5
  require_relative 'cyclone_lariat/event'
6
- require_relative 'cyclone_lariat/events_repo'
6
+ require_relative 'cyclone_lariat/messages_repo'
7
7
  require_relative 'cyclone_lariat/middleware'
8
8
  require_relative 'cyclone_lariat/version'
9
9
 
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require 'aws-sdk-sns'
4
+ require 'luna_park/extensions/injector'
5
+ require_relative '../event'
6
+ require_relative '../command'
7
+ require_relative '../errors'
8
+
9
+ module CycloneLariat
10
+ module Abstract
11
+ class Client
12
+ include LunaPark::Extensions::Injector
13
+
14
+ dependency(:aws_client_class) { raise ArgumentError, 'Client class should be defined' }
15
+ dependency(:aws_credentials_class) { Aws::Credentials }
16
+
17
+ DEFAULT_VERSION = 1
18
+ DEFAULT_INSTANCE = :prod
19
+
20
+ def initialize(key:, secret_key:, region:, version: nil, publisher: nil, instance: nil)
21
+ @key = key
22
+ @secret_key = secret_key
23
+ @region = region
24
+ @version = version
25
+ @publisher = publisher
26
+ @instance = instance
27
+ end
28
+
29
+ def event(type, data: {}, version: self.version, uuid: SecureRandom.uuid)
30
+ Event.wrap(
31
+ uuid: uuid,
32
+ type: type,
33
+ sent_at: Time.now.iso8601,
34
+ version: version,
35
+ publisher: publisher,
36
+ data: data
37
+ )
38
+ end
39
+
40
+ def command(type, data: {}, version: self.version, uuid: SecureRandom.uuid)
41
+ Command.wrap(
42
+ uuid: uuid,
43
+ type: type,
44
+ sent_at: Time.now.iso8601,
45
+ version: version,
46
+ publisher: publisher,
47
+ data: data
48
+ )
49
+ end
50
+
51
+ def publish
52
+ raise LunaPark::Errors::AbstractMethod, 'Publish method should be defined'
53
+ end
54
+
55
+ class << self
56
+ def version(version = nil)
57
+ version.nil? ? @version || DEFAULT_VERSION : @version = version
58
+ end
59
+
60
+ def instance(instance = nil)
61
+ instance.nil? ? @instance || DEFAULT_INSTANCE : @instance = instance
62
+ end
63
+
64
+ def publisher(publisher = nil)
65
+ publisher.nil? ? @publisher || (raise 'You should define publisher') : @publisher = publisher
66
+ end
67
+ end
68
+
69
+ def version
70
+ @version ||= self.class.version
71
+ end
72
+
73
+ def publisher
74
+ @publisher ||= self.class.publisher
75
+ end
76
+
77
+ def instance
78
+ @instance ||= self.class.instance
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :key, :secret_key, :region
84
+
85
+ def aws_client
86
+ @aws_client ||= aws_client_class.new(credentials: aws_credentials, region: region)
87
+ end
88
+
89
+ def aws_credentials
90
+ @aws_credentials ||= aws_credentials_class.new(key, secret_key)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/entities/attributable'
4
+ require_relative '../errors'
5
+
6
+ module CycloneLariat
7
+ module Abstract
8
+ class Message < LunaPark::Entities::Attributable
9
+ attr :uuid, String, :new
10
+ attr :publisher, String, :new
11
+ attr :type, String, :new
12
+ attrs :client_error, :version, :data,
13
+ :sent_at, :processed_at, :received_at
14
+
15
+ def kind
16
+ raise LunaPark::Errors::AbstractMethod
17
+ end
18
+
19
+ def version=(value)
20
+ @version = Integer(value)
21
+ end
22
+
23
+ def sent_at=(value)
24
+ @sent_at = wrap_time(value)
25
+ end
26
+
27
+ def received_at=(value)
28
+ @received_at = wrap_time(value)
29
+ end
30
+
31
+ def processed_at=(value)
32
+ @processed_at = wrap_time(value)
33
+ end
34
+
35
+ def processed?
36
+ !@processed_at.nil?
37
+ end
38
+
39
+ def client_error_message=(txt)
40
+ return unless txt
41
+
42
+ @client_error ||= Errors::ClientError.new
43
+ @client_error.message = txt
44
+ end
45
+
46
+ def client_error_details=(details)
47
+ return unless details
48
+
49
+ @client_error ||= Errors::ClientError.new
50
+ @client_error.details = details
51
+ end
52
+
53
+ def ==(other)
54
+ kind == other.kind &&
55
+ uuid == other.uuid &&
56
+ publisher == other.publisher &&
57
+ type == other.type &&
58
+ client_error&.message == other.client_error&.message &&
59
+ client_error&.details == other.client_error&.details &&
60
+ version == other.version &&
61
+ sent_at.to_i == other.sent_at.to_i &&
62
+ received_at.to_i == other.received_at.to_i
63
+ processed_at.to_i == other.processed_at.to_i
64
+ end
65
+
66
+ def to_json(*args)
67
+ hash = serialize
68
+ hash[:type] = [kind, hash[:type]].join '_'
69
+ hash.to_json(*args)
70
+ end
71
+
72
+ private
73
+
74
+ def wrap_time(value)
75
+ case value
76
+ when String then Time.parse(value)
77
+ when Time then value
78
+ when NilClass then nil
79
+ else raise ArgumentError, "Unknown type `#{value.class}`"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'abstract/message'
4
+
5
+ module CycloneLariat
6
+ class Command < Abstract::Message
7
+ KIND = 'command'
8
+
9
+ def kind
10
+ KIND
11
+ end
12
+ end
13
+ end
@@ -5,11 +5,15 @@ require 'luna_park/errors/business'
5
5
 
6
6
  module CycloneLariat
7
7
  module Errors
8
+ class EmptyMessage < LunaPark::Errors::System
9
+ message 'Received message is empty'
10
+ end
11
+
8
12
  class TopicNotFound < LunaPark::Errors::System
9
13
  message { |d| "Could not found topic: `#{d[:expected_topic]}`" }
10
14
  end
11
15
 
12
- class ProcessingEventLogic < LunaPark::Errors::Business
16
+ class ClientError < LunaPark::Errors::Business
13
17
  attr_writer :message, :details
14
18
 
15
19
  def ==(other)
@@ -1,81 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'luna_park/entities/attributable'
4
- require_relative 'errors'
3
+ require_relative 'abstract/message'
5
4
 
6
5
  module CycloneLariat
7
- class Event < LunaPark::Entities::Attributable
6
+ class Event < Abstract::Message
8
7
  KIND = 'event'
9
8
 
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
9
  def kind
22
10
  KIND
23
11
  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
12
  end
81
13
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+
5
+ module CycloneLariat
6
+ class MessagesRepo
7
+ attr_reader :dataset
8
+
9
+ def initialize(dataset)
10
+ @dataset = dataset
11
+ end
12
+
13
+ def create(msg)
14
+ dataset.insert(
15
+ uuid: msg.uuid,
16
+ kind: msg.kind,
17
+ type: msg.type,
18
+ publisher: msg.publisher,
19
+ data: JSON.generate(msg.data),
20
+ client_error_message: msg.client_error&.message,
21
+ client_error_details: JSON.generate(msg.client_error&.details),
22
+ version: msg.version,
23
+ sent_at: msg.sent_at
24
+ )
25
+ end
26
+
27
+ def exists?(uuid:)
28
+ dataset.where(uuid: uuid).limit(1).any?
29
+ end
30
+
31
+ def processed!(uuid:, error: nil)
32
+ data = { processed_at: Sequel.function(:NOW) }
33
+ data.merge!(client_error_message: error.message, client_error_details: JSON.generate(error.details)) if error
34
+
35
+ !dataset.where(uuid: uuid).update(data).zero?
36
+ end
37
+
38
+ def find(uuid:)
39
+ raw = dataset.where(uuid: uuid).first
40
+ return nil unless raw
41
+
42
+ raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
43
+ if raw[:client_error_details]
44
+ raw[:client_error_details] = JSON.parse(raw[:client_error_details], symbolize_names: true)
45
+ end
46
+ build raw
47
+ end
48
+
49
+ def each_unprocessed
50
+ dataset.where(processed_at: nil).each do |raw|
51
+ raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
52
+ if raw[:client_error_details]
53
+ raw[:client_error_details] = JSON.parse(raw[:client_error_details], symbolize_names: true)
54
+ end
55
+ msg = build raw
56
+ yield(msg)
57
+ end
58
+ end
59
+
60
+ def each_with_client_errors
61
+ dataset.where { (processed_at !~ nil) & (client_error_message !~ nil) }.each do |raw|
62
+ raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
63
+ if raw[:client_error_details]
64
+ raw[:client_error_details] = JSON.parse(raw[:client_error_details], symbolize_names: true)
65
+ end
66
+ msg = build raw
67
+ yield(msg)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def build(raw)
74
+ case kind = raw.delete(:kind)
75
+ when 'event' then Event.wrap raw
76
+ when 'command' then Command.wrap raw
77
+ else raise ArgumentError, "Unknown kind `#{kind}` of message"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'events_repo'
3
+ require_relative 'messages_repo'
4
4
  require 'luna_park/errors'
5
5
  require 'json'
6
6
 
7
7
  module CycloneLariat
8
8
  class Middleware
9
- def initialize(dataset: nil, errors_notifier: nil, message_notifier: nil, repo: EventsRepo)
9
+ def initialize(dataset: nil, errors_notifier: nil, message_notifier: nil, repo: MessagesRepo)
10
10
  @events_repo = repo.new(dataset) if dataset
11
11
  @message_notifier = message_notifier
12
12
  @errors_notifier = errors_notifier
13
13
  end
14
14
 
15
15
  def call(_worker_instance, queue, _sqs_msg, body, &block)
16
- log_received_message queue, body
16
+ msg = receive_message(body)
17
17
 
18
- catch_standard_error(queue, body) do
19
- event = Event.wrap(JSON.parse(body[:Message]))
18
+ message_notifier&.info 'Receive message', message: msg, queue: queue
20
19
 
21
- catch_business_error(event) do
22
- store_in_dataset(event, &block)
20
+ catch_standard_error(queue, msg) do
21
+ event = Event.wrap(msg)
22
+
23
+ store_in_dataset(event) do
24
+ catch_business_error(event, &block)
23
25
  end
24
26
  end
25
27
  end
@@ -28,29 +30,32 @@ module CycloneLariat
28
30
 
29
31
  attr_reader :errors_notifier, :message_notifier, :events_repo
30
32
 
31
- def log_received_message(queue, body)
32
- message_notifier&.info 'Receive message', queue: queue, aws_message_id: body[:MessageId], message: body[:Message]
33
+ def receive_message(body)
34
+ body[:Message] ? JSON.parse(body[:Message], symbolize_names: true ) : body
33
35
  end
34
36
 
35
37
  def store_in_dataset(event)
36
38
  return yield if events_repo.nil?
37
- return true if events_repo.exists?(uuid: event.uuid)
38
39
 
39
- events_repo.create(event)
40
+ existed = events_repo.find(uuid: event.uuid)
41
+ return true if existed&.processed?
42
+
43
+ events_repo.create(event) unless existed
40
44
  yield
41
- events_repo.processed! uuid: event.uuid
45
+ events_repo.processed! uuid: event.uuid, error: event.client_error
42
46
  end
43
47
 
44
48
  def catch_business_error(event)
45
49
  yield
46
50
  rescue LunaPark::Errors::Business => e
47
- errors_notifier&.warning(e, event: event)
51
+ errors_notifier&.error(e, event: event)
52
+ event.client_error = e
48
53
  end
49
54
 
50
- def catch_standard_error(queue, body)
55
+ def catch_standard_error(queue, msg)
51
56
  yield
52
- rescue StandardError => e
53
- errors_notifier&.error(e, queue: queue, aws_message_id: body[:MessageId], message: body[:Message])
57
+ rescue Exception => e
58
+ errors_notifier&.error(e, queue: queue, message: msg)
54
59
  raise e
55
60
  end
56
61
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sns'
4
+ require_relative 'abstract/client'
5
+
6
+ module CycloneLariat
7
+ class SnsClient < Abstract::Client
8
+ include LunaPark::Extensions::Injector
9
+
10
+ dependency(:aws_client_class) { Aws::SNS::Client }
11
+
12
+ SNS_SUFFIX = :fanout
13
+
14
+ def publish(msg, topic: nil)
15
+ topic ||= [instance, msg.kind, SNS_SUFFIX, publisher, msg.type].join('-')
16
+
17
+ aws_client.publish(
18
+ topic_arn: topic_arn(topic),
19
+ message: msg.to_json
20
+ )
21
+ end
22
+
23
+ def publish_event(type, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
24
+ publish event(type, data: data, version: version, uuid: uuid), topic: topic
25
+ end
26
+
27
+ def publish_command(type, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
28
+ publish command(type, data: data, version: version, uuid: uuid), topic: topic
29
+ end
30
+
31
+ private
32
+
33
+ def topic_arn(topic_name)
34
+ list = aws_client.list_topics.topics
35
+ topic = list.find { |t| t.topic_arn.match?(topic_name) }
36
+ raise Errors::TopicNotFound.new(expected_topic: topic_name, existed_topics: list.map(&:topic_arn)) if topic.nil?
37
+
38
+ topic.topic_arn
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-sqs'
4
+ require_relative 'abstract/client'
5
+
6
+ module CycloneLariat
7
+ class SqsClient < Abstract::Client
8
+ include LunaPark::Extensions::Injector
9
+
10
+ dependency(:aws_client_class) { Aws::SQS::Client }
11
+
12
+ SQS_SUFFIX = :queue
13
+
14
+ def publish(msg, dest: nil, topic: nil)
15
+ raise ArgumentError, 'You should define dest or topic' if dest.nil? && topic.nil?
16
+
17
+ topic ||= [instance, msg.kind, SQS_SUFFIX, publisher, msg.type, dest].join('-')
18
+
19
+ aws_client.send_message(
20
+ queue_url: url(topic),
21
+ message_body: msg.to_json
22
+ )
23
+ end
24
+
25
+ def publish_event(type, dest: nil, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
26
+ publish event(type, data: data, version: version, uuid: uuid), dest: dest, topic: topic
27
+ end
28
+
29
+ def publish_command(type, dest: nil, data: {}, version: self.version, uuid: SecureRandom.uuid, topic: nil)
30
+ publish command(type, data: data, version: version, uuid: uuid), dest: dest, topic: topic
31
+ end
32
+
33
+ private
34
+
35
+ def url(topic_name)
36
+ aws_client.get_queue_url(queue_name: topic_name).queue_url
37
+ rescue Aws::SQS::Errors::NonExistentQueue => _e
38
+ raise Errors::TopicNotFound.new(expected_topic: topic_name, existed_topics: aws_client.list_queues)
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CycloneLariat
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.2'
5
5
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cyclone_lariat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Kudrin
8
8
  - Philip Sorokin
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-06-02 00:00:00.000000000 Z
12
+ date: 2021-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: aws-sdk-sns
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: aws-sdk-sqs
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: luna_park
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -235,13 +249,14 @@ dependencies:
235
249
  - - "~>"
236
250
  - !ruby/object:Gem::Version
237
251
  version: '0.9'
238
- description:
252
+ description:
239
253
  email:
240
254
  - kudrin.alexander@gmail.com
241
255
  executables: []
242
256
  extensions: []
243
257
  extra_rdoc_files: []
244
258
  files:
259
+ - ".github/workflows/gem-push.yml"
245
260
  - ".gitignore"
246
261
  - ".rspec"
247
262
  - ".rubocop.yml"
@@ -253,26 +268,29 @@ files:
253
268
  - README.md
254
269
  - Rakefile
255
270
  - config/db.example.rb
256
- - config/db.rb
257
271
  - cyclone_lariat.gemspec
258
272
  - db/migrate/01_add_uuid_extensions.rb
259
273
  - db/migrate/02_add_events.rb
274
+ - docs/_imgs/diagram.png
260
275
  - docs/_imgs/lariat.jpg
261
276
  - lib/cyclone_lariat.rb
262
- - lib/cyclone_lariat/client.rb
277
+ - lib/cyclone_lariat/abstract/client.rb
278
+ - lib/cyclone_lariat/abstract/message.rb
279
+ - lib/cyclone_lariat/command.rb
263
280
  - lib/cyclone_lariat/errors.rb
264
281
  - lib/cyclone_lariat/event.rb
265
- - lib/cyclone_lariat/events_repo.rb
282
+ - lib/cyclone_lariat/messages_repo.rb
266
283
  - lib/cyclone_lariat/middleware.rb
284
+ - lib/cyclone_lariat/sns_client.rb
285
+ - lib/cyclone_lariat/sqs_client.rb
267
286
  - lib/cyclone_lariat/version.rb
268
287
  - lib/tasks/db.rake
269
288
  homepage: https://am-team.github.io/cyclone_lariat/#/
270
289
  licenses:
271
290
  - MIT
272
291
  metadata:
273
- allowed_push_host: https://rubygems.org
274
292
  yard.run: yri
275
- post_install_message:
293
+ post_install_message:
276
294
  rdoc_options: []
277
295
  require_paths:
278
296
  - lib
@@ -287,8 +305,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
287
305
  - !ruby/object:Gem::Version
288
306
  version: '0'
289
307
  requirements: []
290
- rubygems_version: 3.0.6
291
- signing_key:
308
+ rubygems_version: 3.0.3.1
309
+ signing_key:
292
310
  specification_version: 4
293
311
  summary: Shoryuken middleware for LunaPark based application.
294
312
  test_files: []
data/config/db.rb DELETED
@@ -1,9 +0,0 @@
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
@@ -1,98 +0,0 @@
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
@@ -1,50 +0,0 @@
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