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