cyclone_lariat 0.2.3 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +5 -1
- data/README.md +84 -30
- data/cyclone_lariat.gemspec +1 -0
- data/db/migrate/02_add_events.rb +2 -1
- data/lib/cyclone_lariat.rb +2 -2
- data/lib/cyclone_lariat/abstract/client.rb +94 -0
- data/lib/cyclone_lariat/abstract/message.rb +84 -0
- data/lib/cyclone_lariat/command.rb +13 -0
- data/lib/cyclone_lariat/event.rb +2 -74
- data/lib/cyclone_lariat/messages_repo.rb +81 -0
- data/lib/cyclone_lariat/middleware.rb +18 -25
- data/lib/cyclone_lariat/sns_client.rb +41 -0
- data/lib/cyclone_lariat/sqs_client.rb +41 -0
- data/lib/cyclone_lariat/version.rb +1 -1
- metadata +22 -4
- data/lib/cyclone_lariat/client.rb +0 -98
- data/lib/cyclone_lariat/events_repo.rb +0 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3fe1faa17fac474b04c873eb81272506724e8ac3ac23e18549b2f63287174e97
|
4
|
+
data.tar.gz: d645f4859619af32a96448a8357aaf08c6ffc31e1138666df149e44284e00389
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4acacb0d92e72eb15a7247f04e06ad171935f3ab54da976a3427aec45af35beac28f7ce2bbfd79213ce7331cf07541bc7d4a839ff44fc0c76359119b5e341503
|
7
|
+
data.tar.gz: 23b2bc0a32aa5b10392392249f4f23ee8db12edaafd994fd5505b854da3a5f8773f5d101e972678d6e8c33b74bde1f5a2ab4d68ff757852c513e409c5de9bf89
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,28 @@ 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.3.3] - 2021-07-14
|
8
|
+
Changed
|
9
|
+
- Bugfix of message equality check
|
10
|
+
|
11
|
+
## [0.3.2] - 2021-06-11
|
12
|
+
Changed
|
13
|
+
- Bugfix
|
14
|
+
|
15
|
+
## [0.3.1] - 2021-06-11
|
16
|
+
Changed
|
17
|
+
- Command
|
18
|
+
- SqsClient
|
19
|
+
|
20
|
+
## [0.3.0] - 2021-06-09
|
21
|
+
Added
|
22
|
+
- Command
|
23
|
+
- SqsClient
|
24
|
+
|
25
|
+
Changed:
|
26
|
+
- Client renamed to SnsClient
|
27
|
+
- `to:` renamed to `topic:`
|
28
|
+
|
7
29
|
## [0.2.3] - 2021-06-09
|
8
30
|
Added
|
9
31
|
- Skip on empty message with error notify
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
cyclone_lariat (0.2
|
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
@@ -19,59 +19,82 @@ gem 'cyclone_lariat'
|
|
19
19
|
|
20
20
|
![diagram](docs/_imgs/diagram.png)
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
25
38
|
You can use client directly
|
26
39
|
|
27
40
|
```ruby
|
28
|
-
require 'cyclone_lariat/
|
41
|
+
require 'cyclone_lariat/sns_client' # If require: false in Gemfile
|
29
42
|
|
30
|
-
client = CycloneLariat::
|
31
|
-
key:
|
43
|
+
client = CycloneLariat::SnsClient.new(
|
44
|
+
key: APP_CONF.aws.key,
|
32
45
|
secret_key: APP_CONF.aws.secret_key,
|
33
|
-
region:
|
34
|
-
version:
|
35
|
-
publisher:
|
36
|
-
instance:
|
46
|
+
region: APP_CONF.aws.region,
|
47
|
+
version: 1, # at default 1
|
48
|
+
publisher: 'pilot',
|
49
|
+
instance: INSTANCE # at default :prod
|
37
50
|
)
|
38
51
|
```
|
39
52
|
|
40
53
|
You can don't define topic, and it's name will be defined automatically
|
41
54
|
```ruby
|
42
55
|
# event_type data topic
|
43
|
-
client.publish_event
|
44
|
-
client.publish_event
|
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
|
45
59
|
```
|
46
60
|
Or you can define it by handle. For example, if you want to send different events to same channel.
|
47
61
|
```ruby
|
48
62
|
# event_type data topic
|
49
|
-
client.publish_event
|
50
|
-
client.publish_event
|
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'
|
51
66
|
```
|
52
67
|
|
53
68
|
Or you can use client as Repo.
|
54
69
|
|
55
70
|
```ruby
|
56
|
-
require 'cyclone_lariat/
|
71
|
+
require 'cyclone_lariat/sns_client' # If require: false in Gemfile
|
57
72
|
|
58
|
-
class YourClient < CycloneLariat::
|
59
|
-
version
|
73
|
+
class YourClient < CycloneLariat::SnsClient
|
74
|
+
version 1
|
60
75
|
publisher 'pilot'
|
61
|
-
instance
|
62
|
-
|
76
|
+
instance 'stage'
|
77
|
+
|
63
78
|
def email_is_created(mail)
|
64
|
-
publish event(
|
65
|
-
|
66
|
-
|
67
|
-
|
79
|
+
publish event('email_is_created',
|
80
|
+
data: { mail: mail }
|
81
|
+
),
|
82
|
+
to: APP_CONF.aws.fanout.emails
|
68
83
|
end
|
69
|
-
|
84
|
+
|
70
85
|
def email_is_removed(mail)
|
71
|
-
publish event(
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
75
98
|
end
|
76
99
|
end
|
77
100
|
|
@@ -81,8 +104,40 @@ client = YourClient.new(key: APP_CONF.aws.key, secret_key: APP_CONF.aws.secret_k
|
|
81
104
|
# And send topics
|
82
105
|
client.email_is_created 'john.doe@example.com'
|
83
106
|
client.email_is_removed 'john.doe@example.com'
|
107
|
+
client.delete_user 'john.doe@example.com'
|
84
108
|
```
|
85
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
|
+
)
|
125
|
+
```
|
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
|
+
|
86
141
|
# Middleware
|
87
142
|
If you use middleware:
|
88
143
|
- Store all events to dataset
|
@@ -104,7 +159,6 @@ class Receiver
|
|
104
159
|
queue: 'your_sqs_queue_name'
|
105
160
|
|
106
161
|
server_middleware do |chain|
|
107
|
-
|
108
162
|
# Options dataset, errors_notifier and message_notifier is optionals.
|
109
163
|
# If you dont define notifiers - middleware does not notify
|
110
164
|
# If you dont define dataset - middleware does store events in db
|
@@ -144,7 +198,7 @@ end
|
|
144
198
|
# The second one:
|
145
199
|
Sequel.migration do
|
146
200
|
change do
|
147
|
-
create_table :
|
201
|
+
create_table :async_messages do
|
148
202
|
column :uuid, :uuid, primary_key: true
|
149
203
|
String :type, null: false
|
150
204
|
Integer :version, null: false
|
data/cyclone_lariat.gemspec
CHANGED
data/db/migrate/02_add_events.rb
CHANGED
data/lib/cyclone_lariat.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'cyclone_lariat/
|
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/
|
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
|
data/lib/cyclone_lariat/event.rb
CHANGED
@@ -1,85 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require_relative 'errors'
|
3
|
+
require_relative 'abstract/message'
|
5
4
|
|
6
5
|
module CycloneLariat
|
7
|
-
class Event <
|
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 :client_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 client_error_message=(txt)
|
42
|
-
return unless txt
|
43
|
-
|
44
|
-
@client_error ||= Errors::ClientError.new
|
45
|
-
@client_error.message = txt
|
46
|
-
end
|
47
|
-
|
48
|
-
def client_error_details=(details)
|
49
|
-
return unless details
|
50
|
-
|
51
|
-
@client_error ||= Errors::ClientError.new
|
52
|
-
@client_error.details = details
|
53
|
-
end
|
54
|
-
|
55
|
-
def ==(other)
|
56
|
-
kind == other.kind &&
|
57
|
-
uuid == other.uuid &&
|
58
|
-
publisher == other.publisher &&
|
59
|
-
type == other.type &&
|
60
|
-
client_error&.message == other.client_error&.message &&
|
61
|
-
client_error&.details == other.client_error&.details &&
|
62
|
-
version == other.version &&
|
63
|
-
sent_at.to_i == other.sent_at.to_i &&
|
64
|
-
received_at.to_i == other.received_at.to_i
|
65
|
-
processed_at.to_i == other.processed_at.to_i
|
66
|
-
end
|
67
|
-
|
68
|
-
def to_json(*args)
|
69
|
-
hash = serialize
|
70
|
-
hash[:type] = [kind, hash[:type]].join '_'
|
71
|
-
hash.to_json(*args)
|
72
|
-
end
|
73
|
-
|
74
|
-
private
|
75
|
-
|
76
|
-
def wrap_time(value)
|
77
|
-
case value
|
78
|
-
when String then Time.parse(value)
|
79
|
-
when Time then value
|
80
|
-
when NilClass then nil
|
81
|
-
else raise ArgumentError, "Unknown type `#{value.class}`"
|
82
|
-
end
|
83
|
-
end
|
84
12
|
end
|
85
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,27 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
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:
|
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
|
-
|
16
|
+
msg = receive_message(body)
|
17
17
|
|
18
|
-
|
19
|
-
return true unless check(body[:Message])
|
20
|
-
|
21
|
-
event = Event.wrap(JSON.parse(body[:Message]))
|
18
|
+
message_notifier&.info 'Receive message', message: msg, queue: queue
|
22
19
|
|
23
|
-
|
24
|
-
|
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)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -30,15 +30,17 @@ module CycloneLariat
|
|
30
30
|
|
31
31
|
attr_reader :errors_notifier, :message_notifier, :events_repo
|
32
32
|
|
33
|
-
def
|
34
|
-
|
33
|
+
def receive_message(body)
|
34
|
+
body[:Message] ? JSON.parse(body[:Message], symbolize_names: true ) : body
|
35
35
|
end
|
36
36
|
|
37
37
|
def store_in_dataset(event)
|
38
38
|
return yield if events_repo.nil?
|
39
|
-
return true if events_repo.exists?(uuid: event.uuid)
|
40
39
|
|
41
|
-
events_repo.
|
40
|
+
existed = events_repo.find(uuid: event.uuid)
|
41
|
+
return true if existed&.processed?
|
42
|
+
|
43
|
+
events_repo.create(event) unless existed
|
42
44
|
yield
|
43
45
|
events_repo.processed! uuid: event.uuid, error: event.client_error
|
44
46
|
end
|
@@ -50,20 +52,11 @@ module CycloneLariat
|
|
50
52
|
event.client_error = e
|
51
53
|
end
|
52
54
|
|
53
|
-
def catch_standard_error(queue,
|
55
|
+
def catch_standard_error(queue, msg)
|
54
56
|
yield
|
55
|
-
rescue
|
56
|
-
errors_notifier&.error(e, queue: queue,
|
57
|
+
rescue Exception => e
|
58
|
+
errors_notifier&.error(e, queue: queue, message: msg)
|
57
59
|
raise e
|
58
60
|
end
|
59
|
-
|
60
|
-
def check(msg)
|
61
|
-
if msg.nil? || msg.empty?
|
62
|
-
errors_notifier&.error(Errors::EmptyMessage.new)
|
63
|
-
false
|
64
|
-
else
|
65
|
-
true
|
66
|
-
end
|
67
|
-
end
|
68
61
|
end
|
69
62
|
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
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cyclone_lariat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Kudrin
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-07-14 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
|
@@ -260,11 +274,15 @@ files:
|
|
260
274
|
- docs/_imgs/diagram.png
|
261
275
|
- docs/_imgs/lariat.jpg
|
262
276
|
- lib/cyclone_lariat.rb
|
263
|
-
- 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
|
264
280
|
- lib/cyclone_lariat/errors.rb
|
265
281
|
- lib/cyclone_lariat/event.rb
|
266
|
-
- lib/cyclone_lariat/
|
282
|
+
- lib/cyclone_lariat/messages_repo.rb
|
267
283
|
- lib/cyclone_lariat/middleware.rb
|
284
|
+
- lib/cyclone_lariat/sns_client.rb
|
285
|
+
- lib/cyclone_lariat/sqs_client.rb
|
268
286
|
- lib/cyclone_lariat/version.rb
|
269
287
|
- lib/tasks/db.rake
|
270
288
|
homepage: https://am-team.github.io/cyclone_lariat/#/
|
@@ -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,65 +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
|
-
client_error_message: event.client_error&.message,
|
20
|
-
client_error_details: JSON.generate(event.client_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:, error: nil)
|
31
|
-
data = { processed_at: Sequel.function(:NOW) }
|
32
|
-
data.merge!(
|
33
|
-
client_error_message: error&.message,
|
34
|
-
client_error_details: JSON.generate(error&.details),
|
35
|
-
) if error
|
36
|
-
|
37
|
-
!dataset.where(uuid: uuid).update(data).zero?
|
38
|
-
end
|
39
|
-
|
40
|
-
def find(uuid:)
|
41
|
-
raw = dataset.where(uuid: uuid).first
|
42
|
-
raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
|
43
|
-
raw[:client_error_details] = JSON.parse(raw[:client_error_details], symbolize_names: true) if raw[:client_error_details]
|
44
|
-
Event.wrap raw
|
45
|
-
end
|
46
|
-
|
47
|
-
def each_unprocessed
|
48
|
-
dataset.where(processed_at: nil).each do |raw|
|
49
|
-
raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
|
50
|
-
raw[:client_error_details] = JSON.parse(raw[:client_error_details], symbolize_names: true) if raw[:client_error_details]
|
51
|
-
event = Event.wrap(raw)
|
52
|
-
yield(event)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
def each_with_client_errors
|
57
|
-
dataset.where { (processed_at !~ nil) & (client_error_message !~ nil) }.each do |raw|
|
58
|
-
raw[:data] = JSON.parse(raw[:data], symbolize_names: true)
|
59
|
-
raw[:client_error_details] = JSON.parse(raw[:client_error_details], symbolize_names: true) if raw[:client_error_details]
|
60
|
-
event = Event.wrap(raw)
|
61
|
-
yield(event)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|