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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f881a119f2c2123b34c0a7153590c2eab9907b44045e64d51e16ce914e731bb6
4
- data.tar.gz: d30a09f38f02fea3bce19895922d294e40dfb25555e1fcf4fd06b33560d1e2db
3
+ metadata.gz: 02b688da126c6e1e723b3f7df4f5b2bcf985c9d204051292e4b10f76ee9f1060
4
+ data.tar.gz: 512b45ceaa6f67f454c5fa9690684852624c76af9c804697ba79d3f5119a9d6e
5
5
  SHA512:
6
- metadata.gz: df0a95d0f24b7ff85038711be1acd4aec4dc5760accb390fb40fdf07342d911b5b07373c4eacbc2e05e25a7a89137ef26c20e201b556c6e07973469806eddaff
7
- data.tar.gz: 1d329faae4668703b51cd5becdce8215e4251e1217d9c8311795ab602d71e140511b79874908963a947c24939025bd2dbcd8af0497aa985d9478f7919d7602d7
6
+ metadata.gz: f1aaaf3225fc64d1371ceaf295a1184296c691d355d8fa082ef201fe3efc82ed6767cfb60592855012853e1e8f1529a99d6d7f199d80a2e41a0b3dca3bfd2a8d
7
+ data.tar.gz: 2087f71e099d99f80b053bc61d4fee3e591205aa499946ced71c19ea0ad74dc17a0684029e52880f95de470ab93324827e6f29ca92f179a2e9ef2bc80b0c1fae
data/README.md CHANGED
@@ -5,96 +5,130 @@
5
5
 
6
6
  ## Background
7
7
 
8
- Typically in event driven Ruby on Rails applications:
8
+ Outboxer is an ActiveRecord implementation of the [transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html).
9
9
 
10
- 1. a domain event model is created in an SQL database table
11
- 2. a sidekiq worker is queued to handle the domain event asynchronously
10
+ ## Installation
12
11
 
13
- ## Problem
12
+ ### 1. add gem to your application's gemfile
14
13
 
15
- As these two operations span multiple database types (SQL and redis), they can not be combined into a single atomic operation using a transaction. If either step fails, inconsistencies can occur.
16
-
17
- ## Solution
18
-
19
- Outboxer is a simple Ruby on Rails implementation of the [transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html): a well established solution to this problem. It ensures both operations succeed _eventually_, or both fail.
20
-
21
- ### Getting started
22
-
23
- ### Installation
24
-
25
- 1. Add the Outboxer gem to your application's Gemfile:
26
-
27
- ```ruby
14
+ ```
28
15
  gem 'outboxer'
29
16
  ```
30
17
 
31
- 2. Install the Outboxer gem:
18
+ ### 2. install gem
32
19
 
33
- ```bash
20
+ ```
34
21
  bundle install
35
22
  ```
36
23
 
37
- 3. Generate the migration and publisher files
24
+ ## Usage
25
+
26
+ ### 1. generate schema
38
27
 
39
28
  ```bash
40
- bin/rails generate outboxer:install
29
+ bin/rails g outboxer:schema
41
30
  ```
42
31
 
43
- ### Usage
32
+ ### 2. migrate schema
33
+
34
+ ```bash
35
+ bin/rake db:migrate
36
+ ```
44
37
 
45
- #### 1. Include `Outboxer::Outboxable` into your existing Message model
38
+ ### 3. after event created, backlog message
46
39
 
47
40
  ```ruby
48
- class Message < ApplicationRecord
49
- include Outboxer::Outboxable
41
+ class Event < ActiveRecord::Base
42
+ # ...
43
+
44
+ after_create do |event|
45
+ Outboxer::Message.backlog(
46
+ messageable_type: event.class.name,
47
+ messageable_id: event.id)
48
+ end
50
49
  end
51
50
  ```
52
51
 
53
- #### 2. Update the generated bin/publish block e.g.
52
+ ### 4. add event created job
54
53
 
