ruby_event_store-outbox 0.0.2

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '08a5fc74b45a962065d79e01a6347919882bd97903ed30d5c722069d3e7ee617'
4
+ data.tar.gz: 4238ad6ce88f09744ee98feb3b513ec6e3903db92579494109f0575098f23be5
5
+ SHA512:
6
+ metadata.gz: 7939074f5b2fc85d9f4795103e0aaf38a59ca8e10b27f746a265b611fa4eda84a23806b1009845ce918dc61669bce4f5887d71c0139f152e4b04a9e7102a1fd9
7
+ data.tar.gz: 7c6f7168c15b2c81cc462e63dd528ef1415b690eae8b3f49344bd2fdb8681338721ef2a2c0b511d75c265c1590031d799d268417ab69694714624fe0246e1e8a
@@ -0,0 +1,44 @@
1
+ # Ruby Event Store Outbox
2
+
3
+ Very much work in progress.
4
+
5
+
6
+ ## Installation (app)
7
+
8
+ Add to your gemfile in application:
9
+
10
+ ```ruby
11
+ gem "ruby_event_store-outbox"
12
+ ```
13
+
14
+ In your event store configuration, as a dispatcher use `RubyEventStore::ImmediateAsyncDispatcher` with `RubyEventStore::Outbox::SidekiqScheduler`, for example:
15
+
16
+ ```ruby
17
+
18
+ RailsEventStore::Client.new(
19
+ dispatcher: RailsEventStore::ImmediateAsyncDispatcher.new(scheduler: RubyEventStore::Outbox::SidekiqScheduler.new),
20
+ ...
21
+ )
22
+ ```
23
+
24
+ Additionally, your handler's `through_outbox?` method should return `true`, for example:
25
+
26
+ ```ruby
27
+ class SomeHandler
28
+ def self.through_outbox?; true; end
29
+ end
30
+ ```
31
+
32
+
33
+ ## Installation (outbox process)
34
+
35
+ Run following process in any way you prefer:
36
+
37
+ ```
38
+ res_outbox --database-url="mysql2://root@0.0.0.0:3306/my_database" --redis-url="redis://localhost:6379/0" --log-level=info
39
+ ```
40
+
41
+
42
+ ## Contributing
43
+
44
+ Bug reports and pull requests are welcome on GitHub at https://github.com/RailsEventStore/rails_event_store.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ruby_event_store/outbox/cli"
4
+
5
+ cli = RubyEventStore::Outbox::CLI.new
6
+ cli.run(ARGV)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'rails/generators'
5
+ rescue LoadError
6
+ end
7
+
8
+ module RubyEventStore
9
+ module Outbox
10
+ class MigrationGenerator < Rails::Generators::Base
11
+ source_root File.expand_path(File.join(File.dirname(__FILE__), './templates'))
12
+
13
+ def create_migration
14
+ template "create_event_store_outbox_template.rb", "db/migrate/#{timestamp}_create_event_store_outbox.rb"
15
+ end
16
+
17
+ private
18
+
19
+ def rails_version
20
+ Rails::VERSION::STRING
21
+ end
22
+
23
+ def migration_version
24
+ return nil if Gem::Version.new(rails_version) < Gem::Version.new("5.0.0")
25
+ "[4.2]"
26
+ end
27
+
28
+ def timestamp
29
+ Time.now.strftime("%Y%m%d%H%M%S")
30
+ end
31
+ end
32
+ end
33
+ end if defined?(Rails::Generators::Base)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateEventStoreOutbox < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table(:event_store_outbox, force: false) do |t|
6
+ t.string :split_key, null: true
7
+ t.string :format, null: false
8
+ t.binary :payload, null: false
9
+ t.datetime :created_at, null: false
10
+ t.datetime :enqueued_at, null: true
11
+ end
12
+ add_index :event_store_outbox, [:format, :enqueued_at, :split_key], name: "index_event_store_outbox_for_pool"
13
+ add_index :event_store_outbox, [:created_at, :enqueued_at], name: "index_event_store_outbox_for_clear"
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module Outbox
5
+ end
6
+ end
7
+
8
+ require_relative 'outbox/record'
9
+ require_relative 'outbox/sidekiq_scheduler'
10
+ require_relative 'outbox/consumer'
11
+ require_relative 'outbox/version'
@@ -0,0 +1,60 @@
1
+ require "optparse"
2
+ require "ruby_event_store/outbox/version"
3
+ require "ruby_event_store/outbox/consumer"
4
+
5
+ module RubyEventStore
6
+ module Outbox
7
+ class CLI
8
+ Options = Struct.new(:database_url, :redis_url, :log_level, :split_keys, :message_format)
9
+
10
+ class Parser
11
+ def self.parse(argv)
12
+ options = Options.new(nil, nil, :warn, nil, nil)
13
+ OptionParser.new do |option_parser|
14
+ option_parser.banner = "Usage: res_outbox [options]"
15
+
16
+ option_parser.on("--database-url DATABASE_URL", "Database where outbox table is stored") do |database_url|
17
+ options.database_url = database_url
18
+ end
19
+
20
+ option_parser.on("--redis-url REDIS_URL", "URL to redis database") do |redis_url|
21
+ options.redis_url = redis_url
22
+ end
23
+
24
+ option_parser.on("--log-level LOG_LEVEL", [:fatal, :error, :warn, :info, :debug], "Logging level, one of: fatal, error, warn, info, debug") do |log_level|
25
+ options.log_level = log_level.to_sym
26
+ end
27
+
28
+ option_parser.on("--message-format FORMAT", ["sidekiq5"], "Message format, supported: sidekiq5") do |message_format|
29
+ options.message_format = message_format
30
+ end
31
+
32
+ option_parser.on("--split-keys=split_keys", Array, "Split keys which should be handled, all if not specified") do |split_keys|
33
+ options.split_keys = split_keys if !split_keys.empty?
34
+ end
35
+
36
+ option_parser.on_tail("--version", "Show version") do
37
+ puts VERSION
38
+ exit
39
+ end
40
+ end.parse(argv)
41
+ return options
42
+ end
43
+ end
44
+
45
+ def run(argv)
46
+ options = Parser.parse(argv)
47
+ logger = Logger.new(STDOUT, level: options.log_level, progname: "RES-Outbox")
48
+ outbox_consumer = RubyEventStore::Outbox::Consumer.new(
49
+ options.message_format,
50
+ options.split_keys,
51
+ database_url: options.database_url,
52
+ redis_url: options.redis_url,
53
+ logger: logger,
54
+ )
55
+ outbox_consumer.init
56
+ outbox_consumer.run
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,94 @@
1
+ require "logger"
2
+ require "redis"
3
+ require "active_record"
4
+ require "ruby_event_store/outbox/record"
5
+ require "ruby_event_store/outbox/sidekiq5_format"
6
+
7
+ module RubyEventStore
8
+ module Outbox
9
+ class Consumer
10
+ SLEEP_TIME_WHEN_NOTHING_TO_DO = 0.1
11
+
12
+ def initialize(format, split_keys, database_url:, redis_url:, clock: Time, logger:)
13
+ @split_keys = split_keys
14
+ @clock = clock
15
+ @redis = Redis.new(url: redis_url)
16
+ @logger = logger
17
+ ActiveRecord::Base.establish_connection(database_url) unless ActiveRecord::Base.connected?
18
+
19
+ raise "Unknown format" if format != SIDEKIQ5_FORMAT
20
+ @message_format = SIDEKIQ5_FORMAT
21
+
22
+ @gracefully_shutting_down = false
23
+ prepare_traps
24
+ end
25
+
26
+ def init
27
+ @redis.sadd("queues", split_keys)
28
+ logger.info("Initiated RubyEventStore::Outbox v#{VERSION}")
29
+ logger.info("Handling split keys: #{split_keys ? split_keys.join(", ") : "(all of them)"}")
30
+ end
31
+
32
+ def run
33
+ while !@gracefully_shutting_down do
34
+ was_something_changed = one_loop
35
+ sleep SLEEP_TIME_WHEN_NOTHING_TO_DO if !was_something_changed
36
+ end
37
+ logger.info "Gracefully shutting down"
38
+ end
39
+
40
+ def one_loop
41
+ Record.transaction do
42
+ records_scope = Record.lock.where(format: message_format, enqueued_at: nil)
43
+ records_scope = records_scope.where(split_key: split_keys) if !split_keys.nil?
44
+ records = records_scope.order("id ASC").limit(100)
45
+ return false if records.empty?
46
+
47
+ now = @clock.now.utc
48
+ failed_record_ids = []
49
+ records.each do |record|
50
+ begin
51
+ handle_one_record(now, record)
52
+ rescue => e
53
+ failed_record_ids << record.id
54
+ e.full_message.split($/).each {|line| logger.error(line) }
55
+ end
56
+ end
57
+
58
+ update_scope = if failed_record_ids.empty?
59
+ records
60
+ else
61
+ records.where("id NOT IN (?)", failed_record_ids)
62
+ end
63
+ update_scope.update_all(enqueued_at: now)
64
+
65
+ logger.info "Sent #{records.size} messages from outbox table"
66
+ return true
67
+ end
68
+ end
69
+
70
+ private
71
+ attr_reader :split_keys, :logger, :message_format
72
+
73
+ def handle_one_record(now, record)
74
+ hash_payload = JSON.parse(record.payload)
75
+ @redis.lpush("queue:#{hash_payload.fetch("queue")}", JSON.generate(JSON.parse(record.payload).merge({
76
+ "enqueued_at" => now.to_f,
77
+ })))
78
+ end
79
+
80
+ def prepare_traps
81
+ Signal.trap("INT") do
82
+ initiate_graceful_shutdown
83
+ end
84
+ Signal.trap("TERM") do
85
+ initiate_graceful_shutdown
86
+ end
87
+ end
88
+
89
+ def initiate_graceful_shutdown
90
+ @gracefully_shutting_down = true
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+
5
+ module RubyEventStore
6
+ module Outbox
7
+ class Record < ::ActiveRecord::Base
8
+ self.primary_key = :id
9
+ self.table_name = 'event_store_outbox'
10
+
11
+ def hash_payload
12
+ JSON.parse(payload).deep_symbolize_keys
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module Outbox
5
+ SIDEKIQ5_FORMAT = "sidekiq5"
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq'
4
+ require "ruby_event_store/outbox/sidekiq5_format"
5
+
6
+ module RubyEventStore
7
+ module Outbox
8
+ class SidekiqScheduler
9
+ def call(klass, serialized_event)
10
+ sidekiq_client = Sidekiq::Client.new(Sidekiq.redis_pool)
11
+ item = {
12
+ 'class' => klass,
13
+ 'args' => [serialized_event.to_h],
14
+ }
15
+ normalized_item = sidekiq_client.__send__(:normalize_item, item)
16
+ payload = sidekiq_client.__send__(:process_single, normalized_item.fetch('class'), normalized_item)
17
+ if payload
18
+ Record.create!(
19
+ format: SIDEKIQ5_FORMAT,
20
+ split_key: payload.fetch('queue'),
21
+ payload: payload.to_json
22
+ )
23
+ end
24
+ end
25
+
26
+ def verify(subscriber)
27
+ Class === subscriber && subscriber.respond_to?(:through_outbox?) && subscriber.through_outbox?
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyEventStore
4
+ module Outbox
5
+ VERSION = "0.0.2"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_event_store-outbox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Arkency
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby_event_store
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ description:
42
+ email:
43
+ - dev@arkency.com
44
+ executables:
45
+ - res_outbox
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - bin/res_outbox
51
+ - lib/generators/ruby_event_store/outbox/migration_generator.rb
52
+ - lib/generators/ruby_event_store/outbox/templates/create_event_store_outbox_template.rb
53
+ - lib/ruby_event_store/outbox.rb
54
+ - lib/ruby_event_store/outbox/cli.rb
55
+ - lib/ruby_event_store/outbox/consumer.rb
56
+ - lib/ruby_event_store/outbox/record.rb
57
+ - lib/ruby_event_store/outbox/sidekiq5_format.rb
58
+ - lib/ruby_event_store/outbox/sidekiq_scheduler.rb
59
+ - lib/ruby_event_store/outbox/version.rb
60
+ homepage: https://railseventstore.org
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ homepage_uri: https://railseventstore.org/
65
+ changelog_uri: https://github.com/RailsEventStore/rails_event_store/releases
66
+ source_code_uri: https://github.com/RailsEventStore/rails_event_store
67
+ bug_tracker_uri: https://github.com/RailsEventStore/rails_event_store/issues
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.0.3
84
+ signing_key:
85
+ specification_version: 4
86
+ summary: Active Record based outbox for Ruby Event Store
87
+ test_files: []