mirror of
https://github.com/ditkrg/outboxable.git
synced 2026-01-22 22:06:47 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a64179e38 | ||
|
|
f9bfeac427 | ||
|
|
46f56a4a08 | ||
|
|
4a6952f767 | ||
|
|
61767b40c2 | ||
|
|
08ef9242e5 | ||
| 40cbf3342a | |||
|
|
816465eced | ||
|
|
035a9822ee | ||
|
|
e5ebfb84d0 | ||
|
|
de7c304524 | ||
| faa35a5791 | |||
|
|
a8765e15f6 | ||
|
|
664adbb401 | ||
|
|
13b2013f3f | ||
|
|
4de6891945 | ||
|
|
eb6de394bc | ||
| e486de9bb1 | |||
| 873f23ba59 | |||
|
|
5ef8d6a51d | ||
| 6f598f40d4 | |||
| 3b545b0676 | |||
| 92ee8d2eea | |||
| 5a927b7c6a | |||
| e4c2638261 | |||
| bab6309502 | |||
| ee8dae21d5 | |||
| a5153324d5 | |||
| 64c23f9796 | |||
| daf9b980ea | |||
| aee1128c54 | |||
| ae5e53d5c0 | |||
| 15e1c9011a | |||
| 3e10bd9768 | |||
| 689d4effe8 | |||
| 35496ea7b5 | |||
| 286d11984b | |||
| aaf2f57d06 | |||
| fe26826675 | |||
| cf5f49691f | |||
| 02bd4dcc00 | |||
| b45eb3a0a7 | |||
| 18aaa8bc26 | |||
| e6f41a0cf9 | |||
| 160641416e | |||
| 2647c47b53 | |||
| fd69f23345 | |||
| 7bf316e309 | |||
| 6793ac10c5 | |||
| 723481af3d | |||
| 53e3f655e6 | |||
| 6f93829d3c | |||
| 7a163b3d39 | |||
|
|
27852eb589 | ||
|
|
9b6e6e7102 | ||
| c00d458ba5 | |||
| 0213750ea3 | |||
|
|
51b03c5452 | ||
|
|
b229e07dc8 | ||
|
|
ba9028f2a9 | ||
|
|
8c408fe7f6 | ||
|
|
918d4f9700 | ||
|
|
e08006e456 | ||
|
|
6d56b1f57a | ||
|
|
0bf9f7c976 |
18
.github/workflows/main.yml
vendored
18
.github/workflows/main.yml
vendored
@ -14,14 +14,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ruby:
|
ruby:
|
||||||
- '3.1.2'
|
- "4.0.1"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby }}
|
ruby-version: ${{ matrix.ruby }}
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
- name: Run the default task
|
- name: Run the default task
|
||||||
run: bundle exec rake
|
run: bundle exec rake
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,3 +9,6 @@
|
|||||||
|
|
||||||
# rspec failure tracking
|
# rspec failure tracking
|
||||||
.rspec_status
|
.rspec_status
|
||||||
|
|
||||||
|
*.gem
|
||||||
|
.idea
|
||||||
|
|||||||
11
.rubocop.yml
11
.rubocop.yml
@ -1,11 +1,12 @@
|
|||||||
require: rubocop-rails
|
plugins:
|
||||||
|
- rubocop-rails
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
NewCops: enable
|
NewCops: enable
|
||||||
SuggestExtensions: false
|
SuggestExtensions: false
|
||||||
Layout/SpaceBeforeBrackets: # (new in 1.7)
|
Layout/SpaceBeforeBrackets: # (new in 1.7)
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Max: 350
|
Max: 350
|
||||||
Lint/AmbiguousAssignment: # (new in 1.7)
|
Lint/AmbiguousAssignment: # (new in 1.7)
|
||||||
Enabled: true
|
Enabled: true
|
||||||
@ -110,4 +111,8 @@ Metrics/MethodLength:
|
|||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 15
|
Max: 15
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 15
|
Max: 15
|
||||||
|
Lint/DuplicateMethods: # Disables duplicate methods warning
|
||||||
|
Enabled: false
|
||||||
|
Gemspec/RequiredRubyVersion: # Disables required ruby version warning
|
||||||
|
Enabled: false
|
||||||
|
|||||||
14
Gemfile
14
Gemfile
@ -1,12 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source "https://rubygems.org"
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
# Specify your gem's dependencies in outboxable.gemspec
|
# Specify your gem's dependencies in outboxable.gemspec
|
||||||
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"
|
group :development, :test do
|
||||||
|
gem 'activesupport', '~> 8.1.2'
|
||||||
gem "rubocop", "~> 1.21"
|
gem 'sidekiq', '~> 8.1.0'
|
||||||
|
gem 'sidekiq-cron', '~> 2.3.1'
|
||||||
|
end
|
||||||
|
|||||||
146
Gemfile.lock
146
Gemfile.lock
@ -1,69 +1,139 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
outboxable (0.1.1)
|
outboxable (1.0.6)
|
||||||
bunny (>= 2.19.0)
|
bunny (>= 2.22)
|
||||||
connection_pool (~> 2.3.0)
|
connection_pool (>= 2.4)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
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)
|
amq-protocol (2.3.2)
|
||||||
ast (2.4.2)
|
ast (2.4.3)
|
||||||
bunny (2.20.3)
|
base64 (0.3.0)
|
||||||
|
bigdecimal (4.0.1)
|
||||||
|
bunny (2.22.0)
|
||||||
amq-protocol (~> 2.3, >= 2.3.1)
|
amq-protocol (~> 2.3, >= 2.3.1)
|
||||||
sorted_set (~> 1, >= 1.0.2)
|
sorted_set (~> 1, >= 1.0.2)
|
||||||
connection_pool (2.3.0)
|
concurrent-ruby (1.3.6)
|
||||||
diff-lcs (1.5.0)
|
connection_pool (3.0.2)
|
||||||
json (2.6.3)
|
cronex (0.15.0)
|
||||||
parallel (1.22.1)
|
tzinfo
|
||||||
parser (3.2.1.0)
|
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)
|
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)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.3.1)
|
||||||
rbtree (0.4.6)
|
rbtree (0.4.6)
|
||||||
regexp_parser (2.7.0)
|
redis-client (0.26.3)
|
||||||
rexml (3.2.5)
|
connection_pool
|
||||||
rspec (3.12.0)
|
regexp_parser (2.11.3)
|
||||||
rspec-core (~> 3.12.0)
|
rspec (3.13.2)
|
||||||
rspec-expectations (~> 3.12.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-mocks (~> 3.12.0)
|
rspec-expectations (~> 3.13.0)
|
||||||
rspec-core (3.12.1)
|
rspec-mocks (~> 3.13.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-core (3.13.6)
|
||||||
rspec-expectations (3.12.2)
|
rspec-support (~> 3.13.0)
|
||||||
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.12.3)
|
rspec-mocks (3.13.7)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-support (3.12.0)
|
rspec-support (3.13.6)
|
||||||
rubocop (1.45.1)
|
rubocop (1.82.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.0.0)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.48.0, < 2.0)
|
||||||
rubocop-ast (>= 1.24.1, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.26.0)
|
rubocop-ast (1.49.0)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.3.7.2)
|
||||||
ruby-progressbar (1.11.0)
|
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)
|
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)
|
sorted_set (1.0.3)
|
||||||
rbtree
|
rbtree
|
||||||
set (~> 1.0)
|
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
|
PLATFORMS
|
||||||
|
arm64-darwin-25
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
activesupport (~> 8.1.2)
|
||||||
outboxable!
|
outboxable!
|
||||||
rake (~> 13.0)
|
rake (~> 13.3.1)
|
||||||
rspec (~> 3.0)
|
rspec (~> 3.13.2)
|
||||||
rubocop (~> 1.21)
|
rubocop-rails (~> 2.34.3)
|
||||||
|
sidekiq (~> 8.1.0)
|
||||||
|
sidekiq-cron (~> 2.3.1)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.2
|
2.4.17
|
||||||
|
|||||||
51
README.md
51
README.md
@ -1,12 +1,25 @@
|
|||||||
|
# 🚨 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
|
# 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.
|
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.
|
||||||
|
|
||||||
### Restrictions
|
### Restrictions
|
||||||
|
|
||||||
1. When using RabbitMQ, it only publishes events to a ***topic*** exchange.
|
1. When using RabbitMQ, it only publishes events to a ***topic*** exchange.
|
||||||
1. It assumes that you are using routing keys to publish to the topic exchange.
|
1. It assumes that you are using routing keys to publish to the topic exchange.
|
||||||
1. It publishes events in a background job using [Sidekiq](https://github.com/sidekiq/sidekiq). Therefore, you application must use Sidekiq.
|
1. It publishes events in a background job using [Sidekiq](https://github.com/sidekiq/sidekiq). Therefore, you application must use Sidekiq.
|
||||||
1. It implements the [polling publisher pattern](https://microservices.io/patterns/data/polling-publisher.html). For that, it uses [sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron) to check the unpublished outboxes every 5 seconds after the initialization of the application.
|
1. It implements the [polling publisher pattern](https://microservices.io/patterns/data/polling-publisher.html). For that, it uses [sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron) to check the unpublished outboxes every 5 seconds after the initialization of the application.
|
||||||
@ -25,13 +38,19 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|||||||
$ gem install outboxable
|
$ gem install outboxable
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run:
|
For use with ActiveRecord, run:
|
||||||
|
|
||||||
```shell
|
```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
|
```shell
|
||||||
$ rails db:migrate
|
$ rails db:migrate
|
||||||
@ -42,7 +61,7 @@ $ rails db:migrate
|
|||||||
The installation command above will also add a configuration file to your initializer:
|
The installation command above will also add a configuration file to your initializer:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# This monkey patch allows you to customize the message format that you publish to your broker.
|
# 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.
|
# By default, Outboxable publishes a CloudEvent message to your broker.
|
||||||
module Outboxable
|
module Outboxable
|
||||||
module RabbitMq
|
module RabbitMq
|
||||||
@ -64,7 +83,7 @@ module Outboxable
|
|||||||
end
|
end
|
||||||
|
|
||||||
Outboxable.configure do |config|
|
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
|
config.orm = :activerecord
|
||||||
|
|
||||||
# Specify the message broker you are using. For now, only RabbitMQ is supported.
|
# Specify the message broker you are using. For now, only RabbitMQ is supported.
|
||||||
@ -73,14 +92,14 @@ Outboxable.configure do |config|
|
|||||||
# RabbitMQ configurations
|
# RabbitMQ configurations
|
||||||
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
|
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
|
||||||
config.rabbitmq_port = ENV.fetch('RABBITMQ__PORT', 5672)
|
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_password = ENV.fetch('RABBITMQ__PASSWORD')
|
||||||
config.rabbitmq_vhost = ENV.fetch('RABBITMQ__VHOST')
|
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
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The monkey patch in the code above is crucial in giving you a way to customize the format of the message that you will publish to the message broker. Be default, it follows the specs of the [Cloud Native Events Specifications v1.0.2](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md).
|
The monkey patch in the code above is crucial in giving you a way to customize the format of the message that you will publish to the message broker. Be default, it follows the specs of the [Cloud Native Events Specifications v1.0.2](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -155,7 +174,7 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
The ``outbox_configurations`` method will be called and used by the Outboxable Gem to transactionally create an outbox and publish. In the code above, it will create an outbox when the book is created. For that purpose it will use the routing key ``books.created`` as a convention. It will also publish an event if the book is updated, using the routing key: ``books.published`` since it was specified in the hash.
|
The ``outbox_configurations`` method will be called and used by the Outboxable Gem to transactionally create an outbox and publish. In the code above, it will create an outbox when the book is created. For that purpose it will use the routing key ``books.created`` as a convention. It will also publish an event if the book is updated, using the routing key: ``books.published`` since it was specified in the hash.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -196,18 +215,24 @@ Here's the schema of what could be passed to the ``outbox_configurations`` in JS
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
The ``run_on`` key represents another hash that can have the keys ``create`` and ``update``. If one of these keys are not supplied, the outbox will not be created for the unspecified operation; in other words, if you do not specify the configuration for ``update``, for example, an outbox will NOT be created when the book is updated.
|
The ``run_on`` key represents another hash that can have the keys ``create`` and ``update``. If one of these keys are not supplied, the outbox will not be created for the unspecified operation; in other words, if you do not specify the configuration for ``update``, for example, an outbox will NOT be created when the book is updated.
|
||||||
|
|
||||||
Each operation key such as ``create`` and ``update`` can also take a ``condition`` key, which represents a Ruby proc that must return a boolean expression. It can also take a ``routing_key`` option, which specifies that routing key with which the outbox will publish the event to the message broker. If you don't specify the ``routing_key``, it will use the base`s routing key dotted by``created`` for create operation and ``updated`` for update operation.
|
Each operation key such as ``create`` and ``update`` can also take a ``condition`` key, which represents a Ruby proc that must return a boolean expression. It can also take a ``routing_key`` option, which specifies that routing key with which the outbox will publish the event to the message broker. If you don't specify the ``routing_key``, it will use the base`s routing key dotted by``created`` for create operation and ``updated`` for update operation.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Last but not least, run sidekiq so that the Outboxable Gem can publish the events to the broker:
|
Last but not least, run sidekiq so that the Outboxable Gem can publish the events to the broker:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ bundle exec sidekiq
|
$ 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
|
## 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.
|
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.
|
||||||
|
|||||||
6
Rakefile
6
Rakefile
@ -1,11 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "bundler/gem_tasks"
|
require 'bundler/gem_tasks'
|
||||||
require "rspec/core/rake_task"
|
require 'rspec/core/rake_task'
|
||||||
|
|
||||||
RSpec::Core::RakeTask.new(:spec)
|
RSpec::Core::RakeTask.new(:spec)
|
||||||
|
|
||||||
require "rubocop/rake_task"
|
require 'rubocop/rake_task'
|
||||||
|
|
||||||
RuboCop::RakeTask.new
|
RuboCop::RakeTask.new
|
||||||
|
|
||||||
|
|||||||
@ -2,35 +2,48 @@ module Outboxable
|
|||||||
class InstallGenerator < Rails::Generators::Base
|
class InstallGenerator < Rails::Generators::Base
|
||||||
include Rails::Generators::Migration
|
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
|
# Copy initializer into user app
|
||||||
def copy_initializer
|
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
|
end
|
||||||
|
|
||||||
# Copy user information (model & Migrations) into user app
|
# Copy user information (model & Migrations) into user app
|
||||||
def create_user_model
|
def create_user_model
|
||||||
target_path = "app/models/outbox.rb"
|
target_path = 'app/models/outbox.rb'
|
||||||
unless File.exist?(File.join(Rails.root, target_path))
|
|
||||||
template("outbox.rb", target_path)
|
if Rails.root.join(target_path).exist?
|
||||||
|
say_status('skipped', 'Model outbox already exists')
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# Copy migrations
|
# Copy migrations
|
||||||
def copy_migrations
|
def copy_migrations
|
||||||
if self.class.migration_exists?('db/migrate', "create_outboxable_outboxes")
|
return if @orm == 'mongoid'
|
||||||
say_status('skipped', "Migration create_outboxable_outboxes already exists")
|
|
||||||
|
if self.class.migration_exists?('db/migrate', 'create_outboxable_outboxes')
|
||||||
|
say_status('skipped', 'Migration create_outboxable_outboxes already exists')
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# Use to assign migration time otherwise generator will error
|
# Use to assign migration time otherwise generator will error
|
||||||
def self.next_migration_number(dir)
|
def self.next_migration_number(_dir)
|
||||||
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
Time.now.utc.strftime('%Y%m%d%H%M%S')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,30 +1,32 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative "outboxable/version"
|
require_relative 'outboxable/version'
|
||||||
|
|
||||||
require 'outboxable/worker'
|
require 'outboxable/worker'
|
||||||
require 'outboxable/publishing_manager'
|
|
||||||
require 'outboxable/polling_publisher_worker'
|
|
||||||
require 'outboxable/connection'
|
require 'outboxable/connection'
|
||||||
require 'outboxable/configuration'
|
require 'outboxable/configuration'
|
||||||
require 'outboxable/rabbitmq/publisher'
|
require 'outboxable/rabbitmq/publisher'
|
||||||
|
|
||||||
|
require 'active_support/concern'
|
||||||
|
|
||||||
|
require 'outboxable/publishing_manager'
|
||||||
|
require 'outboxable/polling_publisher_worker'
|
||||||
|
|
||||||
module Outboxable
|
module Outboxable
|
||||||
class Error < StandardError; end
|
class Error < StandardError; end
|
||||||
|
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
after_create :instantiate_outbox_for_create, if: proc { |object| object.check_outbox_condition(object:, operation: :create) }
|
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) }
|
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: )
|
def instantiate_outbox(routing_key:)
|
||||||
outboxes.new(
|
outboxes.new(
|
||||||
routing_key:,
|
routing_key:,
|
||||||
exchange: Outboxable.configuration.rabbitmq_event_bus_exchange,
|
exchange: Outboxable.configuration.rabbitmq_exchange_name,
|
||||||
payload: as_json
|
payload: as_json
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -10,14 +10,14 @@ module Outboxable
|
|||||||
|
|
||||||
class Configuration
|
class Configuration
|
||||||
ALLOWED_MESSAGE_BROKERS = %i[rabbitmq].freeze
|
ALLOWED_MESSAGE_BROKERS = %i[rabbitmq].freeze
|
||||||
ALLOWED_ORMS = %i[activerecord].freeze
|
ALLOWED_ORMS = %i[activerecord mongoid].freeze
|
||||||
|
|
||||||
attr_accessor :rabbitmq_host,
|
attr_accessor :rabbitmq_host,
|
||||||
:rabbitmq_port,
|
:rabbitmq_port,
|
||||||
:rabbitmq_user,
|
:rabbitmq_user,
|
||||||
:rabbitmq_password,
|
:rabbitmq_password,
|
||||||
:rabbitmq_vhost,
|
:rabbitmq_vhost,
|
||||||
:rabbitmq_event_bus_exchange,
|
:rabbitmq_exchange_name,
|
||||||
:message_broker,
|
:message_broker,
|
||||||
:orm
|
:orm
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ module Outboxable
|
|||||||
Sidekiq::Options[:cron_poll_interval] = 5
|
Sidekiq::Options[:cron_poll_interval] = 5
|
||||||
|
|
||||||
# Create the cron job for the polling publisher
|
# Create the cron job for the polling publisher
|
||||||
Sidekiq::Cron::Job.create(name: 'OutboxablePollingPublisher', cron: '*/5 * * * * *', class: 'Outboxable::PollingPublisherWorker')
|
Sidekiq::Cron::Job.create(name: 'OutboxablePollingPublisher', cron: '*/5 * * * * *', class: 'Outboxable::PollingPublisherWorker')
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_broker=(message_broker)
|
def message_broker=(message_broker)
|
||||||
|
|||||||
@ -3,15 +3,16 @@ require 'singleton'
|
|||||||
module Outboxable
|
module Outboxable
|
||||||
class Connection
|
class Connection
|
||||||
include ::Singleton
|
include ::Singleton
|
||||||
|
|
||||||
attr_reader :connection
|
attr_reader :connection
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@connection = Bunny.new(
|
@connection = Bunny.new(
|
||||||
host: RabbitCarrots.configuration.rabbitmq_host,
|
host: Outboxable.configuration.rabbitmq_host,
|
||||||
port: RabbitCarrots.configuration.rabbitmq_port,
|
port: Outboxable.configuration.rabbitmq_port,
|
||||||
user: RabbitCarrots.configuration.rabbitmq_user,
|
user: Outboxable.configuration.rabbitmq_user,
|
||||||
password: RabbitCarrots.configuration.rabbitmq_password,
|
password: Outboxable.configuration.rabbitmq_password,
|
||||||
vhost: RabbitCarrots.configuration.rabbitmq_vhost
|
vhost: Outboxable.configuration.rabbitmq_vhost
|
||||||
)
|
)
|
||||||
|
|
||||||
@connection.start
|
@connection.start
|
||||||
|
|||||||
@ -1,14 +1,27 @@
|
|||||||
module Outboxable
|
module Outboxable
|
||||||
class PollingPublisherWorker
|
class PollingPublisherWorker
|
||||||
include Sidekiq::Job
|
include Sidekiq::Job
|
||||||
sidekiq_options queue: 'critical'
|
|
||||||
|
|
||||||
def perform
|
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|
|
batch.each do |outbox|
|
||||||
Outboxable::Worker.perform_async(outbox.id)
|
# 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
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@ -4,34 +4,38 @@ module Outboxable
|
|||||||
def initialize(resource:)
|
def initialize(resource:)
|
||||||
@resource = resource
|
@resource = resource
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_envelope(resource:)
|
def to_envelope(resource:)
|
||||||
# throw not implemented method error
|
# 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
|
end
|
||||||
|
|
||||||
def publish
|
def publish
|
||||||
confirmed = nil
|
confirmed = nil
|
||||||
|
|
||||||
Outboxable::Connection.instance.channel.with do |channel|
|
Outboxable::Connection.instance.channel.with do |channel|
|
||||||
channel.confirm_select
|
channel.confirm_select
|
||||||
|
|
||||||
# Declare a exchange
|
# Declare a exchange
|
||||||
exchange = channel.topic(@resource.exchange, durable: true)
|
exchange = channel.topic(@resource.exchange, durable: true)
|
||||||
|
|
||||||
# Publish the CloudEvent resource to the exchange
|
# 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
|
# Wait for confirmation
|
||||||
confirmed = channel.wait_for_confirms
|
confirmed = channel.wait_for_confirms
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless confirmed
|
return unless confirmed
|
||||||
|
|
||||||
@resource.reload
|
@resource.reload
|
||||||
@resource.increment_attempt
|
|
||||||
@resource.update(status: :published, retry_at: nil)
|
@resource.update(status: :published, retry_at: nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Outboxable
|
module Outboxable
|
||||||
VERSION = "0.1.1"
|
VERSION = '1.0.6'
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
require 'sidekiq'
|
||||||
|
|
||||||
module Outboxable
|
module Outboxable
|
||||||
class Worker
|
class Worker
|
||||||
include Sidekiq::Job
|
include ::Sidekiq::Job
|
||||||
|
|
||||||
def perform(outbox_id)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# This monkey patch allows you to customize the message format that you publish to your broker.
|
# 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.
|
# By default, Outboxable publishes a CloudEvent message to your broker.
|
||||||
module Outboxable
|
module Outboxable
|
||||||
module RabbitMq
|
module RabbitMq
|
||||||
@ -23,14 +23,14 @@ Outboxable.configure do |config|
|
|||||||
# Specify the ORM you are using. For now, only ActiveRecord is supported.
|
# Specify the ORM you are using. For now, only ActiveRecord is supported.
|
||||||
config.orm = :activerecord
|
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
|
config.message_broker = :rabbitmq
|
||||||
|
|
||||||
# RabbitMQ configurations
|
# RabbitMQ configurations
|
||||||
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
|
config.rabbitmq_host = ENV.fetch('RABBITMQ__HOST')
|
||||||
config.rabbitmq_port = ENV.fetch('RABBITMQ__PORT', 5672)
|
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_password = ENV.fetch('RABBITMQ__PASSWORD')
|
||||||
config.rabbitmq_vhost = ENV.fetch('RABBITMQ__VHOST')
|
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
|
end
|
||||||
34
lib/templates/activerecrod_outbox.rb
Normal file
34
lib/templates/activerecrod_outbox.rb
Normal 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
|
||||||
@ -7,6 +7,7 @@ class CreateOutboxableOutboxes < ActiveRecord::Migration[7.0]
|
|||||||
|
|
||||||
t.string :exchange, null: false, default: ''
|
t.string :exchange, null: false, default: ''
|
||||||
t.string :routing_key, 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.integer :attempts, null: false, default: 0
|
||||||
t.datetime :last_attempted_at, null: true
|
t.datetime :last_attempted_at, null: true
|
||||||
@ -17,9 +18,11 @@ class CreateOutboxableOutboxes < ActiveRecord::Migration[7.0]
|
|||||||
|
|
||||||
t.integer :size, null: false, default: 0
|
t.integer :size, null: false, default: 0
|
||||||
|
|
||||||
t.references :outboxable, polymorphic: true, null: true
|
t.references :outboxable, polymorphic: true, null: true
|
||||||
|
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_index :outboxes, %i[status last_attempted_at], name: 'index_outboxes_on_outboxable_status_and_last_attempted_at'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
36
lib/templates/mongoid_initializer.rb
Normal file
36
lib/templates/mongoid_initializer.rb
Normal 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
|
||||||
72
lib/templates/mongoid_outbox.rb
Normal file
72
lib/templates/mongoid_outbox.rb
Normal 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
|
||||||
@ -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
|
|
||||||
@ -1,24 +1,24 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative "lib/outboxable/version"
|
require_relative 'lib/outboxable/version'
|
||||||
|
|
||||||
Gem::Specification.new do |spec|
|
Gem::Specification.new do |spec|
|
||||||
spec.name = "outboxable"
|
spec.name = 'outboxable'
|
||||||
spec.version = Outboxable::VERSION
|
spec.version = Outboxable::VERSION
|
||||||
spec.authors = ["Brusk Awat"]
|
spec.authors = ['Brusk Awat']
|
||||||
spec.email = ["broosk.edogawa@gmail.com"]
|
spec.email = ['broosk.edogawa@gmail.com']
|
||||||
|
|
||||||
spec.summary = "An opiniated Gem for Rails applications to implement the transactional outbox pattern."
|
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.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.homepage = 'https://github.com/broosk1993/outboxable'
|
||||||
spec.license = "MIT"
|
spec.license = 'MIT'
|
||||||
spec.required_ruby_version = ">= 2.6.0"
|
spec.required_ruby_version = '>= 3.1.2'
|
||||||
|
|
||||||
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
||||||
|
|
||||||
spec.metadata["homepage_uri"] = spec.homepage
|
spec.metadata['homepage_uri'] = spec.homepage
|
||||||
spec.metadata["source_code_uri"] = "https://githuh.com/broosk1993/outboxable"
|
spec.metadata['source_code_uri'] = 'https://github.com/broosk1993/outboxable'
|
||||||
spec.metadata["changelog_uri"] = "https://githuh.com/broosk1993/outboxable/CHANGELOG.md"
|
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.
|
# 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.
|
# 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)})
|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
spec.bindir = "exe"
|
spec.bindir = 'exe'
|
||||||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
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 'bunny', '>= 2.22'
|
||||||
spec.add_dependency 'connection_pool', '~> 2.3.0'
|
spec.add_dependency 'connection_pool', '>= 2.4'
|
||||||
|
|
||||||
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'sidekiq/testing'
|
||||||
|
|
||||||
RSpec.describe Outboxable do
|
RSpec.describe Outboxable do
|
||||||
it "has a version number" do
|
it 'has a version number' do
|
||||||
expect(Outboxable::VERSION).not_to be nil
|
expect(Outboxable::VERSION).not_to be nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does something useful" do
|
context 'polling publisher sidekiq worker' do
|
||||||
expect(false).to eq(true)
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "outboxable"
|
require 'outboxable'
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
# Enable flags like --only-failures and --next-failure
|
# 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`
|
# Disable RSpec exposing methods globally on `Module` and `main`
|
||||||
config.disable_monkey_patching!
|
config.disable_monkey_patching!
|
||||||
@ -12,4 +12,8 @@ RSpec.configure do |config|
|
|||||||
config.expect_with :rspec do |c|
|
config.expect_with :rspec do |c|
|
||||||
c.syntax = :expect
|
c.syntax = :expect
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.before(:each) do
|
||||||
|
Sidekiq::Worker.clear_all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user