railbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/generators/railbox/install/install_generator.rb +19 -0
- data/lib/railbox/exceptions/queue_error.rb +5 -0
- data/lib/railbox/exceptions/validation_error.rb +6 -0
- data/lib/railbox/handler/handler.rb +33 -0
- data/lib/railbox/http_client/faraday.rb +33 -0
- data/lib/railbox/interfaces/transactional_outbox_interface.rb +14 -0
- data/lib/railbox/models/transactional_outbox.rb +50 -0
- data/lib/railbox/mutators/transactional_outbox_mutator.rb +29 -0
- data/lib/railbox/processors/handler_processor.rb +19 -0
- data/lib/railbox/processors/http_processor.rb +18 -0
- data/lib/railbox/queue/base_queue.rb +42 -0
- data/lib/railbox/queue/handling_queue.rb +61 -0
- data/lib/railbox/queue/http_queue.rb +61 -0
- data/lib/railbox/workers/base_worker.rb +25 -0
- data/lib/railbox/workers/process_queue_worker.rb +93 -0
- data/lib/railbox.rb +59 -0
- metadata +201 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5e7c53ea3b3e9b5c1f7a67c19273456d4cedb978257834328db3b1a3e058e0bf
|
4
|
+
data.tar.gz: 2832aeeb9558208d4ba229828403459422fcd19ea765ac193b63067031746349
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 670dbf35a4f62c0c81a779bc5d1f2e4af414f13b6d72064fbfd0eedc1bfd5097617c7f6508b8bc2abe082dea5927e5da4dfa78a23dfca0e38dda92c9b77def7a
|
7
|
+
data.tar.gz: ffdd4cc4dd8bd242483ac8b61fecacb3a9b69104d3931c3cead162a6b0c7601e2dfa0381d74005facdf1967b533643b61c242b867029e8b6b6e04f5f3b33c10d
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Railbox
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def create_migration_file
|
11
|
+
migration_template 'migration.rb.tt', "db/migrate/create_transactional_outbox.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.next_migration_number(dirname)
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# Module to be included in service classes for adding queue processing support
|
2
|
+
# and an additional `outbox_entity` property.
|
3
|
+
#
|
4
|
+
# @example Usage
|
5
|
+
# class MyService
|
6
|
+
# include Railbox::Handler
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# MyService.enqueue(method: 'update', body: { key: 'value' })
|
10
|
+
#
|
11
|
+
# @!method outbox_entity
|
12
|
+
# @return [Object] Returns the current value of outbox_entity.
|
13
|
+
#
|
14
|
+
module Railbox
|
15
|
+
module Handler
|
16
|
+
def self.included(base)
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
attr_accessor :outbox_entity
|
22
|
+
|
23
|
+
# Queues a request for asynchronous execution
|
24
|
+
# @param method [String] to be called (default: 'create')
|
25
|
+
# @param body [Hash] main payload for the handler
|
26
|
+
# @param opts [Hash] any additional options (e.g. relative_entity/meta)
|
27
|
+
#
|
28
|
+
def enqueue(method: 'create', body: {}, **opts)
|
29
|
+
HandlingQueue.enqueue(service: name, method: method, body: body, **opts)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Railbox
|
2
|
+
module HttpClient
|
3
|
+
class Faraday
|
4
|
+
class << self
|
5
|
+
def request(method, url, query = {}, body = {}, headers = {})
|
6
|
+
connection = ::Faraday.new do |config|
|
7
|
+
config.adapter :httpclient
|
8
|
+
config.request :json
|
9
|
+
config.response :json, parser_options: {symbol_keys: true}
|
10
|
+
config.response :raise_error
|
11
|
+
|
12
|
+
config.response :logger, Rails.logger, logger_options
|
13
|
+
end
|
14
|
+
|
15
|
+
connection.send(method, url) do |req|
|
16
|
+
req.params = query if query.present?
|
17
|
+
req.headers = headers if headers.present?
|
18
|
+
req.body = body if body.present? && method != :get
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger_options
|
23
|
+
{
|
24
|
+
headers: {request: true, response: true},
|
25
|
+
bodies: {request: true, response: true},
|
26
|
+
errors: true,
|
27
|
+
log_level: :info
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Railbox
|
2
|
+
class TransactionalOutboxInterface
|
3
|
+
|
4
|
+
def initialize(record)
|
5
|
+
@record = record
|
6
|
+
end
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@record,
|
10
|
+
:id, :action_type, :action_data, :status, :relative_entity,
|
11
|
+
:query, :body, :headers, :meta, :attempts, :retry_at, :failure_reasons,
|
12
|
+
:group
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Models
|
3
|
+
# == Schema Information
|
4
|
+
#
|
5
|
+
# Table name: transactional_outboxes
|
6
|
+
#
|
7
|
+
# id :bigint not null, primary key
|
8
|
+
# action_type :string not null # Type of action/event (class_method/http)
|
9
|
+
# action_data :jsonb not null # Action data or payload ({process_class: "MyService", process_method: "create"}/{method: "POST", url: "https://..."}) (default: {})
|
10
|
+
# status :string not null # Processing status (e.g. in_progress, failed, completed)
|
11
|
+
# entity_type :string # Polymorphic type for associated entity
|
12
|
+
# entity_id :integer # Polymorphic ID for associated entity
|
13
|
+
# query :jsonb # Url query (JSON)
|
14
|
+
# body :jsonb # Main body/payload (default: {})
|
15
|
+
# headers :jsonb # Headers (default: {})
|
16
|
+
# meta :jsonb # Metadata or extra info (JSON)
|
17
|
+
# attempts :integer default(0) # Number of processing attempts
|
18
|
+
# retry_at :datetime # Next retry timestamp
|
19
|
+
# failure_reasons :jsonb, array # Array of failure reason objects
|
20
|
+
# created_at :datetime not null # Record creation timestamp
|
21
|
+
# updated_at :datetime not null # Last update timestamp
|
22
|
+
#
|
23
|
+
# Indexes
|
24
|
+
#
|
25
|
+
# index_transactional_outboxes_on_entity_type_and_entity_id (entity_type,entity_id)
|
26
|
+
#
|
27
|
+
# Purpose:
|
28
|
+
# - Queues reliable actions/events for external systems.
|
29
|
+
# - Tracks delivery status and retry logic.
|
30
|
+
# - Stores metadata and failure reasons for auditing/debugging.
|
31
|
+
#
|
32
|
+
class TransactionalOutbox < ::ActiveRecord::Base
|
33
|
+
belongs_to :relative_entity, polymorphic: true, foreign_key: :entity_id, foreign_type: :entity_type
|
34
|
+
|
35
|
+
def in_group?
|
36
|
+
group.present? || entity_group.present?
|
37
|
+
end
|
38
|
+
|
39
|
+
def group
|
40
|
+
action_data[:group]
|
41
|
+
end
|
42
|
+
|
43
|
+
def entity_group
|
44
|
+
"#{entity_type}.#{entity_id}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Mutators
|
3
|
+
class TransactionalOutboxMutator
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def create(attributes)
|
7
|
+
TransactionalOutbox.create!(attributes)
|
8
|
+
end
|
9
|
+
|
10
|
+
def update(record, attributes)
|
11
|
+
record.assign_attributes(attributes)
|
12
|
+
|
13
|
+
if record.status == 'in_progress'
|
14
|
+
record.attempts += 1
|
15
|
+
|
16
|
+
if record.attempts >= Railbox.configuration.max_attempts
|
17
|
+
record.status = 'failed'
|
18
|
+
else
|
19
|
+
record.retry_at = (Railbox.configuration.retry_strategy[record.attempts - 1] || Railbox.configuration.retry_strategy.last).from_now
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
record.save!
|
24
|
+
record
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Processors
|
3
|
+
class HandlerProcessor
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def process(record)
|
7
|
+
interface = Railbox::TransactionalOutboxInterface.new(record)
|
8
|
+
action_data = record.action_data.deep_symbolize_keys!
|
9
|
+
handler = Object.const_get(action_data[:class_name])
|
10
|
+
handler.outbox_entity = interface
|
11
|
+
|
12
|
+
handler.public_send(action_data[:method_name])
|
13
|
+
|
14
|
+
handler.outbox_entity = nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Processors
|
3
|
+
class HttpProcessor
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def process(record)
|
7
|
+
action_data = record.action_data.deep_symbolize_keys!
|
8
|
+
|
9
|
+
Railbox::HttpClient::Faraday.request(action_data[:method_name],
|
10
|
+
action_data[:url],
|
11
|
+
record.query,
|
12
|
+
record.body,
|
13
|
+
record.headers)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Queue
|
3
|
+
# Abstract base class for enqueuing tasks into the transactional outbox.
|
4
|
+
#
|
5
|
+
# This class is intended to be subclassed: descendants should implement their own logic for enqueuing tasks.
|
6
|
+
#
|
7
|
+
# Usage example:
|
8
|
+
# class MyQueue < Railbox::BaseQueue
|
9
|
+
# def self.enqueue(**opts)
|
10
|
+
# # your implementation here
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# The .enqueue method **must** be implemented in each subclass.
|
15
|
+
# Calling .enqueue directly on BaseQueue will raise NotImplementedError.
|
16
|
+
#
|
17
|
+
class BaseQueue
|
18
|
+
class << self
|
19
|
+
# Abstract method for enqueuing a task into the outbox.
|
20
|
+
#
|
21
|
+
# Each subclass must override this method with specific logic.
|
22
|
+
#
|
23
|
+
# @param opts [Hash] Parameters required by a particular implementation.
|
24
|
+
# @raise [NotImplementedError] if the method is not overridden in a subclass.
|
25
|
+
#
|
26
|
+
def enqueue(**_)
|
27
|
+
raise NotImplementedError, 'You must implement this method'
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Creates a record in the transactional outbox with the given attributes.
|
33
|
+
#
|
34
|
+
# @param attributes [Hash] Record attributes
|
35
|
+
# @return [Railbox::TransactionalOutboxMutator] The created record
|
36
|
+
def to_queue(**attributes)
|
37
|
+
Railbox::TransactionalOutboxMutator.create(**attributes)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Queue
|
3
|
+
# HandlingQueue is responsible for enqueuing "class method call" actions into the transactional outbox table.
|
4
|
+
#
|
5
|
+
# Usage example:
|
6
|
+
# Railbox::HandlingQueue.enqueue(
|
7
|
+
# service: "MyService",
|
8
|
+
# method: "perform_action",
|
9
|
+
# body: { key: "value" },
|
10
|
+
# headers: { ... },
|
11
|
+
# query: { ... },
|
12
|
+
# entity_type: "User",
|
13
|
+
# entity_id: 123,
|
14
|
+
# meta: { ... }
|
15
|
+
# )
|
16
|
+
#
|
17
|
+
# This method creates a Railbox::TransactionalOutboxMutator record to be processed by a background job later,
|
18
|
+
# enabling reliable, auditable, and decoupled invocation of class methods in your system.
|
19
|
+
#
|
20
|
+
# A ValidationError is raised if the given options are invalid (for example, missing class, method, or incorrect body format).
|
21
|
+
#
|
22
|
+
class HandlingQueue < BaseQueue
|
23
|
+
OPTIONS = %i[headers query relative_entity meta].freeze
|
24
|
+
|
25
|
+
class << self
|
26
|
+
# Enqueues a class method call operation for asynchronous processing via the transactional outbox.
|
27
|
+
#
|
28
|
+
# @param service [String] Name of the target service class (must exist).
|
29
|
+
# @param method [String, Symbol] Name of the public class method to call (default: 'create').
|
30
|
+
# @param body [Hash] The request payload (must be a Hash)
|
31
|
+
# @param opts [Hash] Optional parameters: headers, query, entity_type, entity_id, meta.
|
32
|
+
# @raise [ValidationError] If the service, method, or body are invalid.
|
33
|
+
# @return [Boolean] true if enqueuing succeeds.
|
34
|
+
#
|
35
|
+
def enqueue(service:, method: 'create', body: {}, **opts)
|
36
|
+
opts.deep_symbolize_keys!.slice!(*OPTIONS)
|
37
|
+
validate_options(service, method, body, **opts)
|
38
|
+
|
39
|
+
to_queue(
|
40
|
+
action_type: 'handler',
|
41
|
+
action_data: {class_name: service, method_name: method},
|
42
|
+
body: body,
|
43
|
+
status: 'in_progress',
|
44
|
+
**opts
|
45
|
+
)
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def validate_options(service, method, body, **_)
|
53
|
+
raise ValidationError, "Service #{service} is not present" unless service.present?
|
54
|
+
raise ValidationError, "Service class #{service} is not defined" unless Object.const_defined?(service)
|
55
|
+
raise ValidationError, "Method #{method} for class #{service} is not defined" unless Object.const_get(service).respond_to?(method)
|
56
|
+
raise ValidationError, 'Body must be a Hash' unless body.is_a?(Hash)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Queue
|
3
|
+
# HttpQueue is responsible for enqueuing HTTP request actions into the transactional outbox.
|
4
|
+
#
|
5
|
+
# Usage example:
|
6
|
+
# Railbox::HttpQueue.enqueue(
|
7
|
+
# url: "https://example.com/api",
|
8
|
+
# method: :post,
|
9
|
+
# body: { data: 1 },
|
10
|
+
# headers: { "Authorization" => "Bearer ..." },
|
11
|
+
# query: { foo: "bar" },
|
12
|
+
# meta: { correlation_id: "123" }
|
13
|
+
# )
|
14
|
+
#
|
15
|
+
# This creates a Railbox::TransactionalOutboxMutator record, which will be processed later by a background worker,
|
16
|
+
# providing reliable, traceable, and decoupled execution of HTTP requests.
|
17
|
+
#
|
18
|
+
# A ValidationError will be raised if the url, method, or body are invalid.
|
19
|
+
#
|
20
|
+
class HttpQueue < BaseQueue
|
21
|
+
OPTIONS = %i[query meta group].freeze
|
22
|
+
METHODS = %i[get post put patch delete].freeze
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Enqueues an HTTP request action for asynchronous processing via the transactional outbox.
|
26
|
+
#
|
27
|
+
# @param url [String] The HTTP endpoint URL (must begin with http:// or https://).
|
28
|
+
# @param method [Symbol] HTTP method to use (:get, :post, :put, :patch, :delete). Defaults to :post.
|
29
|
+
# @param body [Hash] Request payload (must be a Hash). Defaults to empty hash.
|
30
|
+
# @param headers [Hash] Optional HTTP headers.
|
31
|
+
# @param opts [Hash] Additional options: query, meta, group.
|
32
|
+
# @raise [ValidationError] If the url, method, or body are invalid.
|
33
|
+
# @return [Boolean] true if enqueuing is successful.
|
34
|
+
#
|
35
|
+
def enqueue(url:, method: :post, body: {}, headers: {}, **opts)
|
36
|
+
opts.deep_symbolize_keys!.slice!(*OPTIONS)
|
37
|
+
validate_options(url, method, body)
|
38
|
+
|
39
|
+
to_queue(
|
40
|
+
action_type: 'http_request',
|
41
|
+
action_data: {url: url, method_name: method},
|
42
|
+
body: body,
|
43
|
+
headers: headers,
|
44
|
+
status: 'in_progress',
|
45
|
+
**opts
|
46
|
+
)
|
47
|
+
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def validate_options(url, method, body)
|
54
|
+
raise ValidationError, "Url #{url} is not valid" unless %r{\Ahttps?://[^\s/$.?#].\S*\z}i.match?(url)
|
55
|
+
raise ValidationError, "Invalid method: #{method}. Allowed methods are: #{METHODS.join(', ')}" unless METHODS.include?(method)
|
56
|
+
raise ValidationError, 'Body must be a Hash' unless body.is_a?(Hash)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Workers
|
3
|
+
class BaseWorker < ::ActiveJob::Base
|
4
|
+
queue_as :default
|
5
|
+
|
6
|
+
def with_lock
|
7
|
+
lock_id = Zlib.crc32(self.class.name, 0)
|
8
|
+
lock_sql = "SELECT pg_try_advisory_lock(#{lock_id}) AS locked"
|
9
|
+
|
10
|
+
result = ActiveRecord::Base.connection.execute(lock_sql)
|
11
|
+
locked = result.first['locked'] unless result.nil?
|
12
|
+
|
13
|
+
unless locked
|
14
|
+
Rails.logger.info "Another #{self.class.name} work now."
|
15
|
+
return
|
16
|
+
end
|
17
|
+
|
18
|
+
yield
|
19
|
+
|
20
|
+
ensure
|
21
|
+
ActiveRecord::Base.connection.execute("SELECT pg_advisory_unlock(#{lock_id})")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Railbox
|
2
|
+
module Workers
|
3
|
+
class ProcessQueueWorker < BaseWorker
|
4
|
+
PROCESSORS = {
|
5
|
+
'handler' => Processors::HandlerProcessor,
|
6
|
+
'http_request' => Processors::HttpProcessor
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
def perform
|
10
|
+
with_lock do
|
11
|
+
grouped_records.each_value do |group|
|
12
|
+
group.each do |record|
|
13
|
+
process_record(record)
|
14
|
+
rescue => e
|
15
|
+
TransactionalOutboxMutator.update(record, {failure_reasons: record.failure_reasons << { message: e.message, backtrace: e.backtrace&.take(3), at: DateTime.current}})
|
16
|
+
raise e if record.in_group? || record.action_type == 'handler'
|
17
|
+
|
18
|
+
Rails.logger.error("RailboxWorker error: #{e.message}\n #{e.backtrace&.take(3)&.join("\n")}")
|
19
|
+
|
20
|
+
next
|
21
|
+
end
|
22
|
+
rescue => e
|
23
|
+
Rails.logger.error("RailboxWorker error: #{e.message}\n #{e.backtrace&.take(3)&.join("\n")}")
|
24
|
+
next
|
25
|
+
end
|
26
|
+
rescue => e
|
27
|
+
Rails.logger.error("RailboxWorker error: #{e.message}\n #{e.backtrace&.take(3)&.join("\n")}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def grouped_records
|
34
|
+
sql = <<-SQL.squish
|
35
|
+
SELECT t.*
|
36
|
+
FROM transactional_outboxes t
|
37
|
+
WHERE t.status = 'in_progress'
|
38
|
+
AND (t.retry_at IS NULL OR t.retry_at <= NOW())
|
39
|
+
AND NOT EXISTS (
|
40
|
+
SELECT 1
|
41
|
+
FROM transactional_outboxes f
|
42
|
+
WHERE f.status = 'failed'
|
43
|
+
AND (
|
44
|
+
(t.action_data ? 'group' AND (f.action_data->>'group') = (t.action_data->>'group'))
|
45
|
+
OR (
|
46
|
+
(NOT (t.action_data ? 'group') OR (t.action_data->>'group') IS NULL)
|
47
|
+
AND f.entity_type = t.entity_type
|
48
|
+
AND f.entity_id = t.entity_id
|
49
|
+
)
|
50
|
+
)
|
51
|
+
)
|
52
|
+
ORDER BY t.created_at
|
53
|
+
SQL
|
54
|
+
|
55
|
+
records = Railbox::TransactionalOutbox.find_by_sql(sql)
|
56
|
+
|
57
|
+
grouped = records.group_by do |record|
|
58
|
+
record.group.present? ? [:group, record.group] : [:entity, record.entity_group]
|
59
|
+
end
|
60
|
+
|
61
|
+
grouped.transform_values do |group_records|
|
62
|
+
key_record = group_records.first
|
63
|
+
scope = Railbox::TransactionalOutbox.where(status: 'in_progress')
|
64
|
+
|
65
|
+
if key_record.group.present?
|
66
|
+
scope = scope.where("action_data ->> 'group' = ?", key_record.group)
|
67
|
+
else
|
68
|
+
scope = scope.where(entity_type: key_record.entity_type, entity_id: key_record.entity_id)
|
69
|
+
end
|
70
|
+
|
71
|
+
all_group_records = scope.order(:created_at).to_a
|
72
|
+
|
73
|
+
group_records if all_group_records.first&.id.in?(group_records.map(&:id))
|
74
|
+
end.compact.reject { |_k, v| v.blank? }
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_record(record)
|
78
|
+
Rails.logger.info("Start process with transactional outbox ID #{record.id}")
|
79
|
+
processor = PROCESSORS[record.action_type]
|
80
|
+
|
81
|
+
if processor
|
82
|
+
processor.process(record)
|
83
|
+
else
|
84
|
+
raise Railbox::QueueError, "Unknown action_type=#{record.action_type} for outbox #{record.id}"
|
85
|
+
end
|
86
|
+
|
87
|
+
TransactionalOutboxMutator.update(record, {status: 'completed'})
|
88
|
+
|
89
|
+
Rails.logger.info("Finish process with transactional outbox ID #{record.id}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/railbox.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_job'
|
3
|
+
|
4
|
+
require 'faraday'
|
5
|
+
require 'faraday/httpclient'
|
6
|
+
|
7
|
+
require_relative 'railbox/exceptions/validation_error'
|
8
|
+
require_relative 'railbox/exceptions/queue_error'
|
9
|
+
|
10
|
+
require_relative 'railbox/models/transactional_outbox'
|
11
|
+
require_relative 'railbox/mutators/transactional_outbox_mutator'
|
12
|
+
|
13
|
+
require_relative 'railbox/interfaces/transactional_outbox_interface'
|
14
|
+
|
15
|
+
require_relative 'railbox/queue/base_queue'
|
16
|
+
require_relative 'railbox/queue/http_queue'
|
17
|
+
require_relative 'railbox/queue/handling_queue'
|
18
|
+
|
19
|
+
require_relative 'railbox/handler/handler'
|
20
|
+
|
21
|
+
require_relative 'railbox/processors/handler_processor'
|
22
|
+
require_relative 'railbox/processors/http_processor'
|
23
|
+
|
24
|
+
require_relative 'railbox/workers/base_worker'
|
25
|
+
require_relative 'railbox/workers/process_queue_worker'
|
26
|
+
|
27
|
+
require_relative 'railbox/http_client/faraday'
|
28
|
+
|
29
|
+
module Railbox
|
30
|
+
class << self
|
31
|
+
attr_writer :configuration
|
32
|
+
|
33
|
+
def configure
|
34
|
+
self.configuration ||= Configuration.new
|
35
|
+
yield(configuration)
|
36
|
+
end
|
37
|
+
|
38
|
+
def configuration
|
39
|
+
@configuration ||= Configuration.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
class Configuration
|
45
|
+
attr_accessor :max_attempts, :retry_strategy
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@max_attempts = 5
|
49
|
+
@retry_strategy = [1.minute, 10.minutes, 1.hour, 3.hours, 1.day]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
Railbox::HttpQueue = Railbox::Queue::HttpQueue
|
55
|
+
Railbox::HandlingQueue = Railbox::Queue::HandlingQueue
|
56
|
+
Railbox::TransactionalOutbox = Railbox::Models::TransactionalOutbox
|
57
|
+
Railbox::TransactionalOutboxMutator = Railbox::Mutators::TransactionalOutboxMutator
|
58
|
+
Railbox::QueueError = Railbox::Exceptions::QueueError
|
59
|
+
Railbox::ValidationError = Railbox::Exceptions::ValidationError
|
metadata
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: railbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Egor Beresnev
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activejob
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '7.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '7.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activerecord
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '7.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '7.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: faraday
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: faraday-httpclient
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: forwardable
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.3.3
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 1.3.3
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: bundler
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '2.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '2.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: factory_bot
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rake
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '13.0'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '13.0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: rspec
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '3.12'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '3.12'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: sqlite3
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
description: A gem for implementing the transactional outbox pattern with support
|
153
|
+
for HTTP requests and custom message pre-processing.
|
154
|
+
email:
|
155
|
+
- egor594bed@gmail.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- lib/generators/railbox/install/install_generator.rb
|
161
|
+
- lib/railbox.rb
|
162
|
+
- lib/railbox/exceptions/queue_error.rb
|
163
|
+
- lib/railbox/exceptions/validation_error.rb
|
164
|
+
- lib/railbox/handler/handler.rb
|
165
|
+
- lib/railbox/http_client/faraday.rb
|
166
|
+
- lib/railbox/interfaces/transactional_outbox_interface.rb
|
167
|
+
- lib/railbox/models/transactional_outbox.rb
|
168
|
+
- lib/railbox/mutators/transactional_outbox_mutator.rb
|
169
|
+
- lib/railbox/processors/handler_processor.rb
|
170
|
+
- lib/railbox/processors/http_processor.rb
|
171
|
+
- lib/railbox/queue/base_queue.rb
|
172
|
+
- lib/railbox/queue/handling_queue.rb
|
173
|
+
- lib/railbox/queue/http_queue.rb
|
174
|
+
- lib/railbox/workers/base_worker.rb
|
175
|
+
- lib/railbox/workers/process_queue_worker.rb
|
176
|
+
homepage: https://github.com/egor594bed/railbox
|
177
|
+
licenses:
|
178
|
+
- MIT
|
179
|
+
metadata:
|
180
|
+
homepage_uri: https://github.com/egor594bed/railbox
|
181
|
+
source_code_uri: https://github.com/egor594bed/railbox
|
182
|
+
changelog_uri: https://github.com/egor594bed/railbox/CHANGELOG.md
|
183
|
+
rubygems_mfa_required: 'true'
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: 3.0.0
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubygems_version: 3.6.7
|
199
|
+
specification_version: 4
|
200
|
+
summary: Reliable transactional outbox for background tasks and decoupled processing
|
201
|
+
test_files: []
|