cyclone_lariat 1.0.0.rc5 → 1.0.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1e85d9e3115b5cba0ff233723389a20a5eef9f3c
4
- data.tar.gz: d87d6e81b0fba3f4aa78e432b7aab2bf993f5a82
3
+ metadata.gz: fab5b60b7bc9091b050be9f19f409dbb108b868c
4
+ data.tar.gz: 4c2458fe43a0353b5d2a8e1977858889220a9cb8
5
5
  SHA512:
6
- metadata.gz: 285ef03ab2d275f51401d547b3ce6d43c7dbfcc2e16cb53cbbe134dde906d043ec2eb8ea427286b413910a85e70b0b320a8a857ce2f03f7a021db6c47b72e7f4
7
- data.tar.gz: 8fed12ef46bc40ac995b80a377ab9702fc526d2b12ced8750b6e84ca5a44cfef49b14498d45c84ba421cf1a7f3ddaaa71088abdcd98e28a329057369af194b7c
6
+ metadata.gz: '090c611cb5e0bd49c4654042fbd285472b369448887789ff2392fd279dc68f12ebbb82f734254da9ae11750f38423749461f8a794c09370d85b06e11b097c73c'
7
+ data.tar.gz: 1b66103e5350f18335b70789246be6379f21c92ea6c1e92d181e3439e1039f96558ef2fd9c97eb6d95b77bcb21bff61f8c8880cfbd8a8c342a02a3620032f50c
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.0.0.rc6]
8
+ Changed
9
+ - Rename `messages_dataset` to `inbox_dataset`
10
+ Added
11
+ - `CycloneLariat::Outbox` - implementation of the transactional outbox pattern
12
+
7
13
  ## [1.0.0.rc5]
8
14
  Changed
9
15
  - Update Gemfile.lock
data/README.md CHANGED
@@ -67,7 +67,7 @@ Last install command will create 2 files:
67
67
  c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
68
68
  c.instance = ENV['INSTANCE'] # stage, production, test
69
69
  c.driver = :sequel # driver Sequel
70
- c.messages_dataset = DB[:async_messages] # Sequel dataset for store income messages (on receiver)
70
+ c.inbox_dataset = DB[:inbox_messages] # Sequel dataset for store incoming messages (on receiver)
71
71
  c.versions_dataset = DB[:lariat_versions] # Sequel dataset for versions of publisher migrations
72
72
  c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
73
73
  end
@@ -93,7 +93,7 @@ Last install command will create 2 files:
93
93
 
94
94
  Sequel.migration do
95
95
  change do
96
- create_table :async_messages do
96
+ create_table :inbox_messages do
97
97
  column :uuid, :uuid, primary_key: true
98
98
  String :type, null: false
99
99
  Integer :version, null: false
@@ -124,19 +124,19 @@ Last install command will create 2 files:
124
124
  # frozen_string_literal: true
125
125
 
126
126
  CycloneLariat.configure do |c|
127
- c.version = 1 # api version
127
+ c.version = 1 # api version
128
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
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
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
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.inbox_dataset = CycloneLariatInboxMessage # 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
140
  end
141
141
  ```
142
142
 
@@ -734,7 +734,7 @@ $ rake cyclone_lariat:graph # Make graph
734
734
  Graph generated in [grpahviz](https://graphviz.org/) format for the entry scheme. You should install
735
735
  it on your system. For convert it in png use:
736
736
  ```bash
737
- $ rake cyclone_lariat:list:subscriptions | dot -Tpng -o foo.png
737
+ $ rake cyclone_lariat:graph | dot -Tpng -o foo.png
738
738
  ```
739
739
 
740
740
  ## Subscriber
@@ -801,20 +801,91 @@ class Receiver
801
801
  end
802
802
  ```
803
803
 
