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.
- checksums.yaml +7 -0
- data/README.md +44 -0
- data/bin/res_outbox +6 -0
- data/lib/generators/ruby_event_store/outbox/migration_generator.rb +33 -0
- data/lib/generators/ruby_event_store/outbox/templates/create_event_store_outbox_template.rb +15 -0
- data/lib/ruby_event_store/outbox.rb +11 -0
- data/lib/ruby_event_store/outbox/cli.rb +60 -0
- data/lib/ruby_event_store/outbox/consumer.rb +94 -0
- data/lib/ruby_event_store/outbox/record.rb +16 -0
- data/lib/ruby_event_store/outbox/sidekiq5_format.rb +7 -0
- data/lib/ruby_event_store/outbox/sidekiq_scheduler.rb +31 -0
- data/lib/ruby_event_store/outbox/version.rb +7 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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.
|
data/bin/res_outbox
ADDED
@@ -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,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,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
|
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: []
|