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
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'forwardable'
5
+ require 'cyclone_lariat/clients/sns'
6
+ require 'cyclone_lariat/clients/sqs'
7
+ require 'cyclone_lariat/repo/versions'
8
+ require 'cyclone_lariat/services/migrate'
9
+ require 'cyclone_lariat/services/rollback'
10
+ require 'cyclone_lariat/presenters/topics'
11
+ require 'cyclone_lariat/presenters/queues'
12
+ require 'cyclone_lariat/presenters/subscriptions'
13
+ require 'cyclone_lariat/presenters/graph'
14
+ require 'luna_park/errors'
15
+
16
+ module CycloneLariat
17
+ class Migration
18
+ extend Forwardable
19
+ include LunaPark::Extensions::Injector
20
+
21
+ dependency(:sns) { CycloneLariat::Clients::Sns.new }
22
+ dependency(:sqs) { CycloneLariat::Clients::Sqs.new }
23
+
24
+ DIR = './lariat/migrate'
25
+
26
+ def up
27
+ raise LunaPark::Errors::Abstract, "Up method should be defined in #{self.class.name}"
28
+ end
29
+
30
+ def down
31
+ raise LunaPark::Errors::Abstract, "Down method should be defined in #{self.class.name}"
32
+ end
33
+
34
+ def_delegators :sqs, :queue, :custom_queue
35
+ def_delegators :sns, :topic, :custom_topic
36
+
37
+ def create(resource)
38
+ process(
39
+ resource: resource,
40
+ for_topic: ->(topic) { sns.create(topic) },
41
+ for_queue: ->(queue) { sqs.create(queue) }
42
+ )
43
+
44
+ puts " #{resource.class.name.split('::').last} was created `#{resource.name}`"
45
+ end
46
+
47
+ def delete(resource)
48
+ process(
49
+ resource: resource,
50
+ for_topic: ->(topic) { sns.delete(topic) },
51
+ for_queue: ->(queue) { sqs.delete(queue) }
52
+ )
53
+ puts " #{resource.class.name.split('::').last} was deleted `#{resource.name}`"
54
+ end
55
+
56
+ def exists?(resource)
57
+ process(
58
+ resource: resource,
59
+ for_topic: ->(topic) { sns.exists?(topic) },
60
+ for_queue: ->(queue) { sqs.exists?(queue) }
61
+ )
62
+ end
63
+
64
+ def subscribe(topic:, endpoint:, policy: nil)
65
+ policy ||= default_policy(endpoint)
66
+ sqs.add_policy(queue: endpoint, policy: policy) if endpoint.queue?
67
+ sns.subscribe topic: topic, endpoint: endpoint
68
+ puts " Subscription was created `#{topic.name} -> #{endpoint.name}`"
69
+ end
70
+
71
+ def unsubscribe(topic:, endpoint:)
72
+ sns.unsubscribe topic: topic, endpoint: endpoint
73
+ puts " Subscription was deleted `#{topic.name} -> #{endpoint.name}`"
74
+ end
75
+
76
+ def default_policy(queue)
77
+ {
78
+ 'Sid' => queue.arn,
79
+ 'Effect' => 'Allow',
80
+ 'Principal' => {
81
+ 'AWS' => '*'
82
+ },
83
+ 'Action' => 'SQS:*',
84
+ 'Resource' => queue.arn,
85
+ 'Condition' => {
86
+ 'ArnEquals' => {
87
+ 'aws:SourceArn' => fanout_arn_pattern
88
+ }
89
+ }
90
+ }
91
+ end
92
+
93
+ def topics
94
+ sns.list_all
95
+ end
96
+
97
+ def queues
98
+ sqs.list_all
99
+ end
100
+
101
+ def subscriptions
102
+ sns.list_subscriptions
103
+ end
104
+
105
+ private
106
+
107
+ def process(resource:, for_topic:, for_queue:)
108
+ case resource
109
+ when Resources::Topic then for_topic.call(resource)
110
+ when Resources::Queue then for_queue.call(resource)
111
+ else
112
+ raise ArgumentError, "Unknown resource class #{resource.class}"
113
+ end
114
+ end
115
+
116
+ def fanout_arn_pattern
117
+ @fanout_arn_pattern ||= [
118
+ 'arn:aws:sns',
119
+ CycloneLariat.config.aws_region,
120
+ CycloneLariat.config.aws_account_id,
121
+ "#{CycloneLariat.config.instance}-*-fanout-*"
122
+ ].join(':')
123
+ end
124
+
125
+ class << self
126
+ def migrate(repo: CycloneLariat::Repo::Versions.new, dir: DIR, service: Services::Migrate)
127
+ puts service.new(repo: repo, dir: dir).call
128
+ end
129
+
130
+ def rollback(version = nil, repo: CycloneLariat::Repo::Versions.new, dir: DIR, service: Services::Rollback)
131
+ puts service.new(repo: repo, dir: dir).call(version)
132
+ end
133
+
134
+ def list_topics(presenter: Presenters::Topics)
135
+ puts presenter.call(new.topics)
136
+ end
137
+
138
+ def list_queues(presenter: Presenters::Queues)
139
+ puts presenter.call(new.queues)
140
+ end
141
+
142
+ def list_subscriptions(presenter: Presenters::Subscriptions)
143
+ puts presenter.call(new.subscriptions)
144
+ end
145
+
146
+ def build_graph(presenter: Presenters::Graph)
147
+ puts presenter.call(new.subscriptions)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'luna_park/values/compound'
4
+
5
+ module CycloneLariat
6
+ class Options < LunaPark::Values::Compound
7
+ attr_accessor :aws_key, :aws_secret_key, :publisher,
8
+ :aws_region, :instance, :aws_account_id,
9
+ :messages_dataset, :version, :versions_dataset,
10
+ :driver, :fake_publish
11
+
12
+ # @param [CycloneLariat::Options, Hash] other
13
+ # @return [CycloneLariat::Options]
14
+ def merge!(other)
15
+ other = self.class.wrap(other)
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
28
+
29
+ self
30
+ end
31
+
32
+ def merge(other)
33
+ dup.merge!(other)
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ aws_key: aws_key,
39
+ aws_secret_key: aws_secret_key,
40
+ publisher: publisher,
41
+ aws_region: aws_region,
42
+ instance: instance,
43
+ aws_account_id: aws_account_id,
44
+ messages_dataset: messages_dataset,
45
+ version: version,
46
+ versions_dataset: versions_dataset,
47
+ driver: driver,
48
+ fake_publish: fake_publish
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Graph
8
+ HEADS = %w[topic endpoint subscription_arn].freeze
9
+ def self.call(subscriptions)
10
+ new.call(subscriptions)
11
+ end
12
+
13
+ def call(subscriptions)
14
+ return '' if subscriptions.empty?
15
+
16
+ resources_set = Set.new
17
+
18
+ subscriptions.each do |subscription|
19
+ resources_set << subscription[:topic]
20
+ resources_set << subscription[:endpoint]
21
+ end
22
+
23
+ [].tap do |output|
24
+ output << open_graph
25
+
26
+ resources_set.each { |resource| output << present_resource(resource) }
27
+ subscriptions.each { |subscription| output << present_subscription(subscription) }
28
+
29
+ output << close_graph
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def present_resource(resource)
36
+ color = resource.custom? ? ', fillcolor=grey' : ', fillcolor=white'
37
+ style = resource.topic? ? "[shape=component style=filled#{color}]" : "[shape=record, style=\"rounded,filled\"#{color}]"
38
+ " \"#{resource.name}\" #{style};"
39
+ end
40
+
41
+ def present_subscription(subscription)
42
+ " \"#{subscription[:topic].name}\" -> \"#{subscription[:endpoint].name}\";"
43
+ end
44
+
45
+ def open_graph
46
+ "digraph G {\n rankdir=LR;"
47
+ end
48
+
49
+ def close_graph
50
+ '}'
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Queues
8
+ HEADS = %w[valid region account_id name instance kind publisher type destination fifo].freeze
9
+
10
+ def self.call(queues)
11
+ new.call(queues)
12
+ end
13
+
14
+ def call(queues)
15
+ rows = []
16
+ queues.each do |queue|
17
+ rows << row(queue)
18
+ end
19
+
20
+ Terminal::Table.new rows: rows, headings: HEADS
21
+ end
22
+
23
+ private
24
+
25
+ def row(queue)
26
+ [
27
+ queue.custom? ? 'custom' : 'standard',
28
+ queue.region,
29
+ queue.account_id,
30
+ queue.name,
31
+ queue.instance,
32
+ queue.kind,
33
+ queue.publisher,
34
+ queue.type,
35
+ queue.dest,
36
+ queue.fifo
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Subscriptions
8
+ HEADS = %w[topic endpoint subscription_arn].freeze
9
+
10
+ def self.call(subscriptions)
11
+ new.call(subscriptions)
12
+ end
13
+
14
+ def call(subscriptions)
15
+ rows = []
16
+ subscriptions.each do |subscription|
17
+ rows << row(subscription)
18
+ end
19
+
20
+ Terminal::Table.new rows: rows, headings: HEADS
21
+ end
22
+
23
+ private
24
+
25
+ def row(subscription)
26
+ [
27
+ subscription[:topic].name,
28
+ subscription[:endpoint].name,
29
+ subscription[:arn]
30
+ ]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+
5
+ module CycloneLariat
6
+ module Presenters
7
+ class Topics
8
+ HEADS = %w[valid region account_id name instance kind publisher type fifo].freeze
9
+
10
+ def self.call(topics)
11
+ new.call(topics)
12
+ end
13
+
14
+ def call(topics)
15
+ rows = []
16
+ topics.each do |topic|
17
+ rows << row(topic)
18
+ end
19
+
20
+ Terminal::Table.new rows: rows, headings: HEADS
21
+ end
22
+
23
+ private
24
+
25
+ def row(topic)
26
+ [
27
+ topic.custom? ? 'custom' : 'standard',
28
+ topic.region,
29
+ topic.account_id,
30
+ topic.name,
31
+ topic.instance,
32
+ topic.kind,
33
+ topic.publisher,
34
+ topic.type,
35
+ topic.fifo
36
+ ]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/clients/sns'
4
+ require 'cyclone_lariat/clients/sqs'
5
+
6
+ module CycloneLariat
7
+ class Publisher
8
+ include Generators::Event
9
+ include Generators::Command
10
+
11
+ attr_reader :config
12
+
13
+ def initialize(**options)
14
+ @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
15
+ end
16
+
17
+ def sqs
18
+ @sqs ||= Clients::Sqs.new(**config.to_h)
19
+ end
20
+
21
+ def sns
22
+ @sns ||= Clients::Sns.new(**config.to_h)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/messages/v1/event'
4
+ require 'cyclone_lariat/messages/v1/command'
5
+ require 'cyclone_lariat/repo/messages_mapper'
6
+
7
+ module CycloneLariat
8
+ module Repo
9
+ module ActiveRecord
10
+ class Messages
11
+ attr_reader :dataset
12
+
13
+ def initialize(dataset)
14
+ @dataset = dataset
15
+ end
16
+
17
+ def enabled?
18
+ !dataset.nil?
19
+ end
20
+
21
+ def disabled?
22
+ dataset.nil?
23
+ end
24
+
25
+ def create(msg)
26
+ dataset.create(MessagesMapper.to_row(msg)).uuid
27
+ end
28
+
29
+ def exists?(uuid:)
30
+ dataset.exists?(uuid: uuid)
31
+ end
32
+
33
+ def processed!(uuid:, error: nil)
34
+ data = { processed_at: current_timestamp_from_db }
35
+ data.merge!(client_error_message: error.message, client_error_details: JSON.generate(error.details)) if error
36
+
37
+ message = dataset.where(uuid: uuid).first
38
+ return false unless message
39
+
40
+ message.update(data)
41
+ end
42
+
43
+ def find(uuid:)
44
+ row = dataset.where(uuid: uuid).first
45
+ return if row.nil?
46
+
47
+ build_message_from_ar_row(row)
48
+ end
49
+
50
+ def each_unprocessed
51
+ dataset.where(processed_at: nil).each do |row|
52
+ msg = build_message_from_ar_row(row)
53
+ yield(msg)
54
+ end
55
+ end
56
+
57
+ def each_with_client_errors
58
+ dataset
59
+ .where.not(processed_at: nil)
60
+ .where.not(client_error_message: nil)
61
+ .each do |row|
62
+ msg = build_message_from_ar_row(row)
63
+ yield(msg)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def build_message_from_ar_row(row)
70
+ build MessagesMapper.from_row(row.attributes.symbolize_keys)
71
+ end
72
+
73
+ def current_timestamp_from_db
74
+ time_from_db =
75
+ ::ActiveRecord::Base
76
+ .connection.execute('select current_timestamp;')
77
+ .first
78
+ time = time_from_db.is_a?(Hash) ? time_from_db['current_timestamp'] : time_from_db[0]
79
+ time.is_a?(Time) ? time : Time.parse(time)
80
+ end
81
+
82
+ def build(raw)
83
+ case kind = raw.delete(:kind)
84
+ when 'event' then CycloneLariat::Messages::V1::Event.wrap raw
85
+ when 'command' then CycloneLariat::Messages::V1::Command.wrap raw
86
+ else raise ArgumentError, "Unknown kind `#{kind}` of message"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CycloneLariat
4
+ module Repo
5
+ module ActiveRecord
6
+ class Versions
7
+ attr_reader :dataset
8
+
9
+ def initialize(dataset)
10
+ @dataset = dataset
11
+ end
12
+
13
+ def add(version)
14
+ dataset.create(version: version)
15
+ true
16
+ end
17
+
18
+ def remove(version)
19
+ dataset.where(version: version).delete_all.positive?
20
+ end
21
+
22
+ def all
23
+ dataset.pluck(:version).map { |version| { version: version } }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'luna_park/extensions/injector'
5
+ require 'cyclone_lariat/core'
6
+ require 'cyclone_lariat/repo/sequel/messages'
7
+ require 'cyclone_lariat/repo/active_record/messages'
8
+
9
+ module CycloneLariat
10
+ module Repo
11
+ class Messages
12
+ include LunaPark::Extensions::Injector
13
+
14
+ attr_reader :config
15
+
16
+ dependency(:sequel_messages_class) { Repo::Sequel::Messages }
17
+ dependency(:active_record_messages_class) { Repo::ActiveRecord::Messages }
18
+
19
+ extend Forwardable
20
+
21
+ def_delegators :driver, :create, :exists?, :processed!, :find, :each_unprocessed, :each_with_client_errors,
22
+ :enabled?, :disabled?
23
+
24
+ def initialize(**options)
25
+ @config = CycloneLariat::Options.wrap(options).merge!(CycloneLariat.config)
26
+ end
27
+
28
+ def driver
29
+ @driver ||= select(driver: config.driver)
30
+ end
31
+
32
+ private
33
+
34
+ def select(driver:)
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)
38
+ else raise ArgumentError, "Undefined driver `#{driver}`"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
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
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cyclone_lariat/messages/v1/event'
4
+ require 'cyclone_lariat/messages/v1/command'
5
+ require 'cyclone_lariat/repo/messages_mapper'
6
+
7
+ module CycloneLariat
8
+ module Repo
9
+ module Sequel
10
+ class Messages
11
+ attr_reader :dataset
12
+
13
+ def initialize(dataset)
14
+ @dataset = dataset
15
+ end
16
+
17
+ def enabled?
18
+ !dataset.nil?
19
+ end
20
+
21
+ def disabled?
22
+ dataset.nil?
23
+ end
24
+
25
+ def create(msg)
26
+ dataset.insert MessagesMapper.to_row(msg)
27
+ end
28
+
29
+ def exists?(uuid:)
30
+ dataset.where(uuid: uuid).limit(1).any?
31
+ end
32
+
33
+ def processed!(uuid:, error: nil)
34
+ data = { processed_at: ::Sequel.function(:NOW) }
35
+ data.merge!(client_error_message: error.message, client_error_details: JSON.generate(error.details)) if error
36
+
37
+ !dataset.where(uuid: uuid).update(data).zero?
38
+ end
39
+
40
+ def find(uuid:)
41
+ row = dataset.where(uuid: uuid).first
42
+ return if row.nil?
43
+
44
+ build MessagesMapper.from_row(row)
45
+ end
46
+
47
+ def each_unprocessed
48
+ dataset.where(processed_at: nil).each do |row|
49
+ msg = build MessagesMapper.from_row(row)
50
+ yield(msg)
51
+ end
52
+ end
53
+
54
+ def each_with_client_errors
55
+ dataset.where { (processed_at !~ nil) & (client_error_message !~ nil) }.each do |row|
56
+ msg = build MessagesMapper.from_row(row)
57
+ yield(msg)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def build(raw)
64
+ case kind = raw.delete(:kind)
65
+ when 'event' then CycloneLariat::Messages::V1::Event.wrap raw
66
+ when 'command' then CycloneLariat::Messages::V1::Command.wrap raw
67
+ else raise ArgumentError, "Unknown kind `#{kind}` of message"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end