804
+ ## Transactional outbox
805
+
806
+ This extension allows you to save messages to a database inside a transaction. It prevents messages from being lost when publishing fails. After the transaction is copmpleted, publishing will be perfromed and successfully published messages will be deleted from the database. For more information, see [Transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html)
807
+
808
+
809
+ ### Configuration
810
+
811
+ ```ruby
812
+ OutboxErrorLogger = LunaPark::Notifiers::Log.new
813
+ CycloneLariat::Outbox.configure do |config|
814
+ config.dataset = DB[:outbox_messages] # Outbox messages dataset. Sequel dataset or ActiveRecord model
815
+ config.on_sending_error = lambda do |event, error|
816
+ OutboxErrorLogger.error(error, details: event.to_h)
817
+ end
818
+ end
819
+
820
+ CycloneLariat::Outbox.load
821
+ ```
822
+
823
+ Before using the outbox, add and apply this migration:
824
+
825
+ ```ruby
826
+ # Sequel
827
+ DB.create_table :outbox_messages do
828
+ column :uuid, :uuid, primary_key: true
829
+ column :deduplication_id, String, null: true
830
+ column :group_id, String, null: true
831
+ column :serialized_message, :json, null: false
832
+ column :sending_error, String, null: true
833
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
834
+ end
835
+
836
+ # ActiveRecord
837
+ create_table(:outbox_messages, id: :uuid, primary_key: :uuid, default: -> { 'public.uuid_generate_v4()' }) do |t|
838
+ t.string :deduplication_id, null: true
839
+ t.string :group_id, null: true
840
+ t.string :sending_error, null: true
841
+ t.jsonb :serialized_message, null: false
842
+ t.datetime :created_at, null: false, default: -> { 'CURRENT_TIMESTAMP' }
843
+ end
844
+ ```
845
+
846
+ ### Usage example
847
+
848
+ ```ruby
849
+ # Sequel
850
+ DB.transaction(with_outbox: true) do |outbox|
851
+ some_action
852
+ outbox << CycloneLariat::Messages::V1::Event.new(...)
853
+ ...
854
+ end
855
+
856
+ # ActiveRecord
857
+ ActiveRecord::Base.transaction(with_outbox: true) do |outbox|
858
+ some_action
859
+ outbox << CycloneLariat::Messages::V1::Event.new(...)
860
+ ...
861
+ end
862
+ ```
863
+
864
+ ### Resending
865
+
866
+ To resend messages you can use the following service:
867
+
868
+ ```ruby
869
+ CycloneLariat::Outbox::Services::Resend.call
870
+ ```
871
+
872
+ This service tries to publish messages from the outbox table with `sending_error != nil`.
873
+ Successfully published messages will be removed.
874
+
804
875
  ## Rake tasks
805
876
 
806
- For simplify write some Rake tasks you can use `CycloneLariat::Repo::Messages`.
877
+ For simplify write some Rake tasks you can use `CycloneLariat::Repo::InboxMessages`.
807
878
 
808
879
  ```ruby
809
880
  # For retry all unprocessed
810
881
 
811
- CycloneLariat::Repo::Messages.new.each_unprocessed do |event|
882
+ CycloneLariat::Repo::InboxMessages.new.each_unprocessed do |event|
812
883
  # Your logic here
813
884
  end
814
885
 
815
886
  # For retry all events with client errors
816
887
 
817
- CycloneLariat::Repo::Messages.new.each_with_client_errors do |event|
888
+ CycloneLariat::Repo::InboxMessages.new.each_with_client_errors do |event|
818
889
  # Your logic here
819
890
  end
820
891
  ```
data/bin/cyclone_lariat CHANGED
@@ -70,7 +70,7 @@ module CycloneLariat
70
70
  c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
71
71
  c.instance = ENV['INSTANCE'] # stage, production, test
72
72
  c.driver = :sequel # :sequel or :active_record
73
- c.messages_dataset = DB[:messages] # Sequel dataset / ActiveRecord model for store income messages (on receiver)
73
+ c.inbox_dataset = DB[:inbox_messages] # Sequel dataset / ActiveRecord model for store income messages (on receiver)
74
74
  c.versions_dataset = DB[:lariat_versions] # Sequel dataset / ActiveRecord model for publisher migrations
75
75
  c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
76
76
  end
@@ -82,17 +82,17 @@ module CycloneLariat
82
82
  # frozen_string_literal: true
83
83
 
84
84
  CycloneLariat.configure do |c|
