outboxer 0.1.11 → 1.0.0.pre.beta

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -47
  3. data/db/migrate/create_outboxer_exceptions.rb +20 -0
  4. data/db/migrate/create_outboxer_frames.rb +16 -0
  5. data/db/migrate/create_outboxer_messages.rb +19 -0
  6. data/generators/message_publisher_generator.rb +11 -0
  7. data/generators/{outboxer/install_generator.rb → schema_generator.rb} +4 -9
  8. data/lib/outboxer/database.rb +44 -0
  9. data/lib/outboxer/logger.rb +17 -0
  10. data/lib/outboxer/message.rb +262 -13
  11. data/lib/outboxer/messages.rb +232 -0
  12. data/lib/outboxer/models/exception.rb +15 -0
  13. data/lib/outboxer/models/frame.rb +14 -0
  14. data/lib/outboxer/models/message.rb +46 -0
  15. data/lib/outboxer/models.rb +3 -0
  16. data/lib/outboxer/railtie.rb +2 -4
  17. data/lib/outboxer/version.rb +1 -1
  18. data/lib/outboxer/web/public/css/bootstrap-icons.css +2078 -0
  19. data/lib/outboxer/web/public/css/bootstrap-icons.min.css +5 -0
  20. data/lib/outboxer/web/public/css/bootstrap.css +12071 -0
  21. data/lib/outboxer/web/public/css/bootstrap.min.css +6 -0
  22. data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff +0 -0
  23. data/lib/outboxer/web/public/css/fonts/bootstrap-icons.woff2 +0 -0
  24. data/lib/outboxer/web/public/favicon.svg +3 -0
  25. data/lib/outboxer/web/public/js/bootstrap.bundle.js +6306 -0
  26. data/lib/outboxer/web/public/js/bootstrap.bundle.min.js +7 -0
  27. data/lib/outboxer/web/views/error.erb +63 -0
  28. data/lib/outboxer/web/views/home.erb +172 -0
  29. data/lib/outboxer/web/views/layout.erb +80 -0
  30. data/lib/outboxer/web/views/message.erb +81 -0
  31. data/lib/outboxer/web/views/messages/index.erb +60 -0
  32. data/lib/outboxer/web/views/messages/show.erb +31 -0
  33. data/lib/outboxer/web/views/messages.erb +262 -0
  34. data/lib/outboxer/web.rb +430 -0
  35. data/lib/outboxer.rb +9 -5
  36. metadata +279 -22
  37. data/.rspec +0 -3
  38. data/.rubocop.yml +0 -229
  39. data/CHANGELOG.md +0 -5
  40. data/CODE_OF_CONDUCT.md +0 -84
  41. data/LICENSE.txt +0 -21
  42. data/Rakefile +0 -12
  43. data/generators/outboxer/templates/bin/publisher.rb +0 -11
  44. data/generators/outboxer/templates/migrations/create_outboxer_exceptions.rb +0 -15
  45. data/generators/outboxer/templates/migrations/create_outboxer_messages.rb +0 -13
  46. data/lib/outboxer/exception.rb +0 -9
  47. data/lib/outboxer/outboxable.rb +0 -21
  48. data/lib/outboxer/publisher.rb +0 -149
  49. data/lib/tasks/gem.rake +0 -58
  50. data/outboxer.gemspec +0 -33
  51. data/sig/outboxer.rbs +0 -19
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- require "rspec/core/rake_task"
2
-
3
- RSpec::Core::RakeTask.new(:spec)
4
-
5
- require "pry"
6
- require "rubocop/rake_task"
7
-
8
- RuboCop::RakeTask.new
9
-
10
- task default: %i[spec rubocop]
11
-
12
- load "lib/tasks/gem.rake"
@@ -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
@@ -1,9 +0,0 @@
1
- require "active_record"
2
-
3
- module Outboxer
4
- class Exception < ::ActiveRecord::Base
5
- self.table_name = :outboxer_exceptions
6
-
7
- belongs_to :message, class_name: "::Outboxer::Message"
8
- end
9
- end
@@ -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
@@ -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