ruby_event_store-outbox 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []