cyclone_lariat 0.3.10 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/gem-push.yml +4 -4
- data/.gitignore +6 -0
- data/.rubocop.yml +30 -1
- data/CHANGELOG.md +11 -1
- data/Gemfile.lock +137 -30
- data/Guardfile +42 -0
- data/README.md +715 -143
- data/Rakefile +2 -5
- data/bin/cyclone_lariat +206 -0
- data/cyclone_lariat.gemspec +13 -2
- data/lib/cyclone_lariat/clients/abstract.rb +40 -0
- data/lib/cyclone_lariat/clients/sns.rb +163 -0
- data/lib/cyclone_lariat/clients/sqs.rb +114 -0
- data/lib/cyclone_lariat/core.rb +21 -0
- data/lib/cyclone_lariat/errors.rb +38 -0
- data/lib/cyclone_lariat/fake.rb +19 -0
- data/lib/cyclone_lariat/generators/command.rb +53 -0
- data/lib/cyclone_lariat/generators/event.rb +52 -0
- data/lib/cyclone_lariat/generators/queue.rb +30 -0
- data/lib/cyclone_lariat/generators/topic.rb +29 -0
- data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
- data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
- data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
- data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
- data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
- data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
- data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
- data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
- data/lib/cyclone_lariat/middleware.rb +9 -5
- data/lib/cyclone_lariat/migration.rb +151 -0
- data/lib/cyclone_lariat/options.rb +52 -0
- data/lib/cyclone_lariat/presenters/graph.rb +54 -0
- data/lib/cyclone_lariat/presenters/queues.rb +41 -0
- data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
- data/lib/cyclone_lariat/presenters/topics.rb +40 -0
- data/lib/cyclone_lariat/publisher.rb +25 -0
- data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
- data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
- data/lib/cyclone_lariat/repo/messages.rb +43 -0
- data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
- data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
- data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
- data/lib/cyclone_lariat/repo/versions.rb +42 -0
- data/lib/cyclone_lariat/resources/queue.rb +167 -0
- data/lib/cyclone_lariat/resources/topic.rb +132 -0
- data/lib/cyclone_lariat/services/migrate.rb +51 -0
- data/lib/cyclone_lariat/services/rollback.rb +51 -0
- data/lib/cyclone_lariat/version.rb +1 -1
- data/lib/cyclone_lariat.rb +4 -10
- data/lib/tasks/console.rake +13 -0
- data/lib/tasks/cyclone_lariat.rake +42 -0
- data/lib/tasks/db.rake +0 -15
- metadata +161 -20
- data/config/db.example.rb +0 -9
- data/db/migrate/01_add_uuid_extensions.rb +0 -15
- data/db/migrate/02_add_events.rb +0 -19
- data/docs/_imgs/diagram.png +0 -0
- data/docs/_imgs/lariat.jpg +0 -0
- data/lib/cyclone_lariat/abstract/client.rb +0 -106
- data/lib/cyclone_lariat/abstract/message.rb +0 -83
- data/lib/cyclone_lariat/command.rb +0 -13
- data/lib/cyclone_lariat/configure.rb +0 -15
- data/lib/cyclone_lariat/event.rb +0 -13
- data/lib/cyclone_lariat/messages_mapper.rb +0 -46
- data/lib/cyclone_lariat/messages_repo.rb +0 -60
- data/lib/cyclone_lariat/sns_client.rb +0 -38
- data/lib/cyclone_lariat/sqs_client.rb +0 -39
data/README.md
CHANGED
@@ -1,248 +1,820 @@
|
|
1
1
|
# Cyclone lariat
|
2
2
|
|
3
|
-
This
|
3
|
+
This gem work in few scenarios:
|
4
|
+
- As middleware for [shoryuken](https://github.com/ruby-shoryuken/shoryuken).
|
5
|
+
- It saves all events to the database and also catches and throws all exceptions.
|
6
|
+
- As a middleware, it can log all incoming messages.
|
7
|
+
- As a [client](#client--publisher) that can send messages to SNS topics and SQS queues.
|
8
|
+
- Also it can help you with CI\CD to manage topics, queues and subscriptions such as database [migration](#Migrations).
|
4
9
|
|
5
10
|
![Cyclone lariat](docs/_imgs/lariat.jpg)
|
6
11
|
|
12
|
+
## Install and configuration Cyclone Lariat
|
13
|
+
### Install
|
14
|
+
<details>
|
15
|
+
<summary>Sequel</summary>
|
16
|
+
|
17
|
+
#### Install with Sequel
|
18
|
+
Edit Gemfile:
|
19
|
+
```ruby
|
20
|
+
# Gemfile
|
21
|
+
gem 'sequel'
|
22
|
+
gem 'cyclone_lariat'
|
23
|
+
```
|
24
|
+
And run in console:
|
25
|
+
```bash
|
26
|
+
$ bundle install
|
27
|
+
$ cyclone_lariat install
|
28
|
+
```
|
29
|
+
</details>
|
30
|
+
<details>
|
31
|
+
<summary>ActiveRecord</summary>
|
32
|
+
|
33
|
+
#### Install with ActiveRecord
|
34
|
+
Edit Gemfile:
|
35
|
+
```ruby
|
36
|
+
# Gemfile
|
37
|
+
gem 'active_record'
|
38
|
+
gem 'cyclone_lariat'
|
39
|
+
```
|
40
|
+
And run in console:
|
41
|
+
```bash
|
42
|
+
$ bundle install
|
43
|
+
$ cyclone_lariat install --adapter=active_record
|
44
|
+
```
|
45
|
+
</details>
|
46
|
+
|
47
|
+
Last install command will create 2 files:
|
48
|
+
- ./lib/tasks/cyclone_lariat.rake - Rake tasks, for management migrations
|
49
|
+
- ./config/initializers/cyclone_lariat.rb - Configuration default values for cyclone lariat usage
|
50
|
+
|
51
|
+
|
52
|
+
### Configuration
|
53
|
+
<details>
|
54
|
+
<summary>Sequel</summary>
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# frozen_string_literal: true
|
58
|
+
|
59
|
+
CycloneLariat.configure do |c|
|
60
|
+
c.version = 1 # api version
|
61
|
+
|
62
|
+
c.aws_key = ENV['AWS_KEY'] # aws key
|
63
|
+
c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
|
64
|
+
c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
|
65
|
+
c.aws_region = ENV['AWS_REGION'] # aws region
|
66
|
+
|
67
|
+
c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
|
68
|
+
c.instance = ENV['INSTANCE'] # stage, production, test
|
69
|
+
c.driver = :sequel # driver Sequel
|
70
|
+
c.messages_dataset = DB[:async_messages] # Sequel dataset for store income messages (on receiver)
|
71
|
+
c.versions_dataset = DB[:lariat_versions] # Sequel dataset for versions of publisher migrations
|
72
|
+
c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
#### Example migrations
|
77
|
+
Before using the event store, add and apply these migrations:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
Sequel.migration do
|
81
|
+
up do
|
82
|
+
run <<-SQL
|
83
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
84
|
+
SQL
|
85
|
+
end
|
86
|
+
|
87
|
+
down do
|
88
|
+
run <<-SQL
|
89
|
+
DROP EXTENSION IF EXISTS "uuid-ossp";
|
90
|
+
SQL
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
Sequel.migration do
|
95
|
+
change do
|
96
|
+
create_table :async_messages do
|
97
|
+
column :uuid, :uuid, primary_key: true
|
98
|
+
String :type, null: false
|
99
|
+
Integer :version, null: false
|
100
|
+
String :publisher, null: false
|
101
|
+
column :data, :json, null: false
|
102
|
+
String :client_error_message, null: true, default: nil
|
103
|
+
column :client_error_details, :json, null: true, default: nil
|
104
|
+
DateTime :sent_at, null: true, default: nil
|
105
|
+
DateTime :received_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
106
|
+
DateTime :processed_at, null: true, default: nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
Sequel.migration do
|
112
|
+
change do
|
113
|
+
create_table :lariat_versions do
|
114
|
+
Integer :version, null: false, unique: true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
</details>
|
120
|
+
<details>
|
121
|
+
<summary>ActiveRecord</summary>
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
# frozen_string_literal: true
|
125
|
+
|
126
|
+
CycloneLariat.configure do |c|
|
127
|
+
c.version = 1 # api version
|
128
|
+
|
129
|
+
c.aws_key = ENV['AWS_KEY'] # aws key
|
130
|
+
c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
|
131
|
+
c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
|
132
|
+
c.aws_region = ENV['AWS_REGION'] # aws region
|
133
|
+
|
134
|
+
c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
|
135
|
+
c.instance = ENV['INSTANCE'] # stage, production, test
|
136
|
+
c.driver = :active_record # driver ActiveRecord
|
137
|
+
c.messages_dataset = CycloneLariatMessage # ActiveRecord model for store income messages (on receiver)
|
138
|
+
c.versions_dataset = CycloneLariatVersion # ActiveRecord model for versions of publisher migrations
|
139
|
+
c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
#### Example migrations
|
144
|
+
Before using the event store, add and apply these migrations:
|
145
|
+
```ruby
|
146
|
+
# migrations
|
147
|
+
execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
|
148
|
+
|
149
|
+
create_table :cyclone_lariat_messages, id: :uuid, primary_key: :uuid, default: -> { 'public.uuid_generate_v4()' } do |t|
|
150
|
+
t.string :kind, null: false
|
151
|
+
t.string :type, null: false
|
152
|
+
t.integer :version, null: false
|
153
|
+
t.string :publisher, null: false
|
154
|
+
t.jsonb :data, null: false
|
155
|
+
t.string :client_error_message, null: true, default: nil
|
156
|
+
t.jsonb :client_error_details, null: true, default: nil
|
157
|
+
t.datetime :sent_at, null: true, default: nil
|
158
|
+
t.datetime :received_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
|
159
|
+
t.datetime :processed_at, null: true, default: nil
|
160
|
+
end
|
161
|
+
|
162
|
+
create_table :cyclone_lariat_versions do |t|
|
163
|
+
t.integer :version, null: false, index: { unique: true }
|
164
|
+
end
|
165
|
+
|
166
|
+
# models
|
167
|
+
class CycloneLariatMessage < ActiveRecord::Base
|
168
|
+
self.inheritance_column = :_type_disabled
|
169
|
+
self.primary_key = 'uuid'
|
170
|
+
end
|
171
|
+
class CycloneLariatVersion < ActiveRecord::Base
|
172
|
+
end
|
173
|
+
```
|
174
|
+
</details>
|
175
|
+
|
176
|
+
If you are only using your application as a publisher, you may not need to set the _messages_dataset_ parameter.
|
177
|
+
|
178
|
+
## Client / Publisher
|
179
|
+
At first lets understand what the difference between SQS and SNS:
|
180
|
+
- Amazon Simple Queue Service (SQS) lets you send, store, and receive messages between software components at any
|
181
|
+
volume, without losing messages or requiring other services to be available.
|
182
|
+
- Amazon Simple Notification Service (SNS) sends notifications two ways Application2Person (like send sms).
|
183
|
+
And the second way is Application2Application, that's way more important for us. In this way you case use
|
184
|
+
SNS service like fanout.
|
185
|
+
|
186
|
+
![SQS/SNS](docs/_imgs/sqs_sns_diagram.png)
|
187
|
+
|
188
|
+
For use **cyclone_lariat** as _Publisher_ lets make install CycloneLariat.
|
189
|
+
|
190
|
+
Before creating the first migration, let's explain what _CycloneLariat::Messages_ is.
|
191
|
+
|
192
|
+
### Messages
|
193
|
+
Message in Amazon SQS\SNS service it's a
|
194
|
+
[object](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-metadata.html#sqs-message-attributes)
|
195
|
+
that has several attributes. The main attributes are the **body**, which consists of the published
|
196
|
+
data. The body is a _String_, but we can use it as a _JSON_ object. **Cyclone_lariat** use by default scheme - version 1:
|
197
|
+
|
198
|
+
```json
|
199
|
+
// Scheme: version 1
|
200
|
+
{
|
201
|
+
"uuid": "f2ce3813-0905-4d81-a60e-f289f2431f50", // Uniq message identificator
|
202
|
+
"publisher": "sample_app", // Publisher application name
|
203
|
+
"request_id": "51285005-8a06-4181-b5fd-bf29f3b1a45a", // Optional: X-Request-Id
|
204
|
+
"type": "event_note_created", // Type of Event or Command
|
205
|
+
"version": 1, // Version of data structure
|
206
|
+
"data": {
|
207
|
+
"id": 12,
|
208
|
+
"text": "Sample of published data",
|
209
|
+
"attributes": ["one", "two", "three"]
|
210
|
+
},
|
211
|
+
"sent_at": "2022-11-09T11:42:18.203+01:00" // Time when message was sended in ISO8601 Standard
|
212
|
+
}
|
213
|
+
```
|
214
|
+
|
215
|
+
Idea about X-Request-Id you can see at
|
216
|
+
[StackOverflow](https://stackoverflow.com/questions/25433258/what-is-the-x-request-id-http-header).
|
217
|
+
|
218
|
+
As you see, type has prefix 'event_' in cyclone lariat you has two kinds of messages - _Messages::V1::Event_ and
|
219
|
+
_Messages::V1::Command_.
|
220
|
+
|
221
|
+
If you want log all your messages you can use extended scheme - version 2:
|
222
|
+
```json
|
223
|
+
// Scheme: version 2
|
224
|
+
{
|
225
|
+
"uuid": "f2ce3813-0905-4d81-a60e-f289f2431f50", // Uniq message identificator
|
226
|
+
"publisher": "sample_app", // Publisher application name
|
227
|
+
"request_id": "51285005-8a06-4181-b5fd-bf29f3b1a45a", // Optional: X-Request-Id
|
228
|
+
"type": "event_note_created", // Type of Event or Command
|
229
|
+
"version": 2, // Version of data structure
|
230
|
+
"subject": {
|
231
|
+
"type": "user", // Subject type
|
232
|
+
"uuid": "a27c29e2-bbd3-490a-8f1b-caa4f8d902ef" // Subject uuid
|
233
|
+
},
|
234
|
+
"object": {
|
235
|
+
"type": "note", // Object type
|
236
|
+
"uuid": "f46e74db-3335-4c5e-b476-c2a87660a942" // Object uuid
|
237
|
+
},
|
238
|
+
"data": {
|
239
|
+
"id": 12,
|
240
|
+
"text": "Sample of published data",
|
241
|
+
"attributes": ["one", "two", "three"]
|
242
|
+
},
|
243
|
+
"sent_at": "2022-11-09T11:42:18.203+01:00" // Time when message was sended in ISO8601 Standard
|
244
|
+
}
|
245
|
+
```
|
246
|
+
#### Subject vs Object
|
7
247
|
|
248
|
+
The difference between scheme first and second version - is subject and object. This values need to help with actions log.
|
249
|
+
For example, user #42, write to support, "why he could not sign in". The messages log is:
|
250
|
+
|
251
|
+
| Subject | Action | Object |
|
252
|
+
|:---------|:------------|:----------|
|
253
|
+
| user #42 | sign_up | user #42 |
|
254
|
+
| user #42 | sign_in | user #42 |
|
255
|
+
| user #42 | create_note | note #769 |
|
256
|
+
| user #1 | ban | user #42 |
|
257
|
+
|
258
|
+
It is important to understand that user #42 can be both a subject and an object. And you should save both of these fields to keep track of the entire history of this user.
|
259
|
+
|
260
|
+
#### Command vs Event
|
261
|
+
Commands and events are both simple domain structures that contain solely data for reading. That means
|
262
|
+
they contain no behaviour or business logic.
|
263
|
+
|
264
|
+
A command is an object that is sent to the domain for a state change which is handled by a command
|
265
|
+
handler. They should be named with a verb in an imperative mood plus the aggregate name which it
|
266
|
+
operates on. Such request can be rejected due to the data the command holds being invalid/inconsistent.
|
267
|
+
There should be exactly 1 handler for each command. Once the command has been executed, the consumer
|
268
|
+
can then carry out whatever the task is depending on the output of the command.
|
269
|
+
|
270
|
+
An event is a statement of fact about what change has been made to the domain state. They are named
|
271
|
+
with the aggregate name where the change took place plus the verb past-participle. An event happens off
|
272
|
+
the back of a command.
|
273
|
+
A command can emit any number of events. The sender of the event does not care who receives it or
|
274
|
+
whether it has been received at all.
|
275
|
+
|
276
|
+
### Publish
|
277
|
+
For publishing _Messages::V1::Event_ or _Messages::V1::Commands_, you have two ways, send [_Message_](#Messages) directly:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
CycloneLariat.configure do |config|
|
281
|
+
# Options app here
|
282
|
+
end
|
283
|
+
|
284
|
+
client = CycloneLariat::Clients::Sns.new(publisher: 'auth', version: 1)
|
285
|
+
payload = {
|
286
|
+
first_name: 'John',
|
287
|
+
last_name: 'Doe',
|
288
|
+
mail: 'john.doe@example.com'
|
289
|
+
}
|
290
|
+
|
291
|
+
client.publish_command('register_user', data: payload, fifo: false)
|
292
|
+
```
|
293
|
+
|
294
|
+
That's call, will generate a message body:
|
295
|
+
```json
|
296
|
+
{
|
297
|
+
"uuid": "f2ce3813-0905-4d81-a60e-f289f2431f50",
|
298
|
+
"publisher": "auth",
|
299
|
+
"type": "command_register_user",
|
300
|
+
"version": 1,
|
301
|
+
"data": {
|
302
|
+
"first_name": "John",
|
303
|
+
"last_name": "Doe",
|
304
|
+
"mail": "john.doe@example.com"
|
305
|
+
},
|
306
|
+
"sent_at": "2022-11-09T11:42:18.203+01:00" // The time the message was sent. ISO8601 standard.
|
307
|
+
}
|
308
|
+
```
|
309
|
+
|
310
|
+
Or for second schema version code:
|
8
311
|
```ruby
|
9
|
-
|
312
|
+
CycloneLariat.configure do |config|
|
313
|
+
# Options app here
|
314
|
+
end
|
315
|
+
|
316
|
+
client = CycloneLariat::Clients::Sns.new(publisher: 'auth', version: 2)
|
317
|
+
|
318
|
+
client.publish_event(
|
319
|
+
'sign_up',
|
320
|
+
data: {
|
321
|
+
first_name: 'John',
|
322
|
+
last_name: 'Doe',
|
323
|
+
mail: 'john.doe@example.com'
|
324
|
+
},
|
325
|
+
subject: { type: 'user', uuid: '40250522-21c8-4fc7-9b0b-47d9666a4430'},
|
326
|
+
object: { type: 'user', uuid: '40250522-21c8-4fc7-9b0b-47d9666a4430'},
|
327
|
+
fifo: false
|
328
|
+
)
|
329
|
+
```
|
330
|
+
|
331
|
+
Or is it better to make your own client, like a [Repository](https://deviq.com/design-patterns/repository-pattern) pattern.
|
332
|
+
```ruby
|
333
|
+
require 'cyclone_lariat/publisher' # If require: false in Gemfile
|
334
|
+
|
335
|
+
class Publisher < CycloneLariat::Publisher
|
336
|
+
def email_is_created(mail)
|
337
|
+
sns.publish event('email_is_created', data: { mail: mail }), fifo: false
|
338
|
+
end
|
339
|
+
|
340
|
+
def email_is_removed(mail)
|
341
|
+
sns.publish event('email_is_removed', data: { mail: mail }), fifo: false
|
342
|
+
end
|
343
|
+
|
344
|
+
def delete_user(mail)
|
345
|
+
sns.publish command('delete_user', data: { mail: mail }), fifo: false
|
346
|
+
end
|
347
|
+
|
348
|
+
def welcome_message(mail, text)
|
349
|
+
sqs.publish command('welcome', data: {mail: mail, txt: text}), fifo: false
|
350
|
+
end
|
351
|
+
end
|
10
352
|
|
11
|
-
#
|
12
|
-
|
353
|
+
# Init repo
|
354
|
+
publisher = Publisher.new
|
13
355
|
|
14
|
-
#
|
15
|
-
|
356
|
+
# And send topics
|
357
|
+
publisher.email_is_created 'john.doe@example.com'
|
358
|
+
publisher.email_is_removed 'john.doe@example.com'
|
359
|
+
publisher.delete_user 'john.doe@example.com'
|
360
|
+
publisher.welcome_message 'john.doe@example.com', 'You are welcome'
|
16
361
|
```
|
17
362
|
|
18
|
-
|
363
|
+
#### Topics and Queue
|
364
|
+
An Amazon SNS topic and SQS queue is a logical access point that acts as a communication channel. Both
|
365
|
+
of them has specific address ARN.
|
366
|
+
|
367
|
+
```
|
368
|
+
# Topic example
|
369
|
+
arn:aws:sns:eu-west-1:247602342345:test-event-fanout-cyclone_lariat-note_added.fifo
|
19
370
|
|
20
|
-
|
371
|
+
# Queue example
|
372
|
+
arn:aws:sqs:eu-west-1:247602342345:test-event-queue-cyclone_lariat-note_added-notifier.fifo
|
373
|
+
```
|
21
374
|
|
22
|
-
|
23
|
-
|
24
|
-
|
375
|
+
Split ARN:
|
376
|
+
- `arn:aws:sns` - Prefix for SNS Topics
|
377
|
+
- `arn:aws:sqs` - Prefix for SQS Queues
|
378
|
+
- `eu-west-1` - [AWS Region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-regions)
|
379
|
+
- `247602342345` - [AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/console_account-alias.html)
|
380
|
+
- `test-event-fanout-cyclone_lariat-note_added` - Topic \ Queue name
|
381
|
+
- `.fifo` - if Topic or queue is [FIFO](https://aws.amazon.com/blogs/aws/introducing-amazon-sns-fifo-first-in-first-out-pub-sub-messaging/), they must
|
382
|
+
has that suffix.
|
25
383
|
|
26
|
-
|
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.
|
384
|
+
Region and account_id usually set using the **cyclone_lariat** [configuration](#Configuration).
|
31
385
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
received at all.
|
386
|
+
#### Declaration for topic and queues names
|
387
|
+
In **cyclone_lariat** we have a declaration for defining topic and queue names.
|
388
|
+
This can help in organizing the order.
|
36
389
|
|
37
|
-
## Configure
|
38
390
|
```ruby
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
391
|
+
CycloneLariat.configure do |config|
|
392
|
+
config.instance = 'test'
|
393
|
+
config.publisher = 'cyclone_lariat'
|
394
|
+
# ...
|
395
|
+
end
|
396
|
+
|
397
|
+
CycloneLariat::Clients::Sns.new.publish_command(
|
398
|
+
'register_user',
|
399
|
+
data: {
|
400
|
+
first_name: 'John',
|
401
|
+
last_name: 'Doe',
|
402
|
+
mail: 'john.doe@example.com'
|
403
|
+
},
|
404
|
+
fifo: false
|
405
|
+
)
|
406
|
+
|
407
|
+
# or in repository-like style:
|
408
|
+
class Publisher < CycloneLariat::Publisher
|
409
|
+
def register_user(first:, last:, mail:)
|
410
|
+
sns.publish command(
|
411
|
+
'register_user',
|
412
|
+
data: {
|
413
|
+
mail: mail,
|
414
|
+
name: {
|
415
|
+
first: first,
|
416
|
+
last: last
|
417
|
+
}
|
418
|
+
}
|
419
|
+
), fifo: false
|
420
|
+
end
|
48
421
|
end
|
49
422
|
```
|
50
423
|
|
51
|
-
|
52
|
-
You can use client directly
|
424
|
+
We will publish a message on this topic: `test-command-fanout-cyclone_lariat-register_user`.
|
53
425
|
|
426
|
+
Let's split the topic title:
|
427
|
+
- `test` - instance;
|
428
|
+
- `command` - kind - [event or command](#command-vs-event);
|
429
|
+
- `fanount` - resource type - fanout for SNS topics;
|
430
|
+
- `cyclone_lariat` - publisher name;
|
431
|
+
- `regiser_user` - message type.
|
432
|
+
|
433
|
+
For queues you also can define destination.
|
54
434
|
```ruby
|
55
|
-
|
435
|
+
CycloneLariat::Clients::Sqs.new.publish_event(
|
436
|
+
'register_user',
|
437
|
+
data: { mail: 'john.doe@example.com' },
|
438
|
+
dest: :mailer,
|
439
|
+
fifo: false
|
440
|
+
)
|
56
441
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
442
|
+
# or in repository-like style:
|
443
|
+
|
444
|
+
class YourClient < CycloneLariat::Clients::Sns
|
445
|
+
# ...
|
446
|
+
|
447
|
+
def register_user(first:, last:, mail:)
|
448
|
+
publish event('register_user', data: { mail: mail }), fifo: false
|
449
|
+
end
|
450
|
+
end
|
451
|
+
```
|
452
|
+
|
453
|
+
We will publish a message on this queue: `test-event-queue-cyclone_lariat-register_user-mailer`.
|
454
|
+
|
455
|
+
Let's split the queue title:
|
456
|
+
- `test` - instance;
|
457
|
+
- `event` - kind - [event or command](#command-vs-event);
|
458
|
+
- `queue` - resource type - queue for SQS;
|
459
|
+
- `cyclone_lariat` - publisher name;
|
460
|
+
- `regiser_user` - message type.
|
461
|
+
- `mailer` - destination
|
462
|
+
|
463
|
+
You also can sent message to queue with custom name. But this way does not recommended.
|
464
|
+
|
465
|
+
```ruby
|
466
|
+
# Directly
|
467
|
+
CycloneLariat::Clients::Sqs.new.publish_event(
|
468
|
+
'register_user', data: { mail: 'john.doe@example.com' },
|
469
|
+
dest: :mailer, topic: 'custom_topic_name.fifo', fifo: false
|
64
470
|
)
|
471
|
+
|
472
|
+
# Repository
|
473
|
+
class Publisher < CycloneLariat::Publisher
|
474
|
+
# ...
|
475
|
+
|
476
|
+
def register_user(first:, last:, mail:)
|
477
|
+
publish event('register_user', data: { mail: mail }),
|
478
|
+
topic: 'custom_topic_name.fifo', fifo: false
|
479
|
+
end
|
480
|
+
end
|
65
481
|
```
|
482
|
+
Will publish message on queue: `custom_topic_name`
|
483
|
+
|
484
|
+
|
485
|
+
### FIFO and no FIFO
|
486
|
+
The main idea you can read on [AWS Docs](https://aws.amazon.com/blogs/aws/introducing-amazon-sns-fifo-first-in-first-out-pub-sub-messaging/).
|
487
|
+
|
488
|
+
FIFO message should consist two fields:
|
489
|
+
- `group_id` - In each topic, the FIFO sequence is defined only within one group.
|
490
|
+
[AWS Docs](https://docs.aws.amazon.com/sns/latest/dg/fifo-message-grouping.html)
|
491
|
+
- `deduplication_id` - Within the same group, a unique identifier must be defined for each message.
|
492
|
+
[AWS Docs](https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html)
|
493
|
+
|
494
|
+
The unique identifier can definitely be the entire message. In this case, you
|
495
|
+
do not need to pass the deduplication_id parameter. But you must create a queue
|
496
|
+
with the `content_based_deduplication` parameter in migration.
|
497
|
+
|
66
498
|
|
67
|
-
You can don't define topic, and it's name will be defined automatically
|
68
499
|
```ruby
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
500
|
+
class Publisher < CycloneLariat::Publisher
|
501
|
+
def user_created(mail:, uuid:)
|
502
|
+
sns.publish event('user_created', data: {
|
503
|
+
user: {
|
504
|
+
uuid: uuid,
|
505
|
+
mail: mail
|
506
|
+
},
|
507
|
+
},
|
508
|
+
deduplication_id: uuid,
|
509
|
+
group_id: uuid),
|
510
|
+
fifo: true
|
511
|
+
end
|
512
|
+
|
513
|
+
def user_mail_changed(mail:, uuid:)
|
514
|
+
sns.publish event('user_mail_created', data: {
|
515
|
+
user: {
|
516
|
+
uuid: uuid,
|
517
|
+
mail: mail
|
518
|
+
},
|
519
|
+
},
|
520
|
+
deduplication_id: mail,
|
521
|
+
group_id: uuid),
|
522
|
+
fifo: true
|
523
|
+
end
|
524
|
+
end
|
73
525
|
```
|
74
|
-
|
526
|
+
|
527
|
+
### Tests for publishers
|
528
|
+
|
529
|
+
Instead of stub all requests to AWS services, you can set up cyclone lariat for make fake publishing.
|
530
|
+
|
75
531
|
```ruby
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
532
|
+
CycloneLariat.configure do |c|
|
533
|
+
# ...
|
534
|
+
c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
|
535
|
+
end
|
536
|
+
```
|
537
|
+
|
538
|
+
## Migrations
|
539
|
+
|
540
|
+
With **cyclone_lariat** you can use migrations that can create, delete, and subscribe to your queues and topics, just like database migrations do.
|
541
|
+
Before using this function, you must complete the **cyclone_lariat** [configuration](#Configuration).
|
542
|
+
|
543
|
+
```bash
|
544
|
+
$ cyclone_lariat generate migration user_created
|
80
545
|
```
|
81
546
|
|
82
|
-
|
547
|
+
This command should create a migration file, let's edit it.
|
83
548
|
|
84
549
|
```ruby
|
85
|
-
|
550
|
+
# ./lariat/migrate/1668097991_user_created_queue.rb
|
86
551
|
|
87
|
-
|
88
|
-
version 1
|
89
|
-
publisher 'pilot'
|
90
|
-
instance 'stage'
|
552
|
+
# frozen_string_literal: true
|
91
553
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
),
|
96
|
-
to: APP_CONF.aws.fanout.emails
|
554
|
+
class UserCreatedQueue < CycloneLariat::Migration
|
555
|
+
def up
|
556
|
+
create queue(:user_created, dest: :mailer, content_based_deduplication: true, fifo: true)
|
97
557
|
end
|
98
558
|
|
99
|
-
def
|
100
|
-
|
101
|
-
data: { mail: mail }
|
102
|
-
),
|
103
|
-
to: APP_CONF.aws.fanout.email
|
559
|
+
def down
|
560
|
+
delete queue(:user_created, dest: :mailer, content_based_deduplication: true, fifo: true)
|
104
561
|
end
|
562
|
+
end
|
563
|
+
```
|
564
|
+
The `content_based_dedupplication` parameter can only be specified for FIFO resources. When true, the whole message is
|
565
|
+
used as the unique message identifier instead of the `deduplication_id` key.
|
566
|
+
|
567
|
+
To apply migration use:
|
568
|
+
```bash
|
569
|
+
$ rake cyclone_lariat:migrate
|
570
|
+
```
|
105
571
|
|
572
|
+
To decline migration use:
|
573
|
+
```bash
|
574
|
+
$ rake cyclone_lariat:rollback
|
575
|
+
```
|
106
576
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
577
|
+
Since the SNS\SQS management does not support an ACID transaction (in the sense of a database),
|
578
|
+
I highly recommend using the atomic schema:
|
579
|
+
|
580
|
+
```ruby
|
581
|
+
# BAD:
|
582
|
+
class UserCreated < CycloneLariat::Migration
|
583
|
+
def up
|
584
|
+
create queue(:user_created, dest: :mailer, fifo: true)
|
585
|
+
create topic(:user_created, fifo: true)
|
586
|
+
|
587
|
+
subscribe topic: topic(:user_created, fifo: true),
|
588
|
+
endpoint: queue(:user_created, dest: :mailer, fifo: true)
|
589
|
+
end
|
590
|
+
|
591
|
+
def down
|
592
|
+
unsubscribe topic: topic(:user_created, fifo: true),
|
593
|
+
endpoint: queue(:user_created, dest: :mailer, fifo: true)
|
594
|
+
|
595
|
+
delete topic(:user_created, fifo: true)
|
596
|
+
delete queue(:user_created, dest: :mailer, fifo: true)
|
112
597
|
end
|
113
598
|
end
|
114
599
|
|
115
|
-
#
|
116
|
-
|
600
|
+
# GOOD:
|
601
|
+
class UserCreatedQueue < CycloneLariat::Migration
|
602
|
+
def up
|
603
|
+
create queue(:user_created, dest: :mailer, fifo: true)
|
604
|
+
end
|
117
605
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
606
|
+
def down
|
607
|
+
delete queue(:user_created, dest: :mailer, fifo: true)
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
class UserCreatedTopic < CycloneLariat::Migration
|
612
|
+
def up
|
613
|
+
create topic(:user_created, fifo: true)
|
614
|
+
end
|
615
|
+
|
616
|
+
def down
|
617
|
+
delete topic(:user_created, fifo: true)
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
class UserCreatedSubscription < CycloneLariat::Migration
|
622
|
+
def up
|
623
|
+
subscribe topic: topic(:user_created, fifo: true),
|
624
|
+
endpoint: queue(:user_created, dest: :mailer, fifo: true)
|
625
|
+
end
|
626
|
+
|
627
|
+
def down
|
628
|
+
unsubscribe topic: topic(:user_created, fifo: true),
|
629
|
+
endpoint: queue(:user_created, dest: :mailer, fifo: true)
|
630
|
+
end
|
631
|
+
end
|
122
632
|
```
|
123
633
|
|
634
|
+
### Example: one-to-many
|
124
635
|
|
125
|
-
|
126
|
-
|
636
|
+
The first example is when your _registration_ service creates new user. You also have two services:
|
637
|
+
_mailer_ - sending a welcome email, and _statistics_ service.
|
127
638
|
|
128
639
|
```ruby
|
129
|
-
|
640
|
+
create topic(:user_created, fifo: true)
|
641
|
+
create queue(:user_created, dest: :mailer, fifo: true)
|
642
|
+
create queue(:user_created, dest: :stat, fifo: true)
|
130
643
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
instance: INSTANCE # at default :prod
|
138
|
-
)
|
644
|
+
subscribe topic: topic(:user_created, fifo: true),
|
645
|
+
endpoint: queue(:user_created, dest: :mailer, fifo: true)
|
646
|
+
|
647
|
+
|
648
|
+
subscribe topic: topic(:user_created, fifo: true),
|
649
|
+
endpoint: queue(:user_created, dest: :statistic, fifo: true)
|
139
650
|
```
|
651
|
+
![one2many](docs/_imgs/graphviz_01.png)
|
652
|
+
|
653
|
+
### Example: many-to-one
|
140
654
|
|
141
|
-
|
142
|
-
|
655
|
+
The second example is when you have three services: _registration_ - creates new users, _order_
|
656
|
+
service - allows you to create new orders, _statistics_ service collects all statistics.
|
143
657
|
|
144
658
|
```ruby
|
145
|
-
|
146
|
-
|
659
|
+
create topic(:user_created, fifo: false)
|
660
|
+
create topic(:order_created, fifo: false)
|
661
|
+
create queue(publisher: :any, dest: :statistic, fifo: false)
|
662
|
+
|
663
|
+
subscribe topic: topic(:user_created, fifo: false),
|
664
|
+
endpoint: queue(publisher: :any, dest: :statistic, fifo: false)
|
665
|
+
|
666
|
+
subscribe topic: topic(:order_created, fifo: false),
|
667
|
+
endpoint: queue(publisher: :any, dest: :statistic, fifo: false)
|
147
668
|
```
|
669
|
+
![one2many](docs/_imgs/graphviz_02.png)
|
670
|
+
|
671
|
+
If queue receives messages from multiple sources you must specify publisher as `:any`. If the
|
672
|
+
subscriber receives messages with different types, `cyclone_lariat` uses a specific keyword - `all`.
|
673
|
+
|
674
|
+
### Example fanout-to-fanout
|
675
|
+
|
676
|
+
For better organisation you can subscribe topic on topic. For example, you have _management_panel_
|
677
|
+
and _client_panel_ services. Each of these services can register a user with predefined roles.
|
678
|
+
And you want to send this information to the _mailer_ and _statistics_ services.
|
679
|
+
|
680
|
+
```ruby
|
681
|
+
create topic(:client_created, fifo: false)
|
682
|
+
create topic(:manager_created, fifo: false)
|
683
|
+
create topic(:user_created, publisher: :any, fifo: false)
|
684
|
+
create queue(:user_created, publisher: :any, dest: :mailer, fifo: false)
|
685
|
+
create queue(:user_created, publisher: :any, dest: :stat, fifo: false)
|
686
|
+
|
687
|
+
subscribe topic: topic(:client_created, fifo: false),
|
688
|
+
endpoint: topic(:user_created, publisher: :any, fifo: false)
|
689
|
+
|
690
|
+
subscribe topic: topic(:manager_created, fifo: false),
|
691
|
+
endpoint: topic(:user_created, publisher: :any, fifo: false)
|
692
|
+
|
693
|
+
subscribe topic: topic(:user_created, publisher: :any, fifo: false),
|
694
|
+
endpoint: queue(:user_created, publisher: :any, dest: :mailer, fifo: false)
|
695
|
+
|
696
|
+
subscribe topic: topic(:user_created, publisher: :any, fifo: false),
|
697
|
+
endpoint: queue(:user_created, publisher: :any, dest: :stat, fifo: false)
|
698
|
+
```
|
699
|
+
|
700
|
+
![one2many](docs/_imgs/graphviz_03.png)
|
701
|
+
|
702
|
+
### Create and remove custom Topics and Queues
|
703
|
+
|
704
|
+
You can create Topic and Queues with custom names. That way recommended for:
|
705
|
+
- Remove old resources
|
706
|
+
- Receive messages from external sources
|
148
707
|
|
149
|
-
Or you can define topic directly:
|
150
708
|
```ruby
|
151
|
-
|
709
|
+
create custom_topic('custom_topic_name')
|
710
|
+
delete custom_queue('custom_topic_name')
|
152
711
|
```
|
153
712
|
|
713
|
+
### Where should the migration be?
|
714
|
+
|
715
|
+
We recommend locate migration on:
|
716
|
+
- **topic** - on Publisher side;
|
717
|
+
- **queue** - on Subscriber side;
|
718
|
+
- **subscription** - on Subscriber side.
|
719
|
+
|
720
|
+
## Console tasks
|
721
|
+
|
722
|
+
```bash
|
723
|
+
$ cyclone_lariat install - install cyclone_lariat
|
724
|
+
$ cyclone_lariat generate migration - generate new migration
|
725
|
+
|
726
|
+
$ rake cyclone_lariat:list:queues # List all queues
|
727
|
+
$ rake cyclone_lariat:list:subscriptions # List all subscriptions
|
728
|
+
$ rake cyclone_lariat:list:topics # List all topics
|
729
|
+
$ rake cyclone_lariat:migrate # Migrate topics for SQS/SNS
|
730
|
+
$ rake cyclone_lariat:rollback[version] # Rollback topics for SQS/SNS
|
731
|
+
$ rake cyclone_lariat:graph # Make graph
|
732
|
+
```
|
733
|
+
|
734
|
+
Graph generated in [grpahviz](https://graphviz.org/) format for the entry scheme. You should install
|
735
|
+
it on your system. For convert it in png use:
|
736
|
+
```bash
|
737
|
+
$ rake cyclone_lariat:list:subscriptions | dot -Tpng -o foo.png
|
738
|
+
```
|
739
|
+
|
740
|
+
## Subscriber
|
741
|
+
|
742
|
+
This is gem work like middleware for [shoryuken](https://github.com/ruby-shoryuken/shoryuken). It save all events to
|
743
|
+
database. And catch and produce all exceptions.
|
744
|
+
|
745
|
+
The logic of lariat as a subscriber. Imagine that you are working with an http server. And it gives you various response
|
746
|
+
codes. You have the following processing:
|
154
747
|
|
155
|
-
|
748
|
+
- 2xx - success, we process the page.
|
749
|
+
- 4хх - Logic error send the error to the developer and wait until he fixes it
|
750
|
+
- 5xx - Send an error and try again
|
751
|
+
|
752
|
+
|
753
|
+
![diagram](docs/_imgs/logic.png)
|
754
|
+
|
755
|
+
## Middleware
|
156
756
|
If you use middleware:
|
157
757
|
- Store all events to dataset
|
158
758
|
- Notify every input sqs message
|
159
|
-
- Notify every error
|
759
|
+
- Notify every error
|
160
760
|
|
161
761
|
```ruby
|
762
|
+
require 'sequel'
|
162
763
|
require 'cyclone_lariat/middleware' # If require: false in Gemfile
|
764
|
+
require 'luna_park/notifiers/log'
|
163
765
|
|
766
|
+
require_relative './config/initializers/cyclone_lariat'
|
767
|
+
|
768
|
+
Shoryuken::Logging.logger = Logger.new STDOUT
|
769
|
+
Shoryuken::Logging.logger.level = Logger::INFO
|
164
770
|
|
165
771
|
class Receiver
|
166
772
|
include Shoryuken::Worker
|
167
|
-
|
773
|
+
|
168
774
|
DB = Sequel.connect(host: 'localhost', user: 'ruby')
|
169
775
|
|
170
776
|
shoryuken_options auto_delete: true,
|
171
777
|
body_parser: ->(sqs_msg) {
|
172
778
|
JSON.parse(sqs_msg.body, symbolize_names: true)
|
173
779
|
},
|
174
|
-
queue:
|
780
|
+
queue: CycloneLariat.queue(:user_created, dest: :stat, fifo: true).name
|
175
781
|
|
176
782
|
server_middleware do |chain|
|
177
783
|
# Options dataset, errors_notifier and message_notifier is optionals.
|
178
784
|
# If you dont define notifiers - middleware does not notify
|
179
|
-
# If you dont define dataset - middleware does store events in db
|
785
|
+
# If you dont define dataset - middleware does not store events in db
|
180
786
|
chain.add CycloneLariat::Middleware,
|
181
787
|
dataset: DB[:events],
|
182
|
-
errors_notifier:
|
788
|
+
errors_notifier: LunaPark::Notifiers::Sentry.new,
|
183
789
|
message_notifier: LunaPark::Notifiers::Log.new(min_lvl: :debug, format: :pretty_json)
|
184
790
|
end
|
185
791
|
|
186
|
-
|
187
|
-
# Your logic here
|
188
|
-
end
|
189
|
-
end
|
190
|
-
```
|
191
|
-
|
192
|
-
## Migrations
|
193
|
-
Before use events storage add and apply this two migrations
|
194
|
-
|
195
|
-
```ruby
|
196
|
-
|
197
|
-
# First one
|
198
|
-
|
199
|
-
Sequel.migration do
|
200
|
-
up do
|
201
|
-
run <<-SQL
|
202
|
-
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
203
|
-
SQL
|
792
|
+
class UserIsNotRegistered < LunaPark::Errors::Business
|
204
793
|
end
|
205
794
|
|
206
|
-
|
207
|
-
|
208
|
-
DROP EXTENSION IF EXISTS "uuid-ossp";
|
209
|
-
SQL
|
210
|
-
end
|
211
|
-
end
|
795
|
+
def perform(sqs_message, sqs_message_body)
|
796
|
+
# Your logic here
|
212
797
|
|
213
|
-
#
|
214
|
-
|
215
|
-
change do
|
216
|
-
create_table :async_messages do
|
217
|
-
column :uuid, :uuid, primary_key: true
|
218
|
-
String :type, null: false
|
219
|
-
Integer :version, null: false
|
220
|
-
String :publisher, null: false
|
221
|
-
column :data, :json, null: false
|
222
|
-
String :client_error_message, null: true, default: nil
|
223
|
-
column :client_error_details, :json, null: true, default: nil
|
224
|
-
DateTime :sent_at, null: true, default: nil
|
225
|
-
DateTime :received_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
226
|
-
DateTime :processed_at, null: true, default: nil
|
227
|
-
end
|
798
|
+
# If you want to raise business error
|
799
|
+
raise UserIsNotRegistered.new(first_name: 'John', last_name: 'Doe')
|
228
800
|
end
|
229
801
|
end
|
230
802
|
```
|
231
803
|
|
232
|
-
|
804
|
+
## Rake tasks
|
233
805
|
|
234
|
-
For simplify write some Rake tasks you can use CycloneLariat::Repo
|
806
|
+
For simplify write some Rake tasks you can use `CycloneLariat::Repo::Messages`.
|
235
807
|
|
236
808
|
```ruby
|
237
809
|
# For retry all unprocessed
|
238
810
|
|
239
|
-
CycloneLariat.new
|
811
|
+
CycloneLariat::Repo::Messages.new.each_unprocessed do |event|
|
240
812
|
# Your logic here
|
241
813
|
end
|
242
814
|
|
243
815
|
# For retry all events with client errors
|
244
816
|
|
245
|
-
CycloneLariat.new
|
817
|
+
CycloneLariat::Repo::Messages.new.each_with_client_errors do |event|
|
246
818
|
# Your logic here
|
247
819
|
end
|
248
820
|
```
|