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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -47
  3. data/db/migrate/create_outboxer_exceptions.rb +20 -0
  4. data/db/migrate/create_outboxer_frames.rb +16 -0
  5. data/db/migrate/create_outboxer_messages.rb +19 -0
  6. data/generators/message_publisher_generator.rb +11 -0
  7. data/generators/{outboxer/install_generator.rb → schema_generator.rb} +4 -9
  8. data/lib/outboxer/database.rb +44 -0
  9. data/lib/outboxer/logger.rb +17 -0
  10. data/lib/outboxer/message.rb +262 -13
  11. data/lib/outboxer/messages.rb +232 -0
  12. data/lib/outboxer/models/exception.rb +15 -0
  13. data/lib/outboxer/models/frame.rb +14 -0
  14. data/lib/outboxer/models/message.rb +46 -0
  15. data/lib/outboxer/models.rb +3 -0
  16. data/lib/outboxer/railtie.rb +2 -4
  17. data/lib/outboxer/version.rb +1 -1
  18. data/lib/outboxer/web/public/css/bootstrap-icons.css +2078 -0
  19. data/lib/outboxer/web/public/css/bootstrap-icons.min.css +5 -0
  20. data/lib/outboxer/web/public/css/bootstrap.css +12071 -0
  21. data/lib/outboxer/web/public/css/bootstrap.min.css +6 -0
  22. data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff +0 -0
  23. data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff2 +0 -0
  24. data/lib/outboxer/web/public/favicon.svg +3 -0
  25. data/lib/outboxer/web/public/js/bootstrap.bundle.js +6306 -0
  26. data/lib/outboxer/web/public/js/bootstrap.bundle.min.js +7 -0
  27. data/lib/outboxer/web/views/error.erb +63 -0
  28. data/lib/outboxer/web/views/home.erb +172 -0
  29. data/lib/outboxer/web/views/layout.erb +80 -0
  30. data/lib/outboxer/web/views/message.erb +81 -0
  31. data/lib/outboxer/web/views/messages/index.erb +60 -0
  32. data/lib/outboxer/web/views/messages/show.erb +31 -0
  33. data/lib/outboxer/web/views/messages.erb +262 -0
  34. data/lib/outboxer/web.rb +430 -0
  35. data/lib/outboxer.rb +9 -5
  36. metadata +279 -22
  37. data/.rspec +0 -3
  38. data/.rubocop.yml +0 -229
  39. data/CHANGELOG.md +0 -5
  40. data/CODE_OF_CONDUCT.md +0 -84
  41. data/LICENSE.txt +0 -21
  42. data/Rakefile +0 -12
  43. data/generators/outboxer/templates/bin/publisher.rb +0 -11
  44. data/generators/outboxer/templates/migrations/create_outboxer_exceptions.rb +0 -15
  45. data/generators/outboxer/templates/migrations/create_outboxer_messages.rb +0 -13
  46. data/lib/outboxer/exception.rb +0 -9
  47. data/lib/outboxer/outboxable.rb +0 -21
  48. data/lib/outboxer/publisher.rb +0 -149
  49. data/lib/tasks/gem.rake +0 -58
  50. data/outboxer.gemspec +0 -33
  51. 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
@@ -0,0 +1,3 @@
1
+ require_relative "models/frame"
2
+ require_relative "models/exception"
3
+ require_relative "models/message"
@@ -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 "../../generators/outboxer/install_generator"
6
+ require_relative '../../generators/schema_generator'
7
+ require_relative '../../generators/message_publisher_generator'
10
8
  end
11
9
  end
12
10
  end
@@ -1,3 +1,3 @@
1
1
  module Outboxer
2
- VERSION = "0.1.11".freeze
2
+ VERSION = "1.0.0-beta".freeze
3
3
  end