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