outboxer 0.1.11 → 1.0.0.pre.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +81 -47
- data/db/migrate/create_outboxer_exceptions.rb +20 -0
- data/db/migrate/create_outboxer_frames.rb +16 -0
- data/db/migrate/create_outboxer_messages.rb +19 -0
- data/generators/message_publisher_generator.rb +11 -0
- data/generators/{outboxer/install_generator.rb → schema_generator.rb} +4 -9
- data/lib/outboxer/database.rb +44 -0
- data/lib/outboxer/logger.rb +17 -0
- data/lib/outboxer/message.rb +262 -13
- data/lib/outboxer/messages.rb +232 -0
- data/lib/outboxer/models/exception.rb +15 -0
- data/lib/outboxer/models/frame.rb +14 -0
- data/lib/outboxer/models/message.rb +46 -0
- data/lib/outboxer/models.rb +3 -0
- data/lib/outboxer/railtie.rb +2 -4
- data/lib/outboxer/version.rb +1 -1
- data/lib/outboxer/web/public/css/bootstrap-icons.css +2078 -0
- data/lib/outboxer/web/public/css/bootstrap-icons.min.css +5 -0
- data/lib/outboxer/web/public/css/bootstrap.css +12071 -0
- data/lib/outboxer/web/public/css/bootstrap.min.css +6 -0
- data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff +0 -0
- data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff2 +0 -0
- data/lib/outboxer/web/public/favicon.svg +3 -0
- data/lib/outboxer/web/public/js/bootstrap.bundle.js +6306 -0
- data/lib/outboxer/web/public/js/bootstrap.bundle.min.js +7 -0
- data/lib/outboxer/web/views/error.erb +63 -0
- data/lib/outboxer/web/views/home.erb +172 -0
- data/lib/outboxer/web/views/layout.erb +80 -0
- data/lib/outboxer/web/views/message.erb +81 -0
- data/lib/outboxer/web/views/messages/index.erb +60 -0
- data/lib/outboxer/web/views/messages/show.erb +31 -0
- data/lib/outboxer/web/views/messages.erb +262 -0
- data/lib/outboxer/web.rb +430 -0
- data/lib/outboxer.rb +9 -5
- metadata +279 -22
- data/.rspec +0 -3
- data/.rubocop.yml +0 -229
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -84
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -12
- data/generators/outboxer/templates/bin/publisher.rb +0 -11
- data/generators/outboxer/templates/migrations/create_outboxer_exceptions.rb +0 -15
- data/generators/outboxer/templates/migrations/create_outboxer_messages.rb +0 -13
- data/lib/outboxer/exception.rb +0 -9
- data/lib/outboxer/outboxable.rb +0 -21
- data/lib/outboxer/publisher.rb +0 -149
- data/lib/tasks/gem.rake +0 -58
- data/outboxer.gemspec +0 -33
- data/sig/outboxer.rbs +0 -19
data/Rakefile
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require_relative "../config/environment"
|
4
|
-
|
5
|
-
Outboxer::Publisher.publish do |message:, logger:|
|
6
|
-
logger.info("[#{message.id}] publishing")
|
7
|
-
|
8
|
-
HardJob.perform_async({ "message_id" => message.id })
|
9
|
-
|
10
|
-
logger.info("[#{message.id}] published")
|
11
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
class CreateOutboxerExceptions < ActiveRecord::Migration[6.1]
|
2
|
-
def change
|
3
|
-
create_table :outboxer_exceptions do |t|
|
4
|
-
t.references :message, null: false, foreign_key: { to_table: :outboxer_messages }
|
5
|
-
|
6
|
-
t.text :class_name, null: false
|
7
|
-
t.text :message_text, null: false
|
8
|
-
t.column :backtrace, :text, array: true
|
9
|
-
|
10
|
-
t.timestamps
|
11
|
-
end
|
12
|
-
|
13
|
-
remove_column :outboxer_exceptions, :updated_at
|
14
|
-
end
|
15
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
class CreateOutboxerMessages < ActiveRecord::Migration[6.1]
|
2
|
-
def change
|
3
|
-
create_table :outboxer_messages do |t|
|
4
|
-
t.references :message, polymorphic: true, null: false
|
5
|
-
t.text :status, null: false
|
6
|
-
|
7
|
-
t.timestamps
|
8
|
-
end
|
9
|
-
|
10
|
-
add_index :outboxer_messages, %i[status created_at]
|
11
|
-
add_index :outboxer_messages, %i[message_type message_id], unique: true
|
12
|
-
end
|
13
|
-
end
|
data/lib/outboxer/exception.rb
DELETED
data/lib/outboxer/outboxable.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
module Outboxer
|
2
|
-
module Outboxable
|
3
|
-
def self.included(base)
|
4
|
-
base.extend ClassMethods
|
5
|
-
|
6
|
-
base.class_eval do
|
7
|
-
has_one :message, class_name: "::Outboxer::Message", as: :outbox_message,
|
8
|
-
dependent: :destroy
|
9
|
-
|
10
|
-
after_create :create_outbox_message!
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
def create_outbox_message!
|
15
|
-
Message.create!(message: self, status: Message::STATUS[:unpublished])
|
16
|
-
end
|
17
|
-
|
18
|
-
module ClassMethods
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/lib/outboxer/publisher.rb
DELETED
@@ -1,149 +0,0 @@
|
|
1
|
-
require "logger"
|
2
|
-
|
3
|
-
module Outboxer
|
4
|
-
module Publisher
|
5
|
-
module_function
|
6
|
-
|
7
|
-
@publishing = false
|
8
|
-
@logger = Logger.new($stdout)
|
9
|
-
|
10
|
-
# Publishes messages from the Outboxer queue.
|
11
|
-
#
|
12
|
-
# This method will continue to publish messages from the queue as long
|
13
|
-
# as @publishing is set to true. It uses an ActiveRecord connection to
|
14
|
-
# handle database interactions.
|
15
|
-
#
|
16
|
-
# @param poll [Integer] the interval (in seconds) at which the method
|
17
|
-
# should poll the database for new messages.
|
18
|
-
# @param backoff [Proc] a callable object that defines how to increase the
|
19
|
-
# backoff time when an error occurs. It should accept the current backoff
|
20
|
-
# time as a parameter and return the new backoff time.
|
21
|
-
#
|
22
|
-
# @yield [Hash] once a message is dequeued, the method yields a hash to
|
23
|
-
# the given block. The hash contains the :message (the dequeued message)
|
24
|
-
# and :logger (the logger object).
|
25
|
-
#
|
26
|
-
# @yieldparam message [Message] the dequeued message from the Outboxer queue.
|
27
|
-
# @yieldparam logger [Logger] the logger instance to log any information or errors.
|
28
|
-
#
|
29
|
-
# @raise [StandardError] if an error occurs during the yield, it is rescued,
|
30
|
-
# logged, and then the failed method is invoked to handle the error. The
|
31
|
-
# method then moves on to the next message in the queue.
|
32
|
-
#
|
33
|
-
# @return [void] This method does not have a meaningful return value.
|
34
|
-
#
|
35
|
-
# @example Basic usage
|
36
|
-
# Outboxer::Publisher.publish(poll: 1) do |hash|
|
37
|
-
# puts hash[:message]
|
38
|
-
# puts hash[:logger]
|
39
|
-
# end
|
40
|
-
def publish(poll: 1, backoff: ->(current_backoff) { [current_backoff * 2, 5 * 60].min })
|
41
|
-
@publishing = true
|
42
|
-
|
43
|
-
ActiveRecord::Base.connection_pool.with_connection do
|
44
|
-
while @publishing
|
45
|
-
outboxer_message = dequeue(backoff: backoff)
|
46
|
-
|
47
|
-
if outboxer_message.nil?
|
48
|
-
sleep poll
|
49
|
-
|
50
|
-
next
|
51
|
-
end
|
52
|
-
|
53
|
-
begin
|
54
|
-
yield message: outboxer_message.message, logger: @logger
|
55
|
-
rescue StandardError => exception
|
56
|
-
failed(
|
57
|
-
outboxer_message: outboxer_message,
|
58
|
-
backoff: backoff,
|
59
|
-
exception: exception)
|
60
|
-
|
61
|
-
next
|
62
|
-
end
|
63
|
-
|
64
|
-
published(
|
65
|
-
outboxer_message: outboxer_message,
|
66
|
-
backoff: backoff)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def dequeue(backoff:)
|
72
|
-
retry_on_error(backoff: backoff) do
|
73
|
-
ActiveRecord::Base.transaction do
|
74
|
-
message = Message
|
75
|
-
.where(status: Message::STATUS[:unpublished])
|
76
|
-
.order(created_at: :asc)
|
77
|
-
.limit(1)
|
78
|
-
.lock("FOR UPDATE SKIP LOCKED")
|
79
|
-
.first
|
80
|
-
|
81
|
-
message&.update!(status: Message::STATUS[:publishing])
|
82
|
-
|
83
|
-
message
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def published(outboxer_message:, backoff:)
|
89
|
-
retry_on_error(backoff: backoff) do
|
90
|
-
outboxer_message.destroy!
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def failed(outboxer_message:, exception:, backoff:)
|
95
|
-
@logger.error(
|
96
|
-
"Exception raised: #{exception.class}: #{exception.message}\n" \
|
97
|
-
"#{exception.backtrace.join("\n")}")
|
98
|
-
|
99
|
-
retry_on_error(backoff: backoff) do
|
100
|
-
ActiveRecord::Base.transaction do
|
101
|
-
outboxer_message.update!(status: Message::STATUS[:failed])
|
102
|
-
|
103
|
-
outboxer_message.exceptions.create!(
|
104
|
-
class_name: exception.class.name,
|
105
|
-
message_text: exception.message,
|
106
|
-
backtrace: exception.backtrace)
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def retry_on_error(backoff:, &block)
|
112
|
-
current_backoff = 1
|
113
|
-
|
114
|
-
begin
|
115
|
-
block.call
|
116
|
-
rescue StandardError => exception
|
117
|
-
@logger.fatal(
|
118
|
-
"Exception raised: #{exception.class}: #{exception.message}\n" \
|
119
|
-
"#{exception.backtrace.join("\n")}")
|
120
|
-
|
121
|
-
raise exception unless @publishing
|
122
|
-
|
123
|
-
sleep current_backoff
|
124
|
-
|
125
|
-
current_backoff = backoff.call(current_backoff)
|
126
|
-
|
127
|
-
retry
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# Stops the publishing process.
|
132
|
-
#
|
133
|
-
# @note This method will stop the current message publishing process
|
134
|
-
# It is a safe way to interrupt the publishing process at any point.
|
135
|
-
#
|
136
|
-
# @return [void]
|
137
|
-
def stop
|
138
|
-
@publishing = false
|
139
|
-
end
|
140
|
-
|
141
|
-
Signal.trap("TERM") do
|
142
|
-
@logger.info("Received SIGTERM, stopping...")
|
143
|
-
|
144
|
-
stop
|
145
|
-
end
|
146
|
-
|
147
|
-
private_class_method :retry_on_error, :dequeue, :published, :failed
|
148
|
-
end
|
149
|
-
end
|
data/lib/tasks/gem.rake
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
require "rake/packagetask"
|
2
|
-
|
3
|
-
namespace :gem do
|
4
|
-
desc "Bump version number"
|
5
|
-
task :bump, [:type] do |_t, args|
|
6
|
-
args.with_defaults(type: "patch")
|
7
|
-
|
8
|
-
unless %w[major minor patch].include?(args[:type])
|
9
|
-
raise "Invalid version type - choose from major/minor/patch"
|
10
|
-
end
|
11
|
-
|
12
|
-
version_file = File.expand_path("../../lib/outboxer/version.rb", __dir__)
|
13
|
-
version = ""
|
14
|
-
File.open(version_file, "r") do |file|
|
15
|
-
version = file.read.match(/VERSION = "(.*)"/)[1]
|
16
|
-
end
|
17
|
-
|
18
|
-
version_parts = version.split(".").map(&:to_i)
|
19
|
-
case args[:type]
|
20
|
-
when "major"
|
21
|
-
version_parts[0] += 1
|
22
|
-
version_parts[1] = 0
|
23
|
-
version_parts[2] = 0
|
24
|
-
when "minor"
|
25
|
-
version_parts[1] += 1
|
26
|
-
version_parts[2] = 0
|
27
|
-
when "patch"
|
28
|
-
version_parts[2] += 1
|
29
|
-
end
|
30
|
-
|
31
|
-
new_version = version_parts.join(".")
|
32
|
-
File.write(version_file, "module Outboxer\n VERSION = \"#{new_version}\".freeze\nend\n")
|
33
|
-
|
34
|
-
puts "Gem version bumped to #{new_version}"
|
35
|
-
end
|
36
|
-
|
37
|
-
desc "Build the gem"
|
38
|
-
task :build do
|
39
|
-
Outboxer.send(:remove_const, :VERSION) if Outboxer.const_defined?(:VERSION)
|
40
|
-
load "lib/outboxer/version.rb"
|
41
|
-
sh "gem build outboxer.gemspec"
|
42
|
-
|
43
|
-
puts "Gem built successfully."
|
44
|
-
end
|
45
|
-
|
46
|
-
desc "Push the gem to RubyGems"
|
47
|
-
task :push do
|
48
|
-
Outboxer.send(:remove_const, :VERSION) if Outboxer.const_defined?(:VERSION)
|
49
|
-
load "lib/outboxer/version.rb"
|
50
|
-
version = Outboxer::VERSION
|
51
|
-
sh "gem push outboxer-#{version}.gem"
|
52
|
-
|
53
|
-
puts "Gem pushed to RubyGems."
|
54
|
-
end
|
55
|
-
|
56
|
-
desc "Bump, build and push the gem to RubyGems"
|
57
|
-
task release: %i[bump build push]
|
58
|
-
end
|
data/outboxer.gemspec
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
# rubocop:disable Layout/LineLength
|
2
|
-
require_relative "lib/outboxer/version"
|
3
|
-
|
4
|
-
Gem::Specification.new do |spec|
|
5
|
-
spec.name = "outboxer"
|
6
|
-
spec.version = Outboxer::VERSION
|
7
|
-
spec.authors = ["Adam Mikulasev"]
|
8
|
-
spec.email = ["adam@fastprogrammer.co"]
|
9
|
-
|
10
|
-
spec.summary = "Transactional outbox implementation for event driven Ruby on Rails applications that use SQL"
|
11
|
-
spec.homepage = "https://github.com/fast-programmer/outboxer"
|
12
|
-
spec.license = "MIT"
|
13
|
-
spec.required_ruby_version = ">= 2.6.0"
|
14
|
-
|
15
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
-
spec.metadata["source_code_uri"] = "https://github.com/fast-programmer/outboxer"
|
17
|
-
spec.metadata["documentation_uri"] = "https://rubydoc.info/github/fast-programmer/outboxer/master"
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/fast-programmer/outboxer/blob/master/CHANGELOG.md"
|
19
|
-
spec.metadata["rubygems_mfa_required"] = "true"
|
20
|
-
|
21
|
-
spec.files = Dir.chdir(__dir__) do
|
22
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
23
|
-
(File.expand_path(f) == __FILE__) ||
|
24
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
25
|
-
end
|
26
|
-
end
|
27
|
-
spec.bindir = "exe"
|
28
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
|
-
spec.require_paths = ["lib"]
|
30
|
-
|
31
|
-
spec.add_dependency "activerecord", "~> 7.0"
|
32
|
-
end
|
33
|
-
# rubocop:enable Layout/LineLength
|
data/sig/outboxer.rbs
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
module Outboxer
|
2
|
-
VERSION: String
|
3
|
-
|
4
|
-
module Publisher
|
5
|
-
class Args
|
6
|
-
attr_reader message: untyped
|
7
|
-
attr_reader logger: Logger
|
8
|
-
|
9
|
-
def initialize: (untyped message, Logger logger) -> void
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.publish: (?Integer poll, ?Proc[Integer, Integer] backoff) { (Args) -> untyped } -> void
|
13
|
-
def self.dequeue: ({ backoff: Proc[Integer, Integer] }) -> untyped
|
14
|
-
def self.published: ({ outboxer_message: untyped, backoff: Proc[Integer, Integer] }) -> void
|
15
|
-
def self.failed: ({ outboxer_message: untyped, exception: Exception, backoff: Proc[Integer, Integer] }) -> void
|
16
|
-
def self.retry_on_error: ({ backoff: Proc[Integer, Integer] }) { () -> untyped } -> untyped
|
17
|
-
def self.stop: () -> void
|
18
|
-
end
|
19
|
-
end
|