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.
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