cyclone_lariat 0.3.10 → 1.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|

|
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
|
+

|
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
|
+

|
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
|
+

|
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
|
+

|
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
|
+

|
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
|
```
|