cyclone_lariat 0.3.10 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-push.yml +4 -4
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +30 -1
  5. data/CHANGELOG.md +11 -1
  6. data/Gemfile.lock +137 -30
  7. data/Guardfile +42 -0
  8. data/README.md +715 -143
  9. data/Rakefile +2 -5
  10. data/bin/cyclone_lariat +206 -0
  11. data/cyclone_lariat.gemspec +13 -2
  12. data/lib/cyclone_lariat/clients/abstract.rb +40 -0
  13. data/lib/cyclone_lariat/clients/sns.rb +163 -0
  14. data/lib/cyclone_lariat/clients/sqs.rb +114 -0
  15. data/lib/cyclone_lariat/core.rb +21 -0
  16. data/lib/cyclone_lariat/errors.rb +38 -0
  17. data/lib/cyclone_lariat/fake.rb +19 -0
  18. data/lib/cyclone_lariat/generators/command.rb +53 -0
  19. data/lib/cyclone_lariat/generators/event.rb +52 -0
  20. data/lib/cyclone_lariat/generators/queue.rb +30 -0
  21. data/lib/cyclone_lariat/generators/topic.rb +29 -0
  22. data/lib/cyclone_lariat/messages/v1/abstract.rb +139 -0
  23. data/lib/cyclone_lariat/messages/v1/command.rb +20 -0
  24. data/lib/cyclone_lariat/messages/v1/event.rb +20 -0
  25. data/lib/cyclone_lariat/messages/v1/validator.rb +31 -0
  26. data/lib/cyclone_lariat/messages/v2/abstract.rb +149 -0
  27. data/lib/cyclone_lariat/messages/v2/command.rb +20 -0
  28. data/lib/cyclone_lariat/messages/v2/event.rb +20 -0
  29. data/lib/cyclone_lariat/messages/v2/validator.rb +39 -0
  30. data/lib/cyclone_lariat/middleware.rb +9 -5
  31. data/lib/cyclone_lariat/migration.rb +151 -0
  32. data/lib/cyclone_lariat/options.rb +52 -0
  33. data/lib/cyclone_lariat/presenters/graph.rb +54 -0
  34. data/lib/cyclone_lariat/presenters/queues.rb +41 -0
  35. data/lib/cyclone_lariat/presenters/subscriptions.rb +34 -0
  36. data/lib/cyclone_lariat/presenters/topics.rb +40 -0
  37. data/lib/cyclone_lariat/publisher.rb +25 -0
  38. data/lib/cyclone_lariat/repo/active_record/messages.rb +92 -0
  39. data/lib/cyclone_lariat/repo/active_record/versions.rb +28 -0
  40. data/lib/cyclone_lariat/repo/messages.rb +43 -0
  41. data/lib/cyclone_lariat/repo/messages_mapper.rb +49 -0
  42. data/lib/cyclone_lariat/repo/sequel/messages.rb +73 -0
  43. data/lib/cyclone_lariat/repo/sequel/versions.rb +28 -0
  44. data/lib/cyclone_lariat/repo/versions.rb +42 -0
  45. data/lib/cyclone_lariat/resources/queue.rb +167 -0
  46. data/lib/cyclone_lariat/resources/topic.rb +132 -0
  47. data/lib/cyclone_lariat/services/migrate.rb +51 -0
  48. data/lib/cyclone_lariat/services/rollback.rb +51 -0
  49. data/lib/cyclone_lariat/version.rb +1 -1
  50. data/lib/cyclone_lariat.rb +4 -10
  51. data/lib/tasks/console.rake +13 -0
  52. data/lib/tasks/cyclone_lariat.rake +42 -0
  53. data/lib/tasks/db.rake +0 -15
  54. metadata +161 -20
  55. data/config/db.example.rb +0 -9
  56. data/db/migrate/01_add_uuid_extensions.rb +0 -15
  57. data/db/migrate/02_add_events.rb +0 -19
  58. data/docs/_imgs/diagram.png +0 -0
  59. data/docs/_imgs/lariat.jpg +0 -0
  60. data/lib/cyclone_lariat/abstract/client.rb +0 -106
  61. data/lib/cyclone_lariat/abstract/message.rb +0 -83
  62. data/lib/cyclone_lariat/command.rb +0 -13
  63. data/lib/cyclone_lariat/configure.rb +0 -15
  64. data/lib/cyclone_lariat/event.rb +0 -13
  65. data/lib/cyclone_lariat/messages_mapper.rb +0 -46
  66. data/lib/cyclone_lariat/messages_repo.rb +0 -60
  67. data/lib/cyclone_lariat/sns_client.rb +0 -38
  68. data/lib/cyclone_lariat/sqs_client.rb +0 -39
@@ -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