Compare commits

...

65 Commits
v0.1.1 ... 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
02bd4dcc00
Fixes gems 2023-04-06 00:02:36 +03:00
b45eb3a0a7
Removes .gem 2023-04-06 00:01:32 +03:00
18aaa8bc26
Fixes rubocop offencses 2023-04-05 23:56:59 +03:00
e6f41a0cf9
Merge branch 'main' of github.com:broosk1993/outboxable 2023-04-05 23:54:26 +03:00
160641416e
Increases version 2023-04-05 23:52:07 +03:00
2647c47b53
Adds mechanism to avoid too many retries 2023-04-05 23:52:00 +03:00
fd69f23345
Cleans up sidekiq to avoid tests being run between tests 2023-04-05 23:51:44 +03:00
7bf316e309
Adds basic specs 2023-04-05 23:51:27 +03:00
6793ac10c5
Sets last_attempted_at at creation time to avoid null 2023-04-05 23:51:15 +03:00
723481af3d
Explicitly requires sidekiq 2023-04-05 23:50:56 +03:00
53e3f655e6
Explicitly requires active_support 2023-04-05 23:50:30 +03:00
6f93829d3c
Adds development gems 2023-04-05 23:50:15 +03:00
7a163b3d39
Merge pull request #2 from muhammadnawzad/patch-1
Updates Home Repository URL
2023-03-12 17:48:42 +03:00
Muhammad Nawzad
27852eb589
Merge branch 'main' into patch-1 2023-03-12 16:31:30 +03:00
Muhammad Nawzad
9b6e6e7102
Changes RABBITMQ__USER to RABBITMQ__USERNAME 2023-03-12 16:29:20 +03:00
c00d458ba5
Update version.rb 2023-03-12 15:45:14 +03:00
0213750ea3
Fixes disgusting bug 2023-03-12 15:44:50 +03:00
Muhammad Nawzad
51b03c5452
Removes activesupport and sidekiq-cron from the gemfile to allow users to add them manually to their gemfiles 2023-03-08 12:32:12 +03:00
Muhammad Nawzad
b229e07dc8
Changes required ruby version 3.1.2 2023-03-06 18:25:11 +03:00
Muhammad Nawzad
ba9028f2a9
Changes required ruby version from 2.6.0 to 3.0.0 2023-03-06 18:20:33 +03:00
Muhammad Nawzad
8c408fe7f6
Fixes Rubocop Offenses 2023-03-06 16:20:35 +03:00
Muhammad Nawzad
918d4f9700
Updates rubocop gem with rubocop-rails gem 2023-03-06 16:05:02 +03:00
Muhammad Nawzad
e08006e456
Requires sidekiq-cron and activesupport 2023-03-06 15:58:07 +03:00
Muhammad Nawzad
6d56b1f57a
Updates CHANGELOG URL 2023-03-06 14:59:07 +03:00
Muhammad Nawzad
0bf9f7c976
Updates Home Repository URL 2023-03-06 14:52:15 +03:00
24 changed files with 445 additions and 177 deletions

View File

@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
ruby:
- '3.1.2'
- "4.0.1"
steps:
- uses: actions/checkout@v3

3
.gitignore vendored
View File

@ -9,3 +9,6 @@
# rspec failure tracking
.rspec_status
*.gem
.idea

View File

@ -1,4 +1,5 @@
require: rubocop-rails
plugins:
- rubocop-rails
AllCops:
NewCops: enable
@ -111,3 +112,7 @@ Metrics/CyclomaticComplexity:
Max: 15
Metrics/PerceivedComplexity:
Max: 15
Lint/DuplicateMethods: # Disables duplicate methods warning
Enabled: false
Gemspec/RequiredRubyVersion: # Disables required ruby version warning
Enabled: false

14
Gemfile
View File

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

View File

