Compare commits

...

40 Commits
0.1.3 ... main

Author SHA1 Message Date
Brusk Awat
8a64179e38
Merge pull request #8 from muhammadnawzad/main
Updates Dependancies
2026-01-18 10:39:04 +03:00
Muhammad Nawzad
f9bfeac427
refactor: remove metadata field from outboxable templates. 2026-01-18 10:06:03 +03:00
Muhammad Nawzad
46f56a4a08
refactor: remove metadata argument from outbox creation. 2026-01-18 10:04:30 +03:00
Muhammad Nawzad
4a6952f767
Upgrade Ruby to 4.0.1 in CI, update gem dependencies, and apply RuboCop configuration and style fixes. 2026-01-18 10:02:40 +03:00
Muhammad Nawzad
61767b40c2
Adds metadata to outboxes 2024-08-19 11:06:17 +03:00
Brusk Awat
08ef9242e5
Update README.md 2024-08-04 14:01:35 +03:00
40cbf3342a
Merge pull request #4 from muhammadnawzad/main
Indexes columns & adds `content_type` field
2024-05-28 14:22:41 +03:00
Muhammad Nawzad
816465eced
Refactors code for better readablity 2024-05-28 13:51:17 +03:00
Muhammad Nawzad
035a9822ee
Adds content_type configuration 2024-05-28 13:44:54 +03:00
Muhammad Nawzad
e5ebfb84d0
Add index on outboxes table for status and last_attempted_at 2024-04-02 14:50:42 +03:00
Muhammad Nawzad
de7c304524
Fixes initialize 2024-04-02 14:50:27 +03:00
faa35a5791
Merge pull request #3 from muhammadnawzad/main
Updates the variable rabbitmq_event_bus_exchange's name to rabbitmq_e…
2023-12-14 10:59:53 +03:00
Muhammad Nawzad
a8765e15f6
Updates .gitignore 2023-12-06 13:45:57 +03:00
Muhammad Nawzad
664adbb401
Bumps gem version to 1.0.6 2023-12-06 13:45:25 +03:00
Muhammad Nawzad
13b2013f3f
Removes extra argument 2023-12-06 13:45:08 +03:00
Muhammad Nawzad
4de6891945
Updates required gems' versions 2023-12-06 13:44:50 +03:00
Muhammad Nawzad
eb6de394bc
Merge branch 'ditkrg:main' into main 2023-12-06 13:34:18 +03:00
e486de9bb1
Bumps version 2023-04-25 12:39:34 +03:00
873f23ba59
Runs publish inline after commit 2023-04-25 12:39:20 +03:00
Muhammad Nawzad
5ef8d6a51d
Updates the variable rabbitmq_event_bus_exchange's name to rabbitmq_exchange_name 2023-04-18 10:42:55 +03:00
6f598f40d4
Fixes typo 2023-04-16 16:55:39 +03:00
3b545b0676
Updates documentation 2023-04-14 04:18:16 +03:00
92ee8d2eea
Major improvements 2023-04-14 04:10:57 +03:00
5a927b7c6a
Bumps version 2023-04-14 02:39:24 +03:00
e4c2638261
Improves mongoid model 2023-04-14 02:39:02 +03:00
bab6309502
Bug fixes and improvements 2023-04-14 02:38:32 +03:00
ee8dae21d5
Bumps version 2023-04-13 23:44:14 +03:00
a5153324d5
Separates polling publisher for mongoid from activerecord 2023-04-13 23:38:42 +03:00
64c23f9796
Uses respective orm templates 2023-04-13 22:11:13 +03:00
daf9b980ea
Separates mongoid templates from activerecord 2023-04-13 22:10:15 +03:00
aee1128c54
Adds mongo simple enum 2023-04-13 22:09:48 +03:00
ae5e53d5c0
Remove autosave: false 2023-04-13 20:58:47 +03:00
15e1c9011a
Adds circuit-breaking 2023-04-13 12:16:18 +03:00
3e10bd9768
Update version.rb 2023-04-13 12:12:32 +03:00
689d4effe8
Update outbox.rb 2023-04-13 12:12:14 +03:00
35496ea7b5
Bumps to 0.1.6 2023-04-11 14:09:19 +03:00
286d11984b
Adds transitionary state 2023-04-10 10:31:50 +03:00
aaf2f57d06
Bumps version 2023-04-06 17:18:32 +03:00
fe26826675
Removes increment 2023-04-06 17:17:35 +03:00
cf5f49691f
Adds initial 10 seconds as last_attempted_at so that it is not picked again by polling publisher 2023-04-06 17:17:28 +03:00
20 changed files with 317 additions and 112 deletions

View File

@ -14,14 +14,14 @@ jobs:
strategy:
matrix:
ruby:
- '3.1.2'
- "4.0.1"
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the default task
run: bundle exec rake
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Run the default task
run: bundle exec rake

3
.gitignore vendored
View File

@ -10,4 +10,5 @@
# rspec failure tracking
.rspec_status
*.gem
*.gem
.idea

View File

@ -1,4 +1,5 @@
require: rubocop-rails
plugins:
- rubocop-rails
AllCops:
NewCops: enable

12
Gemfile
View File

@ -5,12 +5,12 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in outboxable.gemspec
gemspec
gem 'rake', '~> 13.0'
gem 'rspec', '~> 3.0'
gem 'rubocop-rails', '~> 2.18'
gem 'rake', '~> 13.3.1'
gem 'rspec', '~> 3.13.2'
gem 'rubocop-rails', '~> 2.34.3'
group :development, :test do
gem 'activesupport', '~> 7.0'
gem 'sidekiq', '~> 7.0'
gem 'sidekiq-cron', '~> 1.10'
gem 'activesupport', '~> 8.1.2'
gem 'sidekiq', '~> 8.1.0'
gem 'sidekiq-cron', '~> 2.3.1'
end

View File

@ -1,107 +1,139 @@
PATH
remote: .
specs:
outboxable (0.1.3)
bunny (>= 2.19.0)
connection_pool (~> 2.3.0)
outboxable (1.0.6)
bunny (>= 2.22)
connection_pool (>= 2.4)
GEM
remote: https://rubygems.org/
specs:
activesupport (7.0.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
activesupport (8.1.2)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
json
logger (>= 1.4.2)
minitest (>= 5.1)
tzinfo (~> 2.0)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
amq-protocol (2.3.2)
ast (2.4.2)
bunny (2.20.3)
ast (2.4.3)
base64 (0.3.0)
bigdecimal (4.0.1)
bunny (2.22.0)
amq-protocol (~> 2.3, >= 2.3.1)
sorted_set (~> 1, >= 1.0.2)
concurrent-ruby (1.2.2)
connection_pool (2.3.0)
diff-lcs (1.5.0)
et-orbi (1.2.7)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
cronex (0.15.0)
tzinfo
fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7)
unicode (>= 0.4.4.5)
diff-lcs (1.6.2)
drb (2.2.3)
et-orbi (1.4.0)
tzinfo
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
globalid (1.1.0)
activesupport (>= 5.0)
i18n (1.12.0)
globalid (1.3.0)
activesupport (>= 6.1)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.6.3)
minitest (5.18.0)
parallel (1.22.1)
parser (3.2.2.0)
json (2.18.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
logger (1.7.0)
minitest (6.0.1)
prism (~> 1.5)
parallel (1.27.0)
parser (3.3.10.1)
ast (~> 2.4.1)
racc
prism (1.8.0)
raabro (1.4.0)
rack (2.2.6.4)
racc (1.8.1)
rack (3.2.4)
rainbow (3.1.1)
rake (13.0.6)
rake (13.3.1)
rbtree (0.4.6)
redis-client (0.14.1)
redis-client (0.26.3)
connection_pool
regexp_parser (2.7.0)
rexml (3.2.5)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.1)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
regexp_parser (2.11.3)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
rubocop (1.49.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.6)
rubocop (1.82.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.2.0.0)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.48.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.0)
parser (>= 3.2.1.0)
rubocop-rails (2.18.0)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-rails (2.34.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (1.13.0)
securerandom (0.4.1)
set (1.0.3)
sidekiq (7.0.7)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.11.0)
sidekiq-cron (1.10.0)
fugit (~> 1.8)
sidekiq (8.1.0)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
rack (>= 3.2.0)
redis-client (>= 0.26.0)
sidekiq-cron (2.3.1)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6)
sidekiq (>= 6.5.0)
sorted_set (1.0.3)
rbtree
set (~> 1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
unicode (0.4.4.5)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
PLATFORMS
arm64-darwin-25
x86_64-linux
DEPENDENCIES
activesupport (~> 7.0)
activesupport (~> 8.1.2)
outboxable!
rake (~> 13.0)
rspec (~> 3.0)
rubocop-rails (~> 2.18)
sidekiq (~> 7.0)
sidekiq-cron (~> 1.10)
rake (~> 13.3.1)
rspec (~> 3.13.2)
rubocop-rails (~> 2.34.3)
sidekiq (~> 8.1.0)
sidekiq-cron (~> 2.3.1)
BUNDLED WITH
2.4.7
2.4.17

View File

@ -1,6 +1,19 @@
# 🚨 Discontinuation Notice for ActiveRecord 🚨
**Effective Date: August 4, 2024**
Please be aware that we are no longer maintaing the part related to **ActiveRecord** in this gem. We are dropping support for ActiveRecord in favor of [Solid Queue](https://github.com/rails/solid_queue).
In the meantime, we commit to continously support the Mongoid part of the gem.
### New Recommended Gem: `Solid Queue`
For ActiveRecord users, we recommend transitioning to the `Solid Queue` gem, which provides enhanced functionality, improved performance, and better support for modern application requirements. `Solid Queue` is designed to seamlessly integrate with your existing infrastructure while offering robust features to handle your queuing needs efficiently.
# Outboxable
The Outboxable Gem is tailored for Rails applications to implement the transactional outbox pattern. It currently only supports ActiveRecord.
The Outboxable Gem is tailored for Rails applications to implement the transactional outbox pattern. It supports both ActiveRecord and Mongoid.
Please take into consideration that this Gem is **opinionated**, meaning it expects you to follow a certain pattern and specific setting. If you don't like it, you can always fork it and change it.
@ -25,13 +38,19 @@ If bundler is not being used to manage dependencies, install the gem by executin
$ gem install outboxable
```
Then run:
For use with ActiveRecord, run:
```shell
$ rails g outboxable:install
$ rails g outboxable:install --orm activerecord
```
The command above will add a migration file and the Outbox model. You will need then to run the migrations:
For use with Mongoid, run:
```shell
$ rails g outboxable:install --orm mongoid
```
The command above will add a migration file and the Outbox model. You will need then to run the migrations (ActiveRecord only):
```shell
$ rails db:migrate
@ -64,7 +83,7 @@ module Outboxable
end
Outboxable.configure do |config|
# Specify the ORM you are using. For now, only ActiveRecord is supported.
# Specify the ORM you are using. Supported values are :activerecord and :mongoid
config.orm = :activerecord
# Specify the message broker you are using. For now, only RabbitMQ is supported.
@ -76,7 +95,7 @@ Outboxable.configure do |config|
config.rabbitmq_user = ENV.fetch('RABBITMQ__USERNAME')
config.rabbitmq_password = ENV.fetch('RABBITMQ__PASSWORD')
config.rabbitmq_vhost = ENV.fetch('RABBITMQ__VHOST')
config.rabbitmq_event_bus_exchange = ENV.fetch('EVENTBUS__EXCHANGE_NAME')
config.rabbitmq_exchange_name = ENV.fetch('RABBITMQ__EXCHANGE_NAME')
end
```
@ -208,6 +227,12 @@ Last but not least, run sidekiq so that the Outboxable Gem can publish the event
$ bundle exec sidekiq
```
### Mongoid
The Outboxable gem works smoothly with Mongoid. It is to be noted that when used with Mongoid, Outboxable does not use the `_id` as the idempotency key. It creates a field called ``idempotency_key`` which is a UUID generated at the time of the insertion of the document.
## Development
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

View File

@ -3,24 +3,37 @@ module Outboxable
include Rails::Generators::Migration
source_root File.expand_path('../../templates', __dir__)
class_option :orm, type: :string, default: 'activerecord'
def initialize(*args)
super(*args) # rubocop:disable Style/SuperArguments
@orm = options[:orm] || 'activerecord'
%w[activerecord mongoid].include?(@orm) || raise(ArgumentError, 'Invalid ORM. Only ActiveRecord and Mongoid are supported.')
end
# Copy initializer into user app
def copy_initializer
copy_file('initializer.rb', 'config/initializers/z_outboxable.rb')
copy_file('activerecord_initializer.rb', 'config/initializers/z_outboxable.rb') if @orm == 'activerecord'
copy_file('mongoid_initializer.rb', 'config/initializers/z_outboxable.rb') if @orm == 'mongoid'
end
# Copy user information (model & Migrations) into user app
def create_user_model
target_path = 'app/models/outbox.rb'
if Rails.root.join(target_path).exist?
say_status('skipped', 'Model outbox already exists')
else
template('outbox.rb', target_path)
template('activerecrod_outbox.rb', target_path) if @orm == 'activerecord'
template('mongoid_outbox.rb', target_path) if @orm == 'mongoid'
end
end
# Copy migrations
def copy_migrations
return if @orm == 'mongoid'
if self.class.migration_exists?('db/migrate', 'create_outboxable_outboxes')
say_status('skipped', 'Migration create_outboxable_outboxes already exists')
else

View File

@ -3,14 +3,15 @@
require_relative 'outboxable/version'
require 'outboxable/worker'
require 'outboxable/publishing_manager'
require 'outboxable/polling_publisher_worker'
require 'outboxable/connection'
require 'outboxable/configuration'
require 'outboxable/rabbitmq/publisher'
require 'active_support/concern'
require 'outboxable/publishing_manager'
require 'outboxable/polling_publisher_worker'
module Outboxable
class Error < StandardError; end
@ -20,12 +21,12 @@ module Outboxable
after_create :instantiate_outbox_for_create, if: proc { |object| object.check_outbox_condition(object:, operation: :create) }
after_update :instantiate_outbox_for_update, if: proc { |object| object.check_outbox_condition(object:, operation: :update) }
has_many :outboxes, as: :outboxable, autosave: false
has_many :outboxes, as: :outboxable, dependent: :destroy
def instantiate_outbox(routing_key:)
outboxes.new(
routing_key:,
exchange: Outboxable.configuration.rabbitmq_event_bus_exchange,
exchange: Outboxable.configuration.rabbitmq_exchange_name,
payload: as_json
)
end

View File

@ -10,14 +10,14 @@ module Outboxable
class Configuration
ALLOWED_MESSAGE_BROKERS = %i[rabbitmq].freeze
ALLOWED_ORMS = %i[activerecord].freeze
ALLOWED_ORMS = %i[activerecord mongoid].freeze
attr_accessor :rabbitmq_host,
:rabbitmq_port,
:rabbitmq_user,
:rabbitmq_password,
:rabbitmq_vhost,
:rabbitmq_event_bus_exchange,
:rabbitmq_exchange_name,
:message_broker,
:orm

View File

@ -3,6 +3,7 @@ require 'singleton'
module Outboxable
class Connection
include ::Singleton
attr_reader :connection
def initialize

View File

@ -1,16 +1,27 @@
module Outboxable
class PollingPublisherWorker
include Sidekiq::Job
sidekiq_options queue: 'critical'
def perform
Outboxable.configuration.orm == :mongoid ? perform_mongoid : perform_activerecord
end
def perform_activerecord
Outbox.pending.where(last_attempted_at: [..Time.zone.now, nil]).find_in_batches(batch_size: 100).each do |batch|
batch.each do |outbox|
# This is to prevent a job from being retried too many times. Worst-case scenario is 1 minute delay in jobs.
outbox.update(last_attempted_at: 1.minute.from_now)
Outboxable::Worker.perform_async(outbox.id)
::Outboxable::Worker.perform_async(outbox.id)
outbox.update(last_attempted_at: 1.minute.from_now, status: :processing, allow_publish: false)
end
end
end
def perform_mongoid
Outbox.pending.any_of({ last_attempted_at: ..Time.zone.now }, { last_attempted_at: nil }).each do |outbox|
# This is to prevent a job from being retried too many times. Worst-case scenario is 1 minute delay in jobs.
::Outboxable::Worker.perform_async(outbox.idempotency_key)
outbox.update(last_attempted_at: 1.minute.from_now, status: :processing, allow_publish: false)
end
end
end
end

View File

@ -20,7 +20,12 @@ module Outboxable
exchange = channel.topic(@resource.exchange, durable: true)
# Publish the CloudEvent resource to the exchange
exchange.publish(to_envelope(resource: @resource), routing_key: @resource.routing_key, headers: @resource.try(:headers) || {})
exchange.publish(
to_envelope(resource: @resource),
routing_key: @resource.routing_key,
headers: @resource.try(:headers) || {},
content_type: @resource.try(:content_type) || 'application/json'
)
# Wait for confirmation
confirmed = channel.wait_for_confirms
@ -29,7 +34,6 @@ module Outboxable
return unless confirmed
@resource.reload
@resource.increment_attempt
@resource.update(status: :published, retry_at: nil)
end
end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
module Outboxable
VERSION = '0.1.3'
VERSION = '1.0.6'
end

View File

@ -5,7 +5,8 @@ module Outboxable
include ::Sidekiq::Job
def perform(outbox_id)
Outboxable::PublishingManager.publish(resource: Outbox.find(outbox_id))
Outboxable::PublishingManager.publish(resource: Outbox.find(outbox_id)) if Outboxable.configuration.orm == :activerecord
Outboxable::PublishingManager.publish(resource: Outbox.find_by!(idempotency_key: outbox_id)) if Outboxable.configuration.orm == :mongoid
end
end
end

View File

@ -23,7 +23,7 @@ Outboxable.configure do |config|
# Specify the ORM you are using. For now, only ActiveRecord is supported.
config.orm = :activerecord
# Specify the message broker you are using. For now, only RabbitMQ is supported.
# Specify the ORM you are using. Supported values are :activerecord and :mongoid
config.message_broker = :rabbitmq
# RabbitMQ configurations
@ -32,5 +32,5 @@ Outboxable.configure do |config|
config.rabbitmq_user = ENV.fetch('RABBITMQ__USERNAME')
config.rabbitmq_password = ENV.fetch('RABBITMQ__PASSWORD')
config.rabbitmq_vhost = ENV.fetch('RABBITMQ__VHOST')
config.rabbitmq_event_bus_exchange = ENV.fetch('EVENTBUS__EXCHANGE_NAME')
config.rabbitmq_exchange_name = ENV.fetch('RABBITMQ__EXCHANGE_NAME')
end

View File

@ -1,28 +1,31 @@
class Outbox < ApplicationRecord
attribute :allow_publish, :boolean, default: true
before_save :check_publishing
# Callbacks
before_create :set_last_attempted_at
before_save :check_publishing
after_commit :publish, if: :allow_publish?
# Enums
enum status: { pending: 0, published: 1, failed: 2 }
enum status: { pending: 0, processing: 1, published: 2, failed: 3 }
enum size: { single: 0, batch: 1 }
# Validations
validates :payload, presence: true
validates :exchange, presence: true
validates :routing_key, presence: true
validates :payload, :exchange, :routing_key, presence: true
# Associations
belongs_to :outboxable, polymorphic: true, optional: true
def set_last_attempted_at
self.last_attempted_at = Time.zone.now
self.last_attempted_at = 10.seconds.from_now
end
def publish
Outboxable::Worker.perform_async(id)
# Run this in own thread
threaded = Thread.new do
Outboxable::Worker.perform_inline(id)
update(status: :processing, last_attempted_at: 1.minute.from_now, allow_publish: false)
end
threaded.join
end
def check_publishing

View File

@ -7,6 +7,7 @@ class CreateOutboxableOutboxes < ActiveRecord::Migration[7.0]
t.string :exchange, null: false, default: ''
t.string :routing_key, null: false, default: ''
t.string :content_type, null: false, default: 'application/json'
t.integer :attempts, null: false, default: 0
t.datetime :last_attempted_at, null: true
@ -21,5 +22,7 @@ class CreateOutboxableOutboxes < ActiveRecord::Migration[7.0]
t.timestamps
end
add_index :outboxes, %i[status last_attempted_at], name: 'index_outboxes_on_outboxable_status_and_last_attempted_at'
end
end

View File

@ -0,0 +1,36 @@
# This monkey patch allows you to customize the message format that you publish to your broker.
# By default, Outboxable publishes a CloudEvent message to your broker.
module Outboxable
module RabbitMq
class Publisher
# Override this method to customize the message format that you publish to your broker
# DO NOT CHANGE THE METHOD SIGNATURE
def to_envelope(resource:)
{
id: resource.id,
source: 'http://localhost:3000',
specversion: '1.0',
type: resource.routing_key,
datacontenttype: 'application/json',
data: resource.payload
}.to_json
end
end
end
end
Outboxable.configure do |config|
# Specify the ORM you are using. For now, only ActiveRecord is supported.
config.orm = :mongoid
# Specify the ORM you are using. Supported values are :activerecord and :mongoid
config.message_broker = :rabbitmq
# RabbitMQ configurations
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
config.rabbitmq_port = ENV.fetch('RABBITMQ__PORT', 5672)
config.rabbitmq_user = ENV.fetch('RABBITMQ__USERNAME')
config.rabbitmq_password = ENV.fetch('RABBITMQ__PASSWORD')
config.rabbitmq_vhost = ENV.fetch('RABBITMQ__VHOST')
config.rabbitmq_exchange_name = ENV.fetch('RABBITMQ__EXCHANGE_NAME')
end

View File

@ -0,0 +1,72 @@
class Outbox
include Mongoid::Document
include Mongoid::Timestamps
attr_writer :allow_publish
# Fields
field :status, type: String, default: 'pending'
field :size, type: String, default: 'single'
field :exchange, type: String, default: ''
field :routing_key, type: String, default: ''
field :content_type, type: String, default: 'application/json'
field :attempts, type: Integer, default: 0
field :last_attempted_at, type: DateTime, default: nil
field :retry_at, type: DateTime, default: nil
field :idempotency_key, type: String
field :payload, type: Hash, default: {}
field :headers, type: Hash, default: {}
index({ idempotency_key: 1 }, { unique: true, name: 'idempotency_key_unique_index' })
before_save :check_publishing
before_create :set_idempotency_key
# Callbacks
before_create :set_last_attempted_at
after_save :publish, if: :allow_publish
# Validations
validates :payload, :exchange, :routing_key, presence: true
# Associations
belongs_to :outboxable, polymorphic: true, optional: true
def set_last_attempted_at
self.last_attempted_at = 10.seconds.from_now
end
def publish
Outboxable::Worker.perform_async(idempotency_key)
update(status: :processing, last_attempted_at: 1.minute.from_now, allow_publish: false)
end
def set_idempotency_key
self.idempotency_key = SecureRandom.uuid if idempotency_key.blank?
end
def check_publishing
self.allow_publish = false if published?
end
def allow_publish
return true if @allow_publish.nil?
@allow_publish
end
%w[pending processing published failed].each do |status|
define_method "#{status}?" do
self.status == status
end
# define scope
scope status, -> { where(status:) }
end
end

View File

@ -31,7 +31,8 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']
spec.add_dependency 'bunny', '>= 2.19.0'
spec.add_dependency 'connection_pool', '~> 2.3.0'
spec.add_dependency 'bunny', '>= 2.22'
spec.add_dependency 'connection_pool', '>= 2.4'
spec.metadata['rubygems_mfa_required'] = 'true'
end