outboxer 0.1.11 → 1.0.0.pre.beta
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.
- checksums.yaml +4 -4
- data/README.md +81 -47
- data/db/migrate/create_outboxer_exceptions.rb +20 -0
- data/db/migrate/create_outboxer_frames.rb +16 -0
- data/db/migrate/create_outboxer_messages.rb +19 -0
- data/generators/message_publisher_generator.rb +11 -0
- data/generators/{outboxer/install_generator.rb → schema_generator.rb} +4 -9
- data/lib/outboxer/database.rb +44 -0
- data/lib/outboxer/logger.rb +17 -0
- data/lib/outboxer/message.rb +262 -13
- data/lib/outboxer/messages.rb +232 -0
- data/lib/outboxer/models/exception.rb +15 -0
- data/lib/outboxer/models/frame.rb +14 -0
- data/lib/outboxer/models/message.rb +46 -0
- data/lib/outboxer/models.rb +3 -0
- data/lib/outboxer/railtie.rb +2 -4
- data/lib/outboxer/version.rb +1 -1
- data/lib/outboxer/web/public/css/bootstrap-icons.css +2078 -0
- data/lib/outboxer/web/public/css/bootstrap-icons.min.css +5 -0
- data/lib/outboxer/web/public/css/bootstrap.css +12071 -0
- data/lib/outboxer/web/public/css/bootstrap.min.css +6 -0
- data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff +0 -0
- data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff2 +0 -0
- data/lib/outboxer/web/public/favicon.svg +3 -0
- data/lib/outboxer/web/public/js/bootstrap.bundle.js +6306 -0
- data/lib/outboxer/web/public/js/bootstrap.bundle.min.js +7 -0
- data/lib/outboxer/web/views/error.erb +63 -0
- data/lib/outboxer/web/views/home.erb +172 -0
- data/lib/outboxer/web/views/layout.erb +80 -0
- data/lib/outboxer/web/views/message.erb +81 -0
- data/lib/outboxer/web/views/messages/index.erb +60 -0
- data/lib/outboxer/web/views/messages/show.erb +31 -0
- data/lib/outboxer/web/views/messages.erb +262 -0
- data/lib/outboxer/web.rb +430 -0
- data/lib/outboxer.rb +9 -5
- metadata +279 -22
- data/.rspec +0 -3
- data/.rubocop.yml +0 -229
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -84
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -12
- data/generators/outboxer/templates/bin/publisher.rb +0 -11
- data/generators/outboxer/templates/migrations/create_outboxer_exceptions.rb +0 -15
- data/generators/outboxer/templates/migrations/create_outboxer_messages.rb +0 -13
- data/lib/outboxer/exception.rb +0 -9
- data/lib/outboxer/outboxable.rb +0 -21
- data/lib/outboxer/publisher.rb +0 -149
- data/lib/tasks/gem.rake +0 -58
- data/outboxer.gemspec +0 -33
- data/sig/outboxer.rbs +0 -19
@@ -0,0 +1,232 @@
|
|
1
|
+
module Outboxer
|
2
|
+
module Messages
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def counts_by_status
|
6
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
7
|
+
status_counts = Models::Message::STATUSES.each_with_object({ all: 0 }) do |status, hash|
|
8
|
+
hash[status.to_sym] = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
Models::Message.group(:status).count.each do |status, count|
|
12
|
+
status_counts[status.to_sym] = count
|
13
|
+
status_counts[:all] += count
|
14
|
+
end
|
15
|
+
|
16
|
+
status_counts
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def queue(limit: 1)
|
21
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
22
|
+
ActiveRecord::Base.transaction do
|
23
|
+
messages = Models::Message
|
24
|
+
.where(status: Models::Message::Status::BACKLOGGED)
|
25
|
+
.order(updated_at: :asc)
|
26
|
+
.lock('FOR UPDATE SKIP LOCKED')
|
27
|
+
.limit(limit)
|
28
|
+
.select(:id, :messageable_type, :messageable_id)
|
29
|
+
|
30
|
+
if messages.present?
|
31
|
+
Models::Message
|
32
|
+
.where(id: messages.map { |message| message[:id] })
|
33
|
+
.update_all(updated_at: Time.current, status: Models::Message::Status::QUEUED)
|
34
|
+
end
|
35
|
+
|
36
|
+
messages.map do |message|
|
37
|
+
{
|
38
|
+
id: message.id,
|
39
|
+
messageable_type: message.messageable_type,
|
40
|
+
messageable_id: message.messageable_id
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
LIST_STATUS_OPTIONS = [nil, :backlogged, :queued, :publishing, :failed]
|
48
|
+
LIST_STATUS_DEFAULT = nil
|
49
|
+
|
50
|
+
LIST_SORT_OPTIONS = [:id, :status, :messageable, :created_at, :updated_at]
|
51
|
+
LIST_SORT_DEFAULT = :updated_at
|
52
|
+
|
53
|
+
LIST_ORDER_OPTIONS = [:asc, :desc]
|
54
|
+
LIST_ORDER_DEFAULT = :asc
|
55
|
+
|
56
|
+
LIST_PAGE_DEFAULT = 1
|
57
|
+
|
58
|
+
LIST_PER_PAGE_OPTIONS = [10, 100, 200, 500, 1000]
|
59
|
+
LIST_PER_PAGE_DEFAULT = 100
|
60
|
+
|
61
|
+
def list(status: LIST_STATUS_DEFAULT,
|
62
|
+
sort: LIST_SORT_DEFAULT, order: LIST_ORDER_DEFAULT,
|
63
|
+
page: LIST_PAGE_DEFAULT, per_page: LIST_PER_PAGE_DEFAULT)
|
64
|
+
if !status.nil? && !LIST_STATUS_OPTIONS.include?(status.to_sym)
|
65
|
+
raise ArgumentError, "status must be #{LIST_STATUS_OPTIONS.join(' ')}"
|
66
|
+
end
|
67
|
+
|
68
|
+
if !LIST_SORT_OPTIONS.include?(sort.to_sym)
|
69
|
+
raise ArgumentError, "sort must be #{LIST_SORT_OPTIONS.join(' ')}"
|
70
|
+
end
|
71
|
+
|
72
|
+
if !LIST_ORDER_OPTIONS.include?(order.to_sym)
|
73
|
+
raise ArgumentError, "order must be #{LIST_ORDER_OPTIONS.join(' ')}"
|
74
|
+
end
|
75
|
+
|
76
|
+
if !page.is_a?(Integer) || page <= 0
|
77
|
+
raise ArgumentError, "page must be >= 1"
|
78
|
+
end
|
79
|
+
|
80
|
+
if !LIST_PER_PAGE_OPTIONS.include?(per_page.to_i)
|
81
|
+
raise ArgumentError, "per_page must be #{LIST_PER_PAGE_OPTIONS.join(' ')}"
|
82
|
+
end
|
83
|
+
|
84
|
+
message_scope = Models::Message
|
85
|
+
message_scope = status.nil? ? message_scope.all : message_scope.where(status: status)
|
86
|
+
|
87
|
+
message_scope =
|
88
|
+
if sort.to_sym == :messageable
|
89
|
+
message_scope.order(messageable_type: order, messageable_id: order)
|
90
|
+
else
|
91
|
+
message_scope.order(sort => order)
|
92
|
+
end
|
93
|
+
|
94
|
+
messages = ActiveRecord::Base.connection_pool.with_connection do
|
95
|
+
message_scope.page(page).per(per_page)
|
96
|
+
end
|
97
|
+
|
98
|
+
{
|
99
|
+
messages: messages.map do |message|
|
100
|
+
{
|
101
|
+
id: message.id,
|
102
|
+
status: message.status.to_sym,
|
103
|
+
messageable_type: message.messageable_type,
|
104
|
+
messageable_id: message.messageable_id,
|
105
|
+
created_at: message.created_at.utc,
|
106
|
+
updated_at: message.updated_at.utc
|
107
|
+
}
|
108
|
+
end,
|
109
|
+
total_pages: messages.total_pages,
|
110
|
+
current_page: messages.current_page,
|
111
|
+
limit_value: messages.limit_value,
|
112
|
+
total_count: messages.total_count
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
REPUBLISH_ALL_STATUSES = [:queued, :publishing, :failed]
|
117
|
+
|
118
|
+
def can_republish_all?(status:)
|
119
|
+
REPUBLISH_ALL_STATUSES.include?(status&.to_sym)
|
120
|
+
end
|
121
|
+
|
122
|
+
def republish_all(status:, batch_size: 100)
|
123
|
+
if !can_republish_all?(status: status)
|
124
|
+
status_formatted = status.nil? ? 'nil' : status
|
125
|
+
|
126
|
+
raise ArgumentError,
|
127
|
+
"Status #{status_formatted} must be one of #{REPUBLISH_ALL_STATUSES.join(', ')}"
|
128
|
+
end
|
129
|
+
|
130
|
+
republished_count = 0
|
131
|
+
|
132
|
+
loop do
|
133
|
+
republished_count_batch = 0
|
134
|
+
|
135
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
136
|
+
ActiveRecord::Base.transaction do
|
137
|
+
locked_ids = Models::Message
|
138
|
+
.where(status: status)
|
139
|
+
.order(updated_at: :asc)
|
140
|
+
.limit(batch_size)
|
141
|
+
.lock('FOR UPDATE SKIP LOCKED')
|
142
|
+
.pluck(:id)
|
143
|
+
|
144
|
+
republished_count_batch = Models::Message
|
145
|
+
.where(id: locked_ids)
|
146
|
+
.update_all(status: Models::Message::Status::BACKLOGGED, updated_at: Time.now.utc)
|
147
|
+
|
148
|
+
republished_count += republished_count_batch
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
break if republished_count_batch < batch_size
|
153
|
+
end
|
154
|
+
|
155
|
+
{ republished_count: republished_count }
|
156
|
+
end
|
157
|
+
|
158
|
+
def republish_selected(ids:)
|
159
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
160
|
+
ActiveRecord::Base.transaction do
|
161
|
+
locked_ids = Models::Message
|
162
|
+
.where(id: ids)
|
163
|
+
.order(updated_at: :asc)
|
164
|
+
.lock('FOR UPDATE SKIP LOCKED')
|
165
|
+
.pluck(:id)
|
166
|
+
|
167
|
+
republished_count = Models::Message
|
168
|
+
.where(id: locked_ids)
|
169
|
+
.update_all(status: Models::Message::Status::BACKLOGGED, updated_at: Time.now.utc)
|
170
|
+
|
171
|
+
{ republished_count: republished_count, not_republished_ids: ids - locked_ids }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def delete_all(status: nil, batch_size: 100)
|
177
|
+
deleted_count = 0
|
178
|
+
|
179
|
+
loop do
|
180
|
+
deleted_count_batch = 0
|
181
|
+
|
182
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
183
|
+
ActiveRecord::Base.transaction do
|
184
|
+
query = Models::Message.all
|
185
|
+
query = query.where(status: status) unless status.nil?
|
186
|
+
locked_ids = query.order(:updated_at)
|
187
|
+
.limit(batch_size)
|
188
|
+
.lock('FOR UPDATE SKIP LOCKED')
|
189
|
+
.pluck(:id)
|
190
|
+
|
191
|
+
Models::Frame
|
192
|
+
.joins(:exception)
|
193
|
+
.where(exception: { message_id: locked_ids })
|
194
|
+
.delete_all
|
195
|
+
|
196
|
+
Models::Exception.where(message_id: locked_ids).delete_all
|
197
|
+
|
198
|
+
deleted_count_batch = Models::Message.where(id: locked_ids).delete_all
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
deleted_count += deleted_count_batch
|
203
|
+
|
204
|
+
break if deleted_count_batch < batch_size
|
205
|
+
end
|
206
|
+
|
207
|
+
{ deleted_count: deleted_count }
|
208
|
+
end
|
209
|
+
|
210
|
+
def delete_selected(ids:)
|
211
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
212
|
+
ActiveRecord::Base.transaction do
|
213
|
+
locked_ids = Models::Message
|
214
|
+
.where(id: ids)
|
215
|
+
.lock('FOR UPDATE SKIP LOCKED')
|
216
|
+
.pluck(:id)
|
217
|
+
|
218
|
+
Models::Frame
|
219
|
+
.joins(:exception)
|
220
|
+
.where(exception: { message_id: locked_ids })
|
221
|
+
.delete_all
|
222
|
+
|
223
|
+
Models::Exception.where(message_id: locked_ids).delete_all
|
224
|
+
|
225
|
+
deleted_count = Models::Message.where(id: locked_ids).delete_all
|
226
|
+
|
227
|
+
{ deleted_count: deleted_count, not_deleted_ids: ids - locked_ids }
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Outboxer
|
2
|
+
module Models
|
3
|
+
class Exception < ::ActiveRecord::Base
|
4
|
+
self.table_name = :outboxer_exceptions
|
5
|
+
|
6
|
+
belongs_to :message, class_name: "Outboxer::Models::Message"
|
7
|
+
|
8
|
+
has_many :frames, -> { order(index: :asc) },
|
9
|
+
class_name: "Outboxer::Models::Frame",
|
10
|
+
foreign_key: "exception_id"
|
11
|
+
|
12
|
+
validates :message_id, presence: true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Outboxer
|
2
|
+
module Models
|
3
|
+
class Frame < ::ActiveRecord::Base
|
4
|
+
self.table_name = :outboxer_frames
|
5
|
+
|
6
|
+
belongs_to :exception, class_name: 'Outboxer::Models::Exception', foreign_key: 'exception_id'
|
7
|
+
validates :exception_id, presence: true
|
8
|
+
|
9
|
+
validates :index,
|
10
|
+
presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
11
|
+
validates :text, presence: true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Outboxer
|
2
|
+
module Models
|
3
|
+
# Represents a message in the outbox.
|
4
|
+
#
|
5
|
+
# @!attribute [r] id
|
6
|
+
# @return [Integer] The unique id for the message.
|
7
|
+
# @!attribute [r] messageable_id
|
8
|
+
# @return [Integer] The ID of the associated polymorphic message.
|
9
|
+
# @!attribute [r] messageable_type
|
10
|
+
# @return [String] The type of the associated polymorphic message.
|
11
|
+
# @!attribute status
|
12
|
+
# @return [String] The status of the message (see {STATUSES}).
|
13
|
+
# @!attribute [r] created_at
|
14
|
+
# @return [Time] The timestamp when the record was created.
|
15
|
+
# @!attribute [r] updated_at
|
16
|
+
# @return [Time] The timestamp when the record was last updated.
|
17
|
+
class Message < ::ActiveRecord::Base
|
18
|
+
self.table_name = :outboxer_messages
|
19
|
+
|
20
|
+
module Status
|
21
|
+
BACKLOGGED = 'backlogged'
|
22
|
+
QUEUED = 'queued'
|
23
|
+
PUBLISHING = 'publishing'
|
24
|
+
FAILED = 'failed'
|
25
|
+
end
|
26
|
+
|
27
|
+
STATUSES = [Status::BACKLOGGED, Status::QUEUED, Status::PUBLISHING, Status::FAILED]
|
28
|
+
|
29
|
+
scope :backlogged, -> { where(status: Status::BACKLOGGED) }
|
30
|
+
scope :queued, -> { where(status: Status::QUEUED) }
|
31
|
+
scope :publishing, -> { where(status: Status::PUBLISHING) }
|
32
|
+
scope :failed, -> { where(status: Status::FAILED) }
|
33
|
+
|
34
|
+
attribute :status, default: -> { Status::BACKLOGGED }
|
35
|
+
validates :status, inclusion: { in: STATUSES }, length: { maximum: 255 }
|
36
|
+
|
37
|
+
belongs_to :messageable, polymorphic: true
|
38
|
+
|
39
|
+
has_many :exceptions,
|
40
|
+
-> { order(created_at: :asc) },
|
41
|
+
foreign_key: 'message_id',
|
42
|
+
class_name: "Outboxer::Models::Exception",
|
43
|
+
dependent: :destroy
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/outboxer/railtie.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
-
require "outboxer"
|
2
|
-
require "rails"
|
3
|
-
|
4
1
|
module Outboxer
|
5
2
|
class Railtie < Rails::Railtie
|
6
3
|
railtie_name :outboxer
|
7
4
|
|
8
5
|
generators do
|
9
|
-
require_relative
|
6
|
+
require_relative '../../generators/schema_generator'
|
7
|
+
require_relative '../../generators/message_publisher_generator'
|
10
8
|
end
|
11
9
|
end
|
12
10
|
end
|
data/lib/outboxer/version.rb
CHANGED