cyclone_lariat 0.2.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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