85
- c.version = 1 # messages version
86
- c.aws_key = ENV['AWS_KEY'] # aws key
87
- c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
88
- c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
89
- c.aws_region = ENV['AWS_REGION'] # aws default region
90
- c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
91
- c.instance = ENV['INSTANCE'] # stage, production, test
92
- c.driver = :active_record # :sequel or :active_record
93
- c.messages_dataset = CycloneLariatMessage # Sequel dataset / ActiveRecord model for store income messages (on receiver)
94
- c.versions_dataset = CycloneLariatVersion # Sequel dataset / ActiveRecord model for publisher migrations
95
- c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
85
+ c.version = 1 # messages version
86
+ c.aws_key = ENV['AWS_KEY'] # aws key
87
+ c.aws_account_id = ENV['AWS_ACCOUNT_ID'] # aws account id
88
+ c.aws_secret_key = ENV['AWS_SECRET_KEY'] # aws secret
89
+ c.aws_region = ENV['AWS_REGION'] # aws default region
90
+ c.publisher = ENV['APP_NAME'] # name of your publishers, usually name of your application
91
+ c.instance = ENV['INSTANCE'] # stage, production, test
92
+ c.driver = :active_record # :sequel or :active_record
93
+ c.inbox_dataset = CycloneLariatInboxMessage # Sequel dataset / ActiveRecord model for store incoming messages (on receiver)
94
+ c.versions_dataset = CycloneLariatVersion # Sequel dataset / ActiveRecord model for publisher migrations
95
+ c.fake_publish = ENV['INSTANCE'] == 'test' # when true, prevents messages from being published
96
96
  end
97
97
  CONFIG
98
98
  end
@@ -30,6 +30,7 @@ module CycloneLariat
30
30
 
31
31
  Messages::V1::Event.wrap(params.compact)
32
32
  end
33
+
33
34
  def event_v2(type, subject:, object:, data: {}, request_id: nil, group_id: nil, deduplication_id: nil, uuid: SecureRandom.uuid)
34
35
  params = {
35
36
  uuid: uuid,
@@ -15,7 +15,7 @@ module CycloneLariat
15
15
  attr :publisher, String, :new
16
16
  attr :type, String, :new
17
17
 
18
- attrs :client_error, :version, :data, :request_id, :sent_at,
18
+ attrs :client_error, :sending_error, :version, :data, :request_id, :sent_at,
19
19
  :deduplication_id, :group_id, :processed_at, :received_at
20
20
 
21
21
  # Make validation public
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cyclone_lariat/repo/messages'
3
+ require 'cyclone_lariat/repo/inbox_messages'
4
4
  require 'cyclone_lariat/core'
5
5
  require 'luna_park/errors'
6
6
  require 'cyclone_lariat/messages/builder'
@@ -10,7 +10,7 @@ module CycloneLariat
10
10
  class Middleware
11
11
  attr_reader :config
12
12
 
13
- def initialize(errors_notifier: nil, message_notifier: nil, repo: Repo::Messages, **options)
13
+ def initialize(errors_notifier: nil, message_notifier: nil, repo: Repo::InboxMessages, **options)
14
14
  @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
15
15
  @events_repo = repo.new(**@config.to_h)
16
16
  @message_notifier = message_notifier
@@ -6,7 +6,7 @@ module CycloneLariat
6
6
  class Options < LunaPark::Values::Compound
7
7
  attr_accessor :aws_key, :aws_secret_key, :publisher,
8
8
  :aws_region, :instance, :aws_account_id,
9
- :messages_dataset, :version, :versions_dataset,
9
+ :inbox_dataset, :version, :versions_dataset,
10
10
  :driver, :fake_publish
11
11
 
12
12
  # @param [CycloneLariat::Options, Hash] other
@@ -14,17 +14,17 @@ module CycloneLariat
14
14
  def merge!(other)
15
15
  other = self.class.wrap(other)
16
16
 
17
- self.aws_key ||= other.aws_key
18
- self.aws_secret_key ||= other.aws_secret_key
19
- self.publisher ||= other.publisher
20
- self.aws_region ||= other.aws_region
21
- self.instance ||= other.instance
22
- self.aws_account_id ||= other.aws_account_id
23
- self.messages_dataset ||= other.messages_dataset
24
- self.version ||= other.version
25
- self.versions_dataset ||= other.versions_dataset
26
- self.driver ||= other.driver
27
- self.fake_publish ||= other.fake_publish
17
+ self.aws_key ||= other.aws_key
18
+ self.aws_secret_key ||= other.aws_secret_key
19
+ self.publisher ||= other.publisher
20
+ self.aws_region ||= other.aws_region
21
+ self.instance ||= other.instance
22
+ self.aws_account_id ||= other.aws_account_id
23
+ self.inbox_dataset ||= other.inbox_dataset
24
+ self.version ||= other.version
25
+ self.versions_dataset ||= other.versions_dataset
26
+ self.driver ||= other.driver
27
+ self.fake_publish ||= other.fake_publish
28
28
 
29
29
  self
30
30
  end
@@ -41,7 +41,7 @@ module CycloneLariat
41
41
  aws_region: aws_region,
42
42
  instance: instance,
43
43
  aws_account_id: aws_account_id,
44
- messages_dataset: messages_dataset,
44
+ inbox_dataset: inbox_dataset,
45
45
  version: version,
46
46
  versions_dataset: versions_dataset,
47
47
  driver: driver,
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ class Outbox
5
+ module Configurable
6
+ CONFIG_ATTRS = %i[dataset on_sending_error].freeze
7
+
8
+ def config
9
+ @config ||= Struct.new(*CONFIG_ATTRS).new
10
+ end
11
+
12
+ def configure
13
+ yield(config) if block_given?
14
+ config
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ class Outbox
5
+ module Extensions
6
+ module ActiveRecordOutbox
7
+ def transaction(opts = {}, &block)
8
+ opts = opts.dup
9
+ return super unless opts.delete(:with_outbox)
10
+
11
+ outbox = CycloneLariat::Outbox.new
12
+ result = super(opts) do
13
+ block.call(outbox)
14
+ end
15
+
16
+ outbox.publish
17
+ result
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ class Outbox
5
+ module Extensions
6
+ module SequelOutbox
7
+ def transaction(opts = {}, &block)
8
+ opts = Sequel::OPTS.dup.merge(opts)
9
+ return super unless opts.delete(:with_outbox)
10
+
11
+ outbox = CycloneLariat::Outbox.new
12
+ result = super(opts) do |conn|
13
+ block.call(outbox, conn)
14
+ end
15
+
16
+ outbox.publish
17
+ result
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ class Outbox
5
+ module Loadable
6
+ def load
7
+ extend_driver_transaction
8
+ end
9
+
10
+ private
11
+
12
+ def extend_driver_transaction
13
+ case CycloneLariat.config.driver
14
+ when :sequel
15
+ Sequel::Database.prepend(Outbox::Extensions::SequelOutbox)
16
+ when :active_record
17
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(Outbox::Extensions::ActiveRecordOutbox)
18
+ else
19
+ raise ArgumentError, "Undefined driver `#{driver}`"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/repo/mappers/base'
4
+
5
+ module CycloneLariat
6
+ class Outbox
7
+ module Mappers
8
+ class Messages < CycloneLariat::Repo::Mappers::Base
9
+ class << self
10
+ def from_row(row)
11
+ return if row.nil?
12
+
13
+ attrs = hash_from_json_column(row[:serialized_message]).symbolize_keys
14
+ attrs[:uuid] = row[:uuid]
15
+ attrs[:deduplication_id] = row[:deduplication_id]
16
+ attrs[:group_id] = row[:group_id]
17
+ attrs[:sending_error] = row[:sending_error]
18
+
19
+ attrs
20
+ end
21
+
22
+ def to_row(input)
23
+ {}.tap do |row|
24
+ row[:uuid] = input.uuid if input.uuid
25
+ row[:deduplication_id] = input.deduplication_id
26
+ row[:group_id] = input.group_id
27
+ row[:serialized_message] = input.to_json
28
+ row[:sending_error] = input.sending_error
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/messages/v1/event'
4
+ require 'cyclone_lariat/messages/v1/command'
5
+ require 'cyclone_lariat/messages/builder'
6
+ require 'cyclone_lariat/plugins/outbox/mappers/messages'
7
+
8
+ module CycloneLariat
9
+ class Outbox
10
+ module Repo
11
+ module ActiveRecord
12
+ class Messages
13
+ LIMIT = 1000
14
+
15
+ attr_reader :dataset
16
+
17
+ def initialize(dataset)
18
+ @dataset = dataset
19
+ end
20
+
21
+ def create(msg)
22
+ dataset.create(Outbox::Mappers::Messages.to_row(msg)).uuid
23
+ end
24
+
25
+ def delete(uuid)
26
+ dataset.where(uuid: uuid).delete_all
27
+ end
28
+
29
+ def update_error(uuid, error_message)
30
+ dataset.where(uuid: uuid).update(sending_error: error_message)
31
+ end
32
+
33
+ def each_with_error
34
+ dataset
35
+ .where('sending_error IS NOT NULL')
36
+ .order(created_at: :asc)
37
+ .limit(LIMIT)
38
+ .each do |row|
39
+ msg = build_message_from_ar_row(row)
40
+ yield(msg)
41
+ end
42
+ end
43
+
44
+ def transaction(&block)
45
+ dataset.transaction(&block)
46
+ end
47
+
48
+ def lock(uuid)
49
+ dataset.lock('FOR UPDATE NOWAIT').where(uuid: uuid)
50
+ end
51
+
52
+ private
53
+
54
+ def build_message_from_ar_row(row)
55
+ build Outbox::Mappers::Messages.from_row(row.attributes.symbolize_keys)
56
+ end
57
+
58
+ def build(raw)
59
+ CycloneLariat::Messages::Builder.new(raw_message: raw).call
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'luna_park/extensions/injector'
5
+ require 'cyclone_lariat/plugins/outbox/repo/active_record/messages'
6
+ require 'cyclone_lariat/plugins/outbox/repo/sequel/messages'
7
+
8
+ module CycloneLariat
9
+ class Outbox
10
+ module Repo
11
+ class Messages
12
+ include LunaPark::Extensions::Injector
13
+
14
+ dependency(:sequel_messages_class) { Repo::Sequel::Messages }
15
+ dependency(:active_record_messages_class) { Repo::ActiveRecord::Messages }
16
+ dependency(:general_config) { CycloneLariat.config }
17
+
18
+ extend Forwardable
19
+
20
+ def_delegators :driver, :transaction, :lock, :update_error, :create, :delete, :each_with_error
21
+
22
+ def driver
23
+ @driver ||= select_driver
24
+ end
25
+
26
+ private
27
+
28
+ def select_driver
29
+ case general_config.driver
30
+ when :sequel then sequel_messages_class.new(config.dataset)
31
+ when :active_record then active_record_messages_class.new(config.dataset)
32
+ else raise ArgumentError, "Undefined driver `#{general_config.driver}`"
33
+ end
34
+ end
35
+
36
+ def config
37
+ @config ||= CycloneLariat::Outbox.config
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/messages/v1/event'
4
+ require 'cyclone_lariat/messages/v1/command'
5
+ require 'cyclone_lariat/messages/builder'
6
+ require 'cyclone_lariat/plugins/outbox/mappers/messages'
7
+
8
+ module CycloneLariat
9
+ class Outbox
10
+ module Repo
11
+ module Sequel
12
+ class Messages
13
+ LIMIT = 1000
14
+
15
+ attr_reader :dataset
16
+
17
+ def initialize(dataset)
18
+ @dataset = dataset
19
+ end
20
+
21
+ def create(msg)
22
+ dataset.returning.insert(Outbox::Mappers::Messages.to_row(msg)).first[:uuid]
23
+ end
24
+
25
+ def delete(uuid)
26
+ dataset.where(uuid: uuid).delete
27
+ end
28
+
29
+ def update_error(uuid, error_message)
30
+ dataset.where(uuid: uuid).update(sending_error: error_message)
31
+ end
32
+
33
+ def each_with_error
34
+ dataset
35
+ .where { sending_error !~ nil }
36
+ .order(::Sequel.asc(:created_at))
37
+ .limit(LIMIT)
38
+ .each do |row|
39
+ msg = build Outbox::Mappers::Messages.from_row(row)
40
+ yield(msg)
41
+ end
42
+ end
43
+
44
+ def transaction(&block)
45
+ dataset.db.transaction(&block)
46
+ end
47
+
48
+ def lock(uuid)
49
+ dataset.where(uuid: uuid).for_update.nowait
50
+ end
51
+
52
+ private
53
+
54
+ def build(raw)
55
+ CycloneLariat::Messages::Builder.new(raw_message: raw).call
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/extensions/callable'
4
+ require 'luna_park/extensions/injector'
5
+ require 'cyclone_lariat/clients/sns'
6
+ require 'cyclone_lariat/plugins/outbox/repo/messages'
7
+
8
+ module CycloneLariat
9
+ class Outbox
10
+ module Services
11
+ class Resend
12
+ extend LunaPark::Extensions::Callable
13
+ include LunaPark::Extensions::Injector
14
+
15
+ dependency(:messages_repo) { CycloneLariat::Outbox::Repo::Messages.new }
16
+ dependency(:sns_client) { CycloneLariat::Clients::Sns.new }
17
+ dependency(:on_sending_error) { CycloneLariat::Outbox.config.on_sending_error }
18
+
19
+ def call
20
+ messages_repo.each_with_error do |message|
21
+ messages_repo.transaction do
22
+ begin
23
+ messages_repo.lock(message.uuid)
24
+ sns_client.publish message, fifo: message.fifo?
25
+ messages_repo.delete(message.uuid)
26
+ rescue StandardError => e
27
+ messages_repo.update_error(message.uuid, e.message)
28
+ on_sending_error&.call(message, e)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/core'
4
+ require 'cyclone_lariat/clients/sns'
5
+ require 'cyclone_lariat/plugins/outbox/configurable'
6
+ require 'cyclone_lariat/plugins/outbox/loadable'
7
+ require 'cyclone_lariat/plugins/outbox/extensions/active_record_outbox'
8
+ require 'cyclone_lariat/plugins/outbox/extensions/sequel_outbox'
9
+ require 'cyclone_lariat/plugins/outbox/repo/messages'
10
+
11
+ module CycloneLariat
12
+ class Outbox
13
+ extend CycloneLariat::Outbox::Configurable
14
+ extend CycloneLariat::Outbox::Loadable
15
+ include LunaPark::Extensions::Injector
16
+
17
+ dependency(:sns_client) { CycloneLariat::Clients::Sns.new }
18
+ dependency(:repo) { CycloneLariat::Outbox::Repo::Messages.new }
19
+
20
+ attr_reader :messages
21
+
22
+ def initialize
23
+ @messages = []
24
+ end
25
+
26
+ def publish
27
+ sent_message_uids = messages.each_with_object([]) do |message, sent_message_uuids|
28
+ begin
29
+ sns_client.publish message, fifo: message.fifo?
30
+ sent_message_uuids << message.uuid
31
+ rescue StandardError => e
32
+ repo.update_error(message.uuid, e.message)
33
+ config.on_sending_error&.call(message, e)
34
+ next
35
+ end
36
+ end
37
+ repo.delete(sent_message_uids) unless sent_message_uids.empty?
38
+ end
39
+
40
+ def <<(message)
41
+ message.uuid = repo.create(message)
42
+ messages << message
43
+ end
44
+
45
+ def push(message)
46
+ self << message
47
+ end
48
+
49
+ private
50
+
51
+ def config
52
+ self.class.config
53
+ end
54
+ end
55
+ end
@@ -2,13 +2,13 @@
2
2
 
3
3
  require 'cyclone_lariat/messages/v1/event'
4
4
  require 'cyclone_lariat/messages/v1/command'
5
- require 'cyclone_lariat/repo/messages_mapper'
5
+ require 'cyclone_lariat/repo/mappers/inbox_messages'
6
6
  require 'cyclone_lariat/messages/builder'
7
7
 
8
8
  module CycloneLariat
9
9
  module Repo
10
10
  module ActiveRecord
11
- class Messages
11
+ class InboxMessages
12
12
  attr_reader :dataset
13
13
 
14
14
  def initialize(dataset)
@@ -24,7 +24,7 @@ module CycloneLariat
24
24
  end
25
25
 
26
26
  def create(msg)
27
- dataset.create(MessagesMapper.to_row(msg)).uuid
27
+ dataset.create(Mappers::InboxMessages.to_row(msg)).uuid
28
28
  end
29
29
 
30
30
  def exists?(uuid:)
@@ -68,7 +68,7 @@ module CycloneLariat
68
68
  private
69
69
 
70
70
  def build_message_from_ar_row(row)
71
- build MessagesMapper.from_row(row.attributes.symbolize_keys)
71
+ build Mappers::InboxMessages.from_row(row.attributes.symbolize_keys)
72
72
  end
73
73
 
74
74
  def current_timestamp_from_db
@@ -3,18 +3,18 @@
3
3
  require 'forwardable'
4
4
  require 'luna_park/extensions/injector'
5
5
  require 'cyclone_lariat/core'
6
- require 'cyclone_lariat/repo/sequel/messages'
7
- require 'cyclone_lariat/repo/active_record/messages'
6
+ require 'cyclone_lariat/repo/sequel/inbox_messages'
7
+ require 'cyclone_lariat/repo/active_record/inbox_messages'
8
8
 
9
9
  module CycloneLariat
10
10
  module Repo
11
- class Messages
11
+ class InboxMessages
12
12
  include LunaPark::Extensions::Injector
13
13
 
14
14
  attr_reader :config
15
15
 
16
- dependency(:sequel_messages_class) { Repo::Sequel::Messages }
17
- dependency(:active_record_messages_class) { Repo::ActiveRecord::Messages }
16
+ dependency(:sequel_messages_class) { Repo::Sequel::InboxMessages }
17
+ dependency(:active_record_messages_class) { Repo::ActiveRecord::InboxMessages }
18
18
 
19
19
  extend Forwardable
20
20
 
@@ -33,8 +33,8 @@ module CycloneLariat
33
33
 
34
34
  def select(driver:)
35
35
  case driver
36
- when :sequel then sequel_messages_class.new(config.messages_dataset)
37
- when :active_record then active_record_messages_class.new(config.messages_dataset)
36
+ when :sequel then sequel_messages_class.new(config.inbox_dataset)
37
+ when :active_record then active_record_messages_class.new(config.inbox_dataset)
38
38
  else raise ArgumentError, "Undefined driver `#{driver}`"
39
39
  end
40
40
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Repo
5
+ module Mappers
6
+ class Base
7
+ class << self
8
+ private
9
+
10
+ def hash_from_json_column(data)
11
+ return data if data.is_a?(Hash)
12
+ return JSON.parse(data) if data.is_a?(String)
13
+
14
+ if pg_json_extension_enabled?
15
+ return data.to_h if data.is_a?(::Sequel::Postgres::JSONHash)
16
+ return JSON.parse(data.to_s) if data.is_a?(::Sequel::Postgres::JSONString)
17
+ end
18
+
19
+ raise ArgumentError, "Unknown type of `#{data}`"
20
+ end
21
+
22
+ def pg_json_extension_enabled?
23
+ Object.const_defined?('Sequel::Postgres::JSONHash')
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/repo/mappers/base'
4
+
5
+ module CycloneLariat
6
+ module Repo
7
+ module Mappers
8
+ class InboxMessages < Base
9
+ class << self
10
+ def from_row(row)
11
+ return if row.nil?
12
+
13
+ row[:data] = hash_from_json_column(row[:data])
14
+ row[:client_error_details] = hash_from_json_column(row[:client_error_details]) if row[:client_error_details]
15
+ row
16
+ end
17
+
18
+ def to_row(input)
19
+ {
20
+ uuid: input.uuid,
21
+ kind: input.kind,
22
+ type: input.type,
23
+ publisher: input.publisher,
24
+ data: JSON.generate(input.data),
25
+ client_error_message: input.client_error&.message,
26
+ client_error_details: JSON.generate(input.client_error&.details),
27
+ version: input.version,
28
+ sent_at: input.sent_at
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,13 +2,13 @@
2
2
 
3
3
  require 'cyclone_lariat/messages/v1/event'
4
4
  require 'cyclone_lariat/messages/v1/command'
5
- require 'cyclone_lariat/repo/messages_mapper'
5
+ require 'cyclone_lariat/repo/mappers/inbox_messages'
6
6
  require 'cyclone_lariat/messages/builder'
7
7
 
8
8
  module CycloneLariat
9
9
  module Repo
10
10
  module Sequel
11
- class Messages
11
+ class InboxMessages
12
12
  attr_reader :dataset
13
13
 
14
14
  def initialize(dataset)
@@ -24,7 +24,7 @@ module CycloneLariat
24
24
  end
25
25
 
26
26
  def create(msg)
27
- dataset.insert MessagesMapper.to_row(msg)
27
+ dataset.insert Mappers::InboxMessages.to_row(msg)
28
28
  end
29
29
 
30
30
  def exists?(uuid:)
@@ -42,19 +42,19 @@ module CycloneLariat
42
42
  row = dataset.where(uuid: uuid).first
43
43
  return if row.nil?
44
44
 
45
- build MessagesMapper.from_row(row)
45
+ build Mappers::InboxMessages.from_row(row)
46
46
  end
47
47
 
48
48
  def each_unprocessed
49
49
  dataset.where(processed_at: nil).each do |row|
50
- msg = build MessagesMapper.from_row(row)
50
+ msg = build Mappers::InboxMessages.from_row(row)
51
51
  yield(msg)
52
52
  end
53
53
  end
54
54
 
55
55
  def each_with_client_errors
56
56
  dataset.where { (processed_at !~ nil) & (client_error_message !~ nil) }.each do |row|
57
- msg = build MessagesMapper.from_row(row)
57
+ msg = build Mappers::InboxMessages.from_row(row)
58
58
  yield(msg)
59
59
  end
60
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CycloneLariat
4
- VERSION = '1.0.0.rc5'
4
+ VERSION = '1.0.0.rc6'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cyclone_lariat
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc5
4
+ version: 1.0.0.rc6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Kudrin
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2023-03-30 00:00:00.000000000 Z
14
+ date: 2023-04-19 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: aws-sdk-sns
@@ -400,16 +400,27 @@ files:
400
400
  - lib/cyclone_lariat/middleware.rb
401
401
  - lib/cyclone_lariat/migration.rb
402
402
  - lib/cyclone_lariat/options.rb
403
+ - lib/cyclone_lariat/plugins/outbox.rb
404
+ - lib/cyclone_lariat/plugins/outbox/configurable.rb
405
+ - lib/cyclone_lariat/plugins/outbox/extensions/active_record_outbox.rb
406
+ - lib/cyclone_lariat/plugins/outbox/extensions/sequel_outbox.rb
407
+ - lib/cyclone_lariat/plugins/outbox/loadable.rb
408
+ - lib/cyclone_lariat/plugins/outbox/mappers/messages.rb
409
+ - lib/cyclone_lariat/plugins/outbox/repo/active_record/messages.rb
410
+ - lib/cyclone_lariat/plugins/outbox/repo/messages.rb
411
+ - lib/cyclone_lariat/plugins/outbox/repo/sequel/messages.rb
412
+ - lib/cyclone_lariat/plugins/outbox/services/resend.rb
403
413
  - lib/cyclone_lariat/presenters/graph.rb
404
414
  - lib/cyclone_lariat/presenters/queues.rb
405
415
  - lib/cyclone_lariat/presenters/subscriptions.rb
406
416
  - lib/cyclone_lariat/presenters/topics.rb
407
417
  - lib/cyclone_lariat/publisher.rb
408
- - lib/cyclone_lariat/repo/active_record/messages.rb
418
+ - lib/cyclone_lariat/repo/active_record/inbox_messages.rb
409
419
  - lib/cyclone_lariat/repo/active_record/versions.rb
410
- - lib/cyclone_lariat/repo/messages.rb
411
- - lib/cyclone_lariat/repo/messages_mapper.rb
412
- - lib/cyclone_lariat/repo/sequel/messages.rb
420
+ - lib/cyclone_lariat/repo/inbox_messages.rb
421
+ - lib/cyclone_lariat/repo/mappers/base.rb
422
+ - lib/cyclone_lariat/repo/mappers/inbox_messages.rb
423
+ - lib/cyclone_lariat/repo/sequel/inbox_messages.rb
413
424
  - lib/cyclone_lariat/repo/sequel/versions.rb
414
425
  - lib/cyclone_lariat/repo/versions.rb
415
426
  - lib/cyclone_lariat/resources/queue.rb
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CycloneLariat
4
- module Repo
5
- class MessagesMapper
6
- class << self
7
- def from_row(row)
8
- return if row.nil?
9
-
10
- row[:data] = hash_from_json_column(row[:data])
11
- row[:client_error_details] = hash_from_json_column(row[:client_error_details]) if row[:client_error_details]
12
- row
13
- end
14
-
15
- def to_row(input)
16
- {
17
- uuid: input.uuid,
18
- kind: input.kind,
19
- type: input.type,
20
- publisher: input.publisher,
21
- data: JSON.generate(input.data),
22
- client_error_message: input.client_error&.message,
23
- client_error_details: JSON.generate(input.client_error&.details),
24
- version: input.version,
25
- sent_at: input.sent_at
26
- }
27
- end
28
-
29
- private
30
-
31
- def hash_from_json_column(data)
32
- return data if data.is_a?(Hash)
33
- return JSON.parse(data) if data.is_a?(String)
34
-
35
- if pg_json_extension_enabled?
36
- return data.to_h if data.is_a?(::Sequel::Postgres::JSONHash)
37
- return JSON.parse(data.to_s) if data.is_a?(::Sequel::Postgres::JSONString)
38
- end
39
-
40
- raise ArgumentError, "Unknown type of `#{data}`"
41
- end
42
-
43
- def pg_json_extension_enabled?
44
- Object.const_defined?('Sequel::Postgres::JSONHash')
45
- end
46
- end
47
- end
48
- end
49
- end