55
54
  ```ruby
56
- Outboxer::Publisher.publish do |message:, logger:|
57
- logger.info("[#{message.id}] publishing")
55
+ class EventCreatedJob
56
+ include Sidekiq::Job
58
57
 
59
- HardJob.perform_async({ "message_id" => message.id })
58
+ def perform(args)
59
+ event = Event.find(args['id'])
60
60
 
61
- logger.info("[#{message.id}] published")
61
+ # ...
62
+ end
62
63
  end
63
64
  ```
64
65
 
65
- #### 3. Migrate the database
66
+ ### 5. generate message publisher
66
67
 
67
68
  ```bash
68
- bin/rake db:migrate
69
+ bin/rails g outboxer:message_publisher
69
70
  ```
70
71
 
71
- #### 4. Run the publisher
72
+ ### 6. update publish block to perform event created job async
73
+
74
+ ```ruby
75
+ Outboxer::Publisher.publish do |message|
76
+ case message[:messageable_type]
77
+ when 'Event'
78
+ EventCreatedJob.perform_async({ 'id' => message[:messageable_id] })
79
+ end
80
+ end
81
+ ```
82
+
83
+ ### 6. run message publisher
72
84
 
73
85
  ```bash
74
- bin/publisher
86
+ bin/outboxer_message_publisher
75
87
  ```
76
88
 
77
- ## Implementation
89
+ ### 7. manage messages
78
90
 
79
- 1. when an `ActiveRecord` model that includes `Outbox::Outboxable` is created, an `unpublished` `Outboxer::Message` is automatically created in the same transaction, with `Outboxer::Message#message` polymorphically assigned to the original model
91
+ manage backlogged, queued, publishing and failed messages with the web ui
80
92
 
81
- 2. When the publisher finds a new `unpublished` `Outboxer::Message`, it yields to a user-supplied block and then:
82
- - removes it if the task completes successfully
83
- - marks it as failed and records the error if there's a problem
93
+ <img width="1257" alt="Screenshot 2024-05-20 at 8 47 57 pm" src="https://github.com/fast-programmer/outboxer/assets/394074/0446bc7e-9d5f-4fe1-b210-ff394bdacdd6">
84
94
 
85
- To see all the parts working together in a single place, check out the [publisher_spec.rb](https://github.com/fast-programmer/outboxer/blob/master/spec/outboxer/publisher_spec.rb)
95
+ #### rails
86
96
 
97
+ ##### config/routes.rb
87
98
 
88
- ## Motivation
99
+ ```ruby
100
+ require 'outboxer/web'
101
+
102
+ Rails.application.routes.draw do
103
+ mount Outboxer::Web, at: '/outboxer'
104
+ end
105
+ ```
106
+
107
+ #### rack
89
108
 
90
- Outboxer was created to help high growth SAAS companies transition to event driven architecture quickly.
109
+ ##### config.ru
91
110
 
92
- Specifically this means:
111
+ ```ruby
112
+ require 'outboxer/web'
113
+
114
+ map '/outboxer' do
115
+ run Outboxer::Web
116
+ end
117
+ ```
118
+
119
+ ### 8. monitor message publisher
120
+
121
+ understanding how much memory and cpu is required by the message publisher
122
+
123
+ <img width="310" alt="Screenshot 2024-05-20 at 10 41 57 pm" src="https://github.com/fast-programmer/outboxer/assets/394074/1222ad47-15e3-44d1-bb45-6abc6b3e4325">
124
+
125
+ ```bash
126
+ run bin/outboxer_message_publishermon
127
+ ```
128
+
129
+ ## Motivation
93
130
 
94
- 1. fast integration into existing Ruby on Rails applications (< 1 hour)
95
- 2. comprehensive documentation
96
- 3. high reliability in production environments
97
- 4. forever free to use in commerical applications (MIT licence)
131
+ Outboxer was created to help Rails teams migrate to eventually consistent event driven architecture quickly, using existing tools and infrastructure.
98
132
 
99
133
  ## Contributing
100
134
 
@@ -0,0 +1,20 @@
1
+ class CreateOutboxerExceptions < ActiveRecord::Migration[6.1]
2
+ def up
3
+ ActiveRecord::Base.transaction do
4
+ create_table :outboxer_exceptions do |t|
5
+ t.references :message, null: false, foreign_key: { to_table: :outboxer_messages }
6
+
7
+ t.text :class_name, null: false
8
+ t.text :message_text, null: false
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ remove_column :outboxer_exceptions, :updated_at
14
+ end
15
+ end
16
+
17
+ def down
18
+ drop_table :outboxer_exceptions if table_exists?(:outboxer_exceptions)
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ class CreateOutboxerFrames < ActiveRecord::Migration[6.1]
2
+ def up
3
+ create_table :outboxer_frames do |t|
4
+ t.references :exception, null: false, foreign_key: { to_table: :outboxer_exceptions }
5
+
6
+ t.integer :index, null: false
7
+ t.text :text, null: false
8
+
9
+ t.index [:exception_id, :index], unique: true
10
+ end
11
+ end
12
+
13
+ def down
14
+ drop_table :outboxer_frames if table_exists?(:outboxer_frames)
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ class CreateOutboxerMessages < ActiveRecord::Migration[6.1]
2
+ def up
3
+ create_table :outboxer_messages do |t|
4
+ t.string :status, null: false, limit: 255
5
+
6
+ t.text :messageable_id, null: false
7
+ t.text :messageable_type, null: false
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :outboxer_messages, :status
13
+ add_index :outboxer_messages, [:status, :updated_at]
14
+ end
15
+
16
+ def down
17
+ drop_table :outboxer_messages if table_exists?(:outboxer_messages)
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module Outboxer
2
+ class MessagePublisherGenerator < Rails::Generators::Base
3
+ include Rails::Generators::Migration
4
+
5
+ source_root File.expand_path('../', __dir__)
6
+ def copy_bin_file
7
+ template "bin/outboxer_message_publisher", "bin/outboxer_message_publisher"
8
+ run "chmod +x bin/outboxer_message_publisher"
9
+ end
10
+ end
11
+ end
@@ -1,8 +1,8 @@
1
1
  module Outboxer
2
- class InstallGenerator < Rails::Generators::Base
2
+ class SchemaGenerator < Rails::Generators::Base
3
3
  include Rails::Generators::Migration
4
4
 
5
- source_root File.expand_path("templates", __dir__)
5
+ source_root File.expand_path('../', __dir__)
6
6
 
7
7
  def self.next_migration_number(dirname)
8
8
  next_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
@@ -12,18 +12,13 @@ module Outboxer
12
12
  next_number.to_s
13
13
  end
14
14
 
15
- def copy_bin_file
16
- template "bin/publisher.rb", "bin/publisher"
17
- run "chmod +x bin/publisher"
18
- end
19
-
20
15
  def copy_migrations
21
16
  migration_template(
22
- "migrations/create_outboxer_messages.rb",
17
+ "db/migrate/create_outboxer_messages.rb",
23
18
  "db/migrate/create_outboxer_messages.rb")
24
19
 
25
20
  migration_template(
26
- "migrations/create_outboxer_exceptions.rb",
21
+ "db/migrate/create_outboxer_exceptions.rb",
27
22
  "db/migrate/create_outboxer_exceptions.rb")
28
23
  end
29
24
  end
@@ -0,0 +1,44 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ module Outboxer
5
+ module Database
6
+ extend self
7
+
8
+ def config(environment: ENV['RAILS_ENV'] || 'development', path: 'config/database.yml')
9
+ db_config_content = File.read(path)
10
+ db_config_erb_result = ERB.new(db_config_content).result
11
+ YAML.safe_load(db_config_erb_result, aliases: true)[environment]
12
+ end
13
+
14
+ def connect(config:, logger: Logger.new($stdout, level: Logger::INFO))
15
+ logger&.info " _ _ "
16
+ logger&.info " | | | | "
17
+ logger&.info " ___ _ _| |_| |__ _____ _____ _ __ "
18
+ logger&.info " / _ \\| | | | __| '_ \\ / _ \\ \\/ / _ \\ '__|"
19
+ logger&.info " | (_) | |_| | |_| |_) | (_) > < __/ | "
20
+ logger&.info " \\___/ \\__,_|\\__|_.__/ \\___/_/\\_\\___|_| "
21
+ logger&.info " "
22
+ logger&.info " "
23
+
24
+ logger&.info "Running in ruby #{RUBY_VERSION} " \
25
+ "(#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION[0, 10]}) [#{RUBY_PLATFORM}]"
26
+
27
+ logger&.info "Connecting to database"
28
+ ActiveRecord::Base.establish_connection(config)
29
+ ActiveRecord::Base.connection_pool.with_connection {}
30
+ logger&.info "Connected to database"
31
+ end
32
+
33
+ def connected?
34
+ ActiveRecord::Base.connected?
35
+ end
36
+
37
+ def disconnect(logger: Logger.new($stdout, level: Logger::INFO))
38
+ logger&.info "Disconnecting from database"
39
+ ActiveRecord::Base.connection_handler.clear_active_connections!
40
+ ActiveRecord::Base.connection_handler.connection_pool_list.each(&:disconnect!)
41
+ logger&.info "Disconnected from database"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ require 'logger'
2
+
3
+ module Outboxer
4
+ class Logger < ::Logger
5
+ def initialize(*args, **kwargs)
6
+ super(*args, **kwargs)
7
+
8
+ self.formatter = proc do |severity, datetime, progname, msg|
9
+ formatted_time = datetime.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
10
+ pid = Process.pid
11
+ tid = Thread.current.object_id.to_s(36)
12
+ level = severity
13
+ "#{formatted_time} pid=#{pid} tid=#{tid} #{level}: #{msg}\n"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,19 +1,268 @@
1
- require "active_record"
2
-
3
1
  module Outboxer
4
- class Message < ::ActiveRecord::Base
5
- self.table_name = :outboxer_messages
2
+ module Message
3
+ extend self
4
+
5
+ Status = Models::Message::Status
6
+
7
+ def backlog(messageable_type:, messageable_id:)
8
+ ActiveRecord::Base.connection_pool.with_connection do
9
+ ActiveRecord::Base.transaction do
10
+ current_time = Time.now.utc
11
+
12
+ message = Models::Message.create!(
13
+ messageable_id: messageable_id,
14
+ messageable_type: messageable_type,
15
+ status: Models::Message::Status::BACKLOGGED,
16
+ created_at: current_time,
17
+ updated_at: current_time)
18
+
19
+ { id: message.id }
20
+ end
21
+ end
22
+ end
23
+
24
+ def find_by_id(id:)
25
+ ActiveRecord::Base.connection_pool.with_connection do
26
+ ActiveRecord::Base.transaction do
27
+ message = Models::Message.includes(exceptions: :frames).find_by!(id: id)
28
+
29
+ {
30
+ id: message.id,
31
+ status: message.status,
32
+ messageable_type: message.messageable_type,
33
+ messageable_id: message.messageable_id,
34
+ created_at: message.created_at.utc.to_s,
35
+ updated_at: message.updated_at.utc.to_s,
36
+ exceptions: message.exceptions.map do |exception|
37
+ {
38
+ id: exception.id,
39
+ class_name: exception.class_name,
40
+ message_text: exception.message_text,
41
+ created_at: exception.created_at.utc.to_s,
42
+ frames: exception.frames.map do |frame|
43
+ {
44
+ id: frame.id,
45
+ index: frame.index,
46
+ text: frame.text
47
+ }
48
+ end
49
+ }
50
+ end
51
+ }
52
+ end
53
+ end
54
+ end
55
+
56
+ def publishing(id:)
57
+ ActiveRecord::Base.connection_pool.with_connection do
58
+ ActiveRecord::Base.transaction do
59
+ message = Models::Message.lock.find_by!(id: id)
60
+
61
+ if message.status != Models::Message::Status::QUEUED
62
+ raise ArgumentError,
63
+ "cannot transition outboxer message #{message.id} " \
64
+ "from #{message.status} to #{Models::Message::Status::PUBLISHING}"
65
+ end
66
+
67
+ message.update!(
68
+ status: Models::Message::Status::PUBLISHING,
69
+ updated_at: Time.now.utc)
70
+
71
+ {
72
+ id: id,
73
+ status: message.status,
74
+ messageable_type: message.messageable_type,
75
+ messageable_id: message.messageable_id
76
+ }
77
+ end
78
+ end
79
+ end
80
+
81
+ def published(id:)
82
+ ActiveRecord::Base.connection_pool.with_connection do
83
+ ActiveRecord::Base.transaction do
84
+ message = Models::Message.lock.find_by!(id: id)
85
+
86
+ if message.status != Models::Message::Status::PUBLISHING
87
+ raise ArgumentError,
88
+ "cannot transition outboxer message #{message.id} " \
89
+ "from #{message.status} to (deleted)"
90
+ end
91
+
92
+ message.exceptions.each { |exception| exception.frames.each(&:delete) }
93
+ message.exceptions.delete_all
94
+ message.delete
95
+
96
+ { id: id }
97
+ end
98
+ end
99
+ end
100
+
101
+ def failed(id:, exception:)
102
+ ActiveRecord::Base.connection_pool.with_connection do
103
+ ActiveRecord::Base.transaction do
104
+ message = Models::Message.order(created_at: :asc).lock.find_by!(id: id)
105
+
106
+ if message.status != Models::Message::Status::PUBLISHING
107
+ raise ArgumentError,
108
+ "cannot transition outboxer message #{id} " \
109
+ "from #{message.status} to #{Models::Message::Status::FAILED}"
110
+ end
111
+
112
+ message.update!(
113
+ status: Models::Message::Status::FAILED,
114
+ updated_at: Time.now.utc)
115
+
116
+ outboxer_exception = message.exceptions.create!(
117
+ class_name: exception.class.name, message_text: exception.message)
118
+
119
+ exception.backtrace.each_with_index do |frame, index|
120
+ outboxer_exception.frames.create!(index: index, text: frame)
121
+ end
122
+
123
+ { id: id }
124
+ end
125
+ end
126
+ end
127
+
128
+ def delete(id:)
129
+ ActiveRecord::Base.connection_pool.with_connection do
130
+ ActiveRecord::Base.transaction do
131
+ message = Models::Message.includes(exceptions: :frames).lock.find_by!(id: id)
132
+
133
+ message.exceptions.each { |exception| exception.frames.each(&:delete) }
134
+ message.exceptions.delete_all
135
+ message.delete
136
+
137
+ { id: id }
138
+ end
139
+ end
140
+ end
141
+
142
+ def republish(id:)
143
+ ActiveRecord::Base.connection_pool.with_connection do
144
+ ActiveRecord::Base.transaction do
145
+ message = Models::Message.lock.find_by!(id: id)
146
+
147
+ message.update!(
148
+ status: Models::Message::Status::BACKLOGGED,
149
+ updated_at: Time.now.utc)
150
+
151
+ { id: id }
152
+ end
153
+ end
154
+ end
155
+
156
+ def stop_publishing
157
+ @publishing = false
158
+ end
159
+
160
+ def publish(threads: 5, queue: 10, poll: 1,
161
+ logger: Logger.new($stdout, level: Logger::INFO),
162
+ kernel: Kernel, &block)
163
+ ruby_queue = Queue.new
164
+
165
+ @publishing = true
166
+
167
+ logger.info "Initializing #{threads} worker threads"
168
+ ruby_threads = threads.times.map do
169
+ Thread.new do
170
+ loop do
171
+ begin
172
+ queued_message = ruby_queue.pop
173
+ break if queued_message.nil?
174
+
175
+ queued_at = queued_message[:queued_at]
176
+
177
+ message = Message.publishing(id: queued_message[:id])
178
+ logger.info "Publishing message #{message[:id]} for " \
179
+ "#{message[:messageable_type]}::#{message[:messageable_id]} " \
180
+ "in #{(Time.now.utc - queued_at).round(3)}s"
181
+
182
+ begin
183
+ block.call(message)
184
+ rescue Exception => exception
185
+ Message.failed(id: message[:id], exception: exception)
186
+ logger.error "Failed to publish message #{message[:id]} for " \
187
+ "#{message[:messageable_type]}::#{message[:messageable_id]} " \
188
+ "in #{(Time.now.utc - queued_at).round(3)}s"
189
+
190
+ raise
191
+ end
192
+
193
+ Message.published(id: message[:id])
194
+ logger.info "Published message #{message[:id]} for " \
195
+ "#{message[:messageable_type]}::#{message[:messageable_id]} " \
196
+ "in #{(Time.now.utc - queued_at).round(3)}s"
197
+ rescue StandardError => exception
198
+ logger.error "#{exception.class}: #{exception.message} " \
199
+ "in #{(Time.now.utc - queued_at).round(3)}s"
200
+
201
+ exception.backtrace.each { |frame| logger.error frame }
202
+ rescue Exception => exception
203
+ logger.fatal "#{exception.class}: #{exception.message} " \
204
+ "in #{(Time.now.utc - queued_at).round(3)}s"
205
+ exception.backtrace.each { |frame| logger.error frame }
206
+
207
+ @publishing = false
208
+
209
+ break
210
+ end
211
+ end
212
+ end
213
+ end
214
+ logger.info "Initialized #{threads} worker threads"
215
+
216
+ logger.info "Queuing up to #{queue} messages every #{poll}s"
217
+ while @publishing
218
+ begin
219
+ messages = []
220
+
221
+ queue_available = queue - ruby_queue.length
222
+ queue_stats = { total: queue, available: queue_available, current: ruby_queue.length }
223
+ logger.debug "Queue: #{queue_stats}"
224
+
225
+ logger.debug "Connection pool: #{ActiveRecord::Base.connection_pool.stat}"
226
+
227
+ messages = (queue_available > 0) ? Messages.queue(limit: queue_available) : []
228
+ logger.debug "Updated #{messages.length} messages from backlogged to queued"
229
+
230
+ messages.each do |message|
231
+ logger.info "Queuing message #{message[:id]} for " \
232
+ "#{message[:messageable_type]}::#{message[:messageable_id]}"
233
+
234
+ ruby_queue.push({ id: message[:id], queued_at: Time.now.utc })
235
+ end
236
+
237
+ logger.debug "Pushed #{messages.length} messages to queue."
238
+
239
+ if messages.empty?
240
+ logger.debug "Sleeping for #{poll} seconds because no messages were queued..."
241
+
242
+ kernel.sleep(poll)
243
+ elsif ruby_queue.length >= queue
244
+ logger.debug "Sleeping for #{poll} seconds because queue was full..."
245
+
246
+ kernel.sleep(poll)
247
+ end
248
+ rescue StandardError => exception
249
+ logger.error "#{exception.class}: #{exception.message}"
250
+ exception.backtrace.each { |frame| logger.error frame }
6
251
 
7
- STATUS = {
8
- unpublished: "unpublished",
9
- publishing: "publishing",
10
- failed: "failed"
11
- }.freeze
252
+ kernel.sleep(poll)
253
+ rescue Exception => exception
254
+ logger.fatal "#{exception.class}: #{exception.message}"
255
+ exception.backtrace.each { |frame| logger.fatal frame }
12
256
 
13
- belongs_to :message, polymorphic: true
257
+ @publishing = false
258
+ end
259
+ end
260
+ logger.info "Stopped queueing messages"
14
261
 
15
- has_many :exceptions, -> { order(created_at: :asc) },
16
- class_name: "::Outboxer::Exception",
17
- dependent: :destroy
262
+ logger.info "Shutting down #{threads} worker threads"
263
+ ruby_threads.length.times { ruby_queue.push(nil) }
264
+ ruby_threads.each(&:join)
265
+ logger.info "Shut down #{threads} worker threads"
266
+ end
18
267
  end
19
268
  end