outboxer 0.1.11 → 1.0.0.pre.beta
Sign up to get free protection for your applications and to get access to all the features.
- 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