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