@ -1,69 +1,139 @@
PATH
remote: .
specs:
outboxable (0.1.1)
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 (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)
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)
connection_pool (2.3.0)
diff-lcs (1.5.0)
json (2.6.3)
parallel (1.22.1)
parser (3.2.1.0)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
cronex (0.15.0)
tzinfo
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.3.0)
activesupport (>= 6.1)
i18n (1.14.8)
concurrent-ruby (~> 1.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)
racc (1.8.1)
rack (3.2.4)
rainbow (3.1.1)
rake (13.0.6)
rake (13.3.1)
rbtree (0.4.6)
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)
redis-client (0.26.3)
connection_pool
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.3)
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.45.1)
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.24.1, < 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.26.0)
parser (>= 3.2.1.0)
ruby-progressbar (1.11.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.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 (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.5.0)
sorted_set (1.0.3)
rbtree
set (~> 1.0)
unicode-display_width (2.4.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
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 (~> 8.1.2)
outboxable!
rake (~> 13.0)
rspec (~> 3.0)
rubocop (~> 1.21)
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.2
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.
@ -73,10 +92,10 @@ Outboxable.configure do |config|
# RabbitMQ configurations
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
config.rabbitmq_port = ENV.fetch('RABBITMQ__PORT', 5672)
config.rabbitmq_user = ENV.fetch('RABBITMQ__USER')
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

@ -1,11 +1,11 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
require "rubocop/rake_task"
require 'rubocop/rake_task'
RuboCop::RakeTask.new

View File

@ -2,35 +2,48 @@ module Outboxable
class InstallGenerator < Rails::Generators::Base
include Rails::Generators::Migration
source_root File.expand_path('../../../templates', __FILE__)
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"
unless File.exist?(File.join(Rails.root, target_path))
template("outbox.rb", target_path)
target_path = 'app/models/outbox.rb'
if Rails.root.join(target_path).exist?
say_status('skipped', 'Model outbox already exists')
else
say_status('skipped', "Model outbox already exists")
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
if self.class.migration_exists?('db/migrate', "create_outboxable_outboxes")
say_status('skipped', "Migration create_outboxable_outboxes already exists")
return if @orm == 'mongoid'
if self.class.migration_exists?('db/migrate', 'create_outboxable_outboxes')
say_status('skipped', 'Migration create_outboxable_outboxes already exists')
else
migration_template('create_outboxable_outboxes.rb', "db/migrate/create_outboxable_outboxes.rb")
migration_template('create_outboxable_outboxes.rb', 'db/migrate/create_outboxable_outboxes.rb')
end
end
# Use to assign migration time otherwise generator will error
def self.next_migration_number(dir)
Time.now.utc.strftime("%Y%m%d%H%M%S")
def self.next_migration_number(_dir)
Time.now.utc.strftime('%Y%m%d%H%M%S')
end
end
end

View File

@ -1,14 +1,16 @@
# frozen_string_literal: true
require_relative "outboxable/version"
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
@ -19,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,15 +3,16 @@ require 'singleton'
module Outboxable
class Connection
include ::Singleton
attr_reader :connection
def initialize
@connection = Bunny.new(
host: RabbitCarrots.configuration.rabbitmq_host,
port: RabbitCarrots.configuration.rabbitmq_port,
user: RabbitCarrots.configuration.rabbitmq_user,
password: RabbitCarrots.configuration.rabbitmq_password,
vhost: RabbitCarrots.configuration.rabbitmq_vhost
host: Outboxable.configuration.rabbitmq_host,
port: Outboxable.configuration.rabbitmq_port,
user: Outboxable.configuration.rabbitmq_user,
password: Outboxable.configuration.rabbitmq_password,
vhost: Outboxable.configuration.rabbitmq_vhost
)
@connection.start

View File

@ -1,13 +1,26 @@
module Outboxable
class PollingPublisherWorker
include Sidekiq::Job
sidekiq_options queue: 'critical'
def perform
Outbox.pending.find_in_batches(batch_size: 100).each do |batch|
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|
Outboxable::Worker.perform_async(outbox.id)
end
# 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.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

View File

@ -7,7 +7,7 @@ module Outboxable
def to_envelope(resource:)
# throw not implemented method error
raise NotImplementedError, "Please implement the to_envelope method in your own module"
raise NotImplementedError, 'Please implement the to_envelope method in your own module'
end
def publish
@ -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.1"
VERSION = '1.0.6'
end

View File

@ -1,9 +1,12 @@
require 'sidekiq'
module Outboxable
class Worker
include Sidekiq::Job
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,14 +23,14 @@ 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
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
config.rabbitmq_port = ENV.fetch('RABBITMQ__PORT', 5672)
config.rabbitmq_user = ENV.fetch('RABBITMQ__USER')
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

@ -0,0 +1,34 @@
class Outbox < ApplicationRecord
attribute :allow_publish, :boolean, default: true
before_save :check_publishing
# Callbacks
before_create :set_last_attempted_at
after_commit :publish, if: :allow_publish?
# Enums
enum status: { pending: 0, processing: 1, published: 2, failed: 3 }
enum size: { single: 0, batch: 1 }
# 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
# 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
self.allow_publish = false if published?
end
end

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

@ -1,32 +0,0 @@
class Outbox < ApplicationRecord
attribute :allow_publish, :boolean, default: true
# Callbacks
after_commit :publish, if: :allow_publish?
before_save :check_publishing
# Enums
enum status: { pending: 0, published: 1, failed: 2 }
enum size: { single: 0, batch: 1 }
# Validations
validates :payload, presence: true
validates :exchange, presence: true
validates :routing_key, presence: true
# Associations
belongs_to :outboxable, polymorphic: true, optional: true
def increment_attempt
self.attempts = attempts + 1
self.last_attempted_at = Time.zone.now
end
def publish
Outboxable::Worker.perform_async(id)
end
def check_publishing
self.allow_publish = false if published?
end
end

View File

@ -1,24 +1,24 @@
# frozen_string_literal: true
require_relative "lib/outboxable/version"
require_relative 'lib/outboxable/version'
Gem::Specification.new do |spec|
spec.name = "outboxable"
spec.name = 'outboxable'
spec.version = Outboxable::VERSION
spec.authors = ["Brusk Awat"]
spec.email = ["broosk.edogawa@gmail.com"]
spec.authors = ['Brusk Awat']
spec.email = ['broosk.edogawa@gmail.com']
spec.summary = "An opiniated Gem for Rails applications to implement the transactional outbox pattern."
spec.description = "The Outboxable Gem is tailored for Rails applications to implement the transactional outbox pattern. It currently only supports ActiveRecord."
spec.homepage = "https://githuh.com/broosk1993/outboxable"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
spec.summary = 'An opiniated Gem for Rails applications to implement the transactional outbox pattern.'
spec.description = 'The Outboxable Gem is tailored for Rails applications to implement the transactional outbox pattern. It currently only supports ActiveRecord.'
spec.homepage = 'https://github.com/broosk1993/outboxable'
spec.license = 'MIT'
spec.required_ruby_version = '>= 3.1.2'
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://githuh.com/broosk1993/outboxable"
spec.metadata["changelog_uri"] = "https://githuh.com/broosk1993/outboxable/CHANGELOG.md"
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = 'https://github.com/broosk1993/outboxable'
spec.metadata['changelog_uri'] = 'https://github.com/broosk1993/outboxable/blob/main/CHANGELOG.md'
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@ -27,10 +27,12 @@ Gem::Specification.new do |spec|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
end
end
spec.bindir = "exe"
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
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

View File

@ -1,11 +1,17 @@
# frozen_string_literal: true
require 'sidekiq/testing'
RSpec.describe Outboxable do
it "has a version number" do
it 'has a version number' do
expect(Outboxable::VERSION).not_to be nil
end
it "does something useful" do
expect(false).to eq(true)
context 'polling publisher sidekiq worker' do
it 'should be able to perform' do
expect do
Outboxable::PollingPublisherWorker.perform_async
end.to change(Outboxable::PollingPublisherWorker.jobs, :size).by(1)
end
end
end

View File

@ -1,10 +1,10 @@
# frozen_string_literal: true
require "outboxable"
require 'outboxable'
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
config.example_status_persistence_file_path = '.rspec_status'
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
@ -12,4 +12,8 @@ RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
config.before(:each) do
Sidekiq::Worker.clear_all
end
end