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.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +4 -4
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +30 -1
  5. data/CHANGELOG.md +11 -1
  6. data/Gemfile.lock +137 -30
  7. data/Guardfile +42 -0
  8. data/README.md +715 -143
  9. data/Rakefile +2 -5
  10. data/bin/cyclone_lariat +206 -0
  11. data/cyclone_lariat.gemspec +13 -2
  12. data/lib/cyclone_lariat/clients/abstract.rb +40 -0
  13. data/lib/cyclone_lariat/clients/sns.rb +163 -0
  14. data/lib/cyclone_lariat/clients/sqs.rb +114 -0
  15. data/lib/cyclone_lariat/core.rb +21 -0
  16. data/lib/cyclone_lariat/errors.rb +38 -0
  17. data/lib/cyclone_lariat/fake.rb +19 -0
  18. data/lib/cyclone_lariat/generators/command.rb +53 -0
  19. data/lib/cyclone_lariat/generators/event.rb +52 -0
  20. data/lib/cyclone_lariat/generators/queue.rb +30 -0
  21. data/lib/cyclone_lariat/generators/topic.rb +29 -0
  22. data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
  23. data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
  24. data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
  25. data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
  26. data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
  27. data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
  28. data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
  29. data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
  30. data/lib/cyclone_lariat/middleware.rb +9 -5
  31. data/lib/cyclone_lariat/migration.rb +151 -0
  32. data/lib/cyclone_lariat/options.rb +52 -0
  33. data/lib/cyclone_lariat/presenters/graph.rb +54 -0
  34. data/lib/cyclone_lariat/presenters/queues.rb +41 -0
  35. data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
  36. data/lib/cyclone_lariat/presenters/topics.rb +40 -0
  37. data/lib/cyclone_lariat/publisher.rb +25 -0
  38. data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
  39. data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
  40. data/lib/cyclone_lariat/repo/messages.rb +43 -0
  41. data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
  42. data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
  43. data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
  44. data/lib/cyclone_lariat/repo/versions.rb +42 -0
  45. data/lib/cyclone_lariat/resources/queue.rb +167 -0
  46. data/lib/cyclone_lariat/resources/topic.rb +132 -0
  47. data/lib/cyclone_lariat/services/migrate.rb +51 -0
  48. data/lib/cyclone_lariat/services/rollback.rb +51 -0
  49. data/lib/cyclone_lariat/version.rb +1 -1
  50. data/lib/cyclone_lariat.rb +4 -10
  51. data/lib/tasks/console.rake +13 -0
  52. data/lib/tasks/cyclone_lariat.rake +42 -0
  53. data/lib/tasks/db.rake +0 -15
  54. metadata +161 -20
  55. data/config/db.example.rb +0 -9
  56. data/db/migrate/01_add_uuid_extensions.rb +0 -15
  57. data/db/migrate/02_add_events.rb +0 -19
  58. data/docs/_imgs/diagram.png +0 -0
  59. data/docs/_imgs/lariat.jpg +0 -0
  60. data/lib/cyclone_lariat/abstract/client.rb +0 -106
  61. data/lib/cyclone_lariat/abstract/message.rb +0 -83
  62. data/lib/cyclone_lariat/command.rb +0 -13
  63. data/lib/cyclone_lariat/configure.rb +0 -15
  64. data/lib/cyclone_lariat/event.rb +0 -13
  65. data/lib/cyclone_lariat/messages_mapper.rb +0 -46
  66. data/lib/cyclone_lariat/messages_repo.rb +0 -60
  67. data/lib/cyclone_lariat/sns_client.rb +0 -38
  68. 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 is gem work like middleware for [shoryuken](https://github.com/ruby-shoryuken/shoryuken). It save all events to database. And catch and produce all exceptions.
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
- # Gemfile
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
- # If use client or middleware
12
- gem 'cyclone_lariat', require: false
353
+ # Init repo
354
+ publisher = Publisher.new
13
355
 
14
- # If use client
15
- gem 'cyclone_lariat'
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
- ## Logic
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
- ![diagram](docs/_imgs/diagram.png)
371
+ # Queue example
372
+ arn:aws:sqs:eu-west-1:247602342345:test-event-queue-cyclone_lariat-note_added-notifier.fifo
373
+ ```
21
374
 
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.
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
- 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.
384
+ Region and account_id usually set using the **cyclone_lariat** [configuration](#Configuration).
31
385
 
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.
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
- # 'config/initializers/cyclone_lariat.rb'
40
- CycloneLariat.tap do |cl|
41
- cl.default_version = 1 # api version default is 1
42
- cl.aws_key = # aws key
43
- cl.aws_secret_key = # aws secret
44
- cl.aws_client_id = # aws client id
45
- cl.aws_default_region = # aws default region
46
- cl.publisher = 'auth' # name of your publishers, usually name of your application
47
- cl.default_instance = APP_INSTANCE # stage, production, test
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
- ## SnsClient
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
- require 'cyclone_lariat/sns_client' # If require: false in Gemfile
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
- client = CycloneLariat::SnsClient.new(
58
- key: APP_CONF.aws.key,
59
- secret_key: APP_CONF.aws.secret_key,
60
- region: APP_CONF.aws.region,
61
- version: 1, # at default 1
62
- publisher: 'pilot',
63
- instance: INSTANCE # at default :prod
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
- # event_type data topic
70
- client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' } # prod-event-fanout-pilot-email_is_created
71
- client.publish_event 'email_is_removed', data: { mail: 'john.doe@example.com' } # prod-event-fanout-pilot-email_is_removed
72
- client.publish_command 'delete_user', data: { mail: 'john.doe@example.com' } # prod-command-fanout-pilot-delete_user
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
- Or you can define it by handle. For example, if you want to send different events to same channel.
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
- # event_type data topic
77
- client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, topic: 'prod-event-fanout-pilot-emails'
78
- client.publish_event 'email_is_removed', data: { mail: 'john.doe@example.com' }, topic: 'prod-event-fanout-pilot-emails'
79
- client.publish_command 'delete_user', data: { mail: 'john.doe@example.com' }, topic: 'prod-command-fanout-pilot-emails'
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
- Or you can use client as Repo.
547
+ This command should create a migration file, let's edit it.
83
548
 
84
549
  ```ruby
85
- require 'cyclone_lariat/sns_client' # If require: false in Gemfile
550
+ # ./lariat/migrate/1668097991_user_created_queue.rb
86
551
 
87
- class YourClient < CycloneLariat::SnsClient
88
- version 1
89
- publisher 'pilot'
90
- instance 'stage'
552
+ # frozen_string_literal: true
91
553
 
92
- def email_is_created(mail)
93
- publish event('email_is_created',
94
- data: { mail: mail }
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 email_is_removed(mail)
100
- publish event('email_is_removed',
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
- def delete_user(mail)
108
- publish command('delete_user',
109
- data: { mail: mail }
110
- ),
111
- to: APP_CONF.aws.fanout.email
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
- # Init repo
116
- client = YourClient.new(key: APP_CONF.aws.key, secret_key: APP_CONF.aws.secret_key, region: APP_CONF.aws.region)
600
+ # GOOD:
601
+ class UserCreatedQueue < CycloneLariat::Migration
602
+ def up
603
+ create queue(:user_created, dest: :mailer, fifo: true)
604
+ end
117
605
 
118
- # And send topics
119
- client.email_is_created 'john.doe@example.com'
120
- client.email_is_removed 'john.doe@example.com'
121
- client.delete_user 'john.doe@example.com'
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
- # SqsClient
126
- SqsClient is really similar to SnsClient. It can be initialized in same way:
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
- require 'cyclone_lariat/sns_client' # If require: false in Gemfile
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
- client = CycloneLariat::SqsClient.new(
132
- key: APP_CONF.aws.key,
133
- secret_key: APP_CONF.aws.secret_key,
134
- region: APP_CONF.aws.region,
135
- version: 1, # at default 1
136
- publisher: 'pilot',
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
- As you see all params identity. And you can easily change your sqs-queue to sns-topic when you start work with more
142
- subscribes. But you should define destination.
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
- client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, dest: 'notify_service'
146
- # prod-event-queue-pilot-email_is_created-notify_service
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
- client.publish_event 'email_is_created', data: { mail: 'john.doe@example.com' }, topic: 'prod-event-fanout-pilot-emails'
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
- # Middleware
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: 'your_sqs_queue_name'
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: LunaPark::Notifiers::Sentry.new,
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
- def perform(sqs_message, sqs_message_body)
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
- down do
207
- run <<-SQL
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
- # The second one:
214
- Sequel.migration do
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
- ### Rake tasks
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(DB[:events]).each_unprocessed do |event|
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(DB[:events]).each_with_client_errors do |event|
817
+ CycloneLariat::Repo::Messages.new.each_with_client_errors do |event|
246
818
  # Your logic here
247
819
  end
248
820
  ```