trabox 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 44bcd5a8b174aa887c7cdbd415aeeba5f1af25a11e10a8a23543967ba2f4fd84
4
+ data.tar.gz: 0b2d81b7a826f457dd24763bf5eb39278d14061293cf75f225b54639e13057e4
5
+ SHA512:
6
+ metadata.gz: 02b16217802b7beaa2c2f87e6bf60681556554c458f839c34ed7cfc049c80107be7a5646b2ae2d42de76159e4973776701cb4020987d9d2462dcad75e4183f14
7
+ data.tar.gz: 3b45467e241f3339de38f6502121347bc569d3b0ac9f29ff286befbbcd9d4866dd348c6a650e4fa63d901c6ba0eb3c8bfe90244bd7b812568c498366c82d4dcb
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 TODO: Write your name
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Trabox
2
+
3
+ Transactional-Outbox for Rails.
4
+
5
+ ## Usage
6
+
7
+ How to use my plugin.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'trabox'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ ```bash
20
+ bundle
21
+ ```
22
+
23
+ Or install it yourself as:
24
+
25
+ ```bash
26
+ gem install trabox
27
+ ```
28
+
29
+ ## Contributing
30
+
31
+ Contribution directions go here.
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
data/exe/trabox ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'trabox/command'
4
+
5
+ $stdout.sync = true
6
+ $stderr.sync = true
7
+
8
+ begin
9
+ command_parser = Trabox::Command::Parser.new
10
+ command = command_parser.parse!
11
+
12
+ Trabox::Command.invoke command
13
+ rescue StandardError => e
14
+ warn e.backtrace
15
+ warn e.message
16
+ warn ''
17
+ warn command_parser.usage
18
+ exit 1
19
+ end
20
+
21
+ # vim:ft=ruby:
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate init Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trabox
4
+ class ConfigureGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def create_initializer
8
+ copy_file 'initializer.rb', 'config/initializers/trabox.rb'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,44 @@
1
+ # Trabox::Metric.setup unless Rails.env.test?
2
+
3
+ Trabox::Command::Relay.configure do |config|
4
+ config.limit = 10
5
+
6
+ # ref: https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html#method-i-lock-21
7
+ config.lock = true
8
+ config.interval = 5
9
+ config.log_level = :info
10
+
11
+ # String or Proc
12
+ ordering_key = Trabox::Publisher::Google::Cloud::PubSub::OrderingKey.new ->(event) { event.model_name.name }
13
+
14
+ config.publisher = Trabox::Publisher::Google::Cloud::PubSub.new(
15
+ 'trabox',
16
+ message_ordering: true,
17
+ ordering_key: ordering_key
18
+ )
19
+ end
20
+
21
+ Trabox::Command::Subscribe.configure do |config|
22
+ config.log_level = :info
23
+
24
+ before_listen_acknowledge_callbacks = []
25
+ before_listen_acknowledge_callbacks << lambda do |received_message|
26
+ Rails.logger.info "id=#{received_message.message_id} message=#{received_message.data} ordering_key=#{received_message.ordering_key}"
27
+ end
28
+
29
+ # after_listen_acknowledge_callbacks = []
30
+ # after_listen_acknowledge_callbacks << lambda do |_received_message|
31
+ # end
32
+
33
+ # error_listen_callbacks = []
34
+ # error_listen_callbacks << lambda do |_exception|
35
+ # end
36
+
37
+ config.subscriber = Trabox::Subscriber::Google::Cloud::PubSub.new(
38
+ 'trabox-sub',
39
+ listen_options: {},
40
+ before_listen_acknowledge_callbacks: before_listen_acknowledge_callbacks,
41
+ after_listen_acknowledge_callbacks: [],
42
+ error_listen_callbacks: []
43
+ )
44
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ bin/rails generate trabox:model Thing
6
+
7
+ This will create:
8
+ what/will/it/create
@@ -0,0 +1,31 @@
1
+ require 'rails/generators/active_record/model/model_generator'
2
+
3
+ module Trabox
4
+ class ModelGenerator < ActiveRecord::Generators::ModelGenerator
5
+ source_root "#{base_root}/active_record/model/templates"
6
+
7
+ class_option :polymorphic, type: :string, desc: 'add polymorphic column.'
8
+
9
+ def initialize(*args)
10
+ super
11
+
12
+ add_attribute "#{options[:polymorphic]}:references{polymorphic}" if options[:polymorphic].present?
13
+
14
+ add_attribute 'event_data:binary'
15
+ add_attribute 'message_id:string'
16
+ add_attribute 'published_at:datetime'
17
+ end
18
+
19
+ def insert_include
20
+ inject_into_class File.join('app/models', class_path, "#{file_name}.rb"), class_name do
21
+ " include Trabox::Relay::Relayable\n"
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def add_attribute(attribute)
28
+ attributes << Rails::Generators::GeneratedAttribute.parse(attribute)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :trabox do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,40 @@
1
+ require 'optparse'
2
+
3
+ module Trabox
4
+ module Command
5
+ class Parser
6
+ def initialize
7
+ @parser = OptionParser.new(nil, 20) do |o|
8
+ o.version = VERSION
9
+ o.banner = "\e[1mUsage: #{o.program_name}\e[0m <COMMAND> [OPTIONS]"
10
+ o.on_tail('-h', '--help', 'Print help information') do
11
+ $stdout.puts usage
12
+ exit 0
13
+ end
14
+
15
+ o.on_tail('-v', '--version', 'Print version information')
16
+ end
17
+ end
18
+
19
+ def parse!
20
+ @parser.order!
21
+
22
+ command = ARGV.shift
23
+
24
+ return command unless command.nil?
25
+
26
+ warn usage
27
+ exit 1
28
+ end
29
+
30
+ def usage
31
+ <<~USAGE
32
+ #{@parser.help}
33
+ \e[1mCommands\e[0m:
34
+ r, relay Relay events
35
+ s, subscribe Subscribe events
36
+ USAGE
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ require 'trabox/command/parser'
2
+ require 'trabox/commands/relay'
3
+ require 'trabox/commands/subscribe'
4
+
5
+ module Trabox
6
+ module Command
7
+ # @param command [String]
8
+ def self.invoke(command)
9
+ case command
10
+ when 'r', 'relay'
11
+ Trabox::Command::Relay.perform
12
+ when 's', 'subscribe'
13
+ Trabox::Command::Subscribe.perform
14
+ else
15
+ raise "no such command: #{command}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Trabox
2
+ module Command
3
+ class Configuration
4
+ DEFAULT_LOG_LEVEL = :info
5
+ # @!attribute [rw] log_level
6
+ # @return [Symbol]
7
+ attr_accessor :log_level
8
+
9
+ def initialize
10
+ @log_level = ENV['TRABOX_LOG_LEVEL']&.downcase&.to_sym || DEFAULT_LOG_LEVEL
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trabox
4
+ module Command
5
+ module Runner
6
+ RAILS_ROOT = ENV['RAILS_ROOT'] || '.'
7
+ APP_PATH ||= "#{RAILS_ROOT}/config/application"
8
+
9
+ def self.load_runner
10
+ require "#{RAILS_ROOT}/config/boot"
11
+
12
+ require APP_PATH
13
+ Rails.application.require_environment!
14
+ Rails.application.load_runner
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ require 'optparse'
2
+ require_relative './configuration'
3
+
4
+ module Trabox
5
+ module Command
6
+ module Relay
7
+ class ArgumentParser
8
+ def self.parse!
9
+ new
10
+ end
11
+
12
+ def initialize
13
+ opts = parse!
14
+
15
+ config_overwrite(opts)
16
+ end
17
+
18
+ private
19
+
20
+ def parse!
21
+ opts = {}
22
+
23
+ @parser = OptionParser.new do |o|
24
+ o.banner = <<~BANNER
25
+ \e[1mUsage\e[0m: \e[1mtrabox relay\e[0m [OPTIONS]
26
+
27
+ Overwrite configuration
28
+
29
+ BANNER
30
+
31
+ o.on('-l NUM', '--limit', Integer)
32
+ o.on('-i SEC', '--interval', Integer)
33
+ o.on('-L', '--[no-]lock', TrueClass)
34
+ o.on('--log-level LEVEL', String) { |v| v.downcase.to_sym }
35
+ end
36
+
37
+ @parser.parse!(into: opts)
38
+
39
+ opts.transform_keys { |k| k.to_s.underscore.to_sym }
40
+ end
41
+
42
+ def config_overwrite(opts)
43
+ opts.each do |attr, val|
44
+ next if val.nil?
45
+
46
+ Relay.config.send("#{attr}=", val)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,63 @@
1
+ require_relative '../common/configuration'
2
+
3
+ module Trabox
4
+ module Command
5
+ module Relay
6
+ class << self
7
+ attr_accessor :active
8
+ alias active? active
9
+
10
+ def configure
11
+ return unless active?
12
+
13
+ yield config
14
+ end
15
+
16
+ def config
17
+ @config ||= Configuration.new
18
+ end
19
+
20
+ def config_activate
21
+ @active = true
22
+ end
23
+ end
24
+
25
+ class Configuration < Command::Configuration
26
+ DEFAULT_SELECT_LIMIT = 3
27
+ DEFAULT_INTERVAL = 5
28
+ DEFAULT_LOCK = true
29
+
30
+ # @!attribute [rw] limit
31
+ # @return [Integer]
32
+ # @!attribute [rw] interval
33
+ # @return [Integer]
34
+ # @!attribute [rw] lock
35
+ # @return [Boolean, String]
36
+ # @!attribute [rw] publisher
37
+ # @return [Trabox::Publisher, Class]
38
+ attr_accessor :limit,
39
+ :interval,
40
+ :lock,
41
+ :publisher
42
+
43
+ def initialize
44
+ @limit = ENV['TRABOX_RELAYER_LIMIT'] || DEFAULT_SELECT_LIMIT
45
+ @interval = ENV['TRABOX_RELAYER_INTERVAL'] || DEFAULT_INTERVAL
46
+ @lock = ENV['TRABOX_RELAYER_LOCK'] || DEFAULT_LOCK
47
+
48
+ super
49
+ end
50
+
51
+ def interval=(interval)
52
+ @interval = interval.to_i
53
+ end
54
+
55
+ def check
56
+ return if @publisher.respond_to?(:publish)
57
+
58
+ raise 'Relay Configuration: config.publisher must be have :publish method.'
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,51 @@
1
+ require_relative './relay/argument_parser'
2
+ require_relative './relay/configuration'
3
+ require_relative './common/runner'
4
+
5
+ module Trabox
6
+ module Command
7
+ module Relay
8
+ class << self
9
+ def prepare
10
+ config_activate
11
+
12
+ Runner.load_runner
13
+
14
+ ArgumentParser.parse!
15
+
16
+ config.check
17
+
18
+ Rails.logger.level = config.log_level
19
+ end
20
+ end
21
+
22
+ def self.perform
23
+ prepare
24
+
25
+ relayer = Trabox::Relay::Relayer.new(
26
+ config.publisher,
27
+ limit: config.limit,
28
+ lock: config.lock
29
+ )
30
+
31
+ interval = config.interval
32
+
33
+ loop do
34
+ begin
35
+ relayer.perform
36
+
37
+ Metric.service_check('relay.service.check', Metric::SERVICE_OK)
38
+ rescue StandardError => e
39
+ Rails.logger.error e
40
+
41
+ ActiveRecord::Base.clear_all_connections!
42
+
43
+ Metric.service_check('relay.service.check', Metric::SERVICE_CRITICAL)
44
+ end
45
+
46
+ sleep interval
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,49 @@
1
+ require 'optparse'
2
+ require_relative './configuration'
3
+
4
+ module Trabox
5
+ module Command
6
+ module Subscribe
7
+ class ArgumentParser
8
+ def self.parse!
9
+ new
10
+ end
11
+
12
+ def initialize
13
+ opts = parse!
14
+
15
+ config_overwrite(opts)
16
+ end
17
+
18
+ private
19
+
20
+ def parse!
21
+ opts = {}
22
+
23
+ @parser = OptionParser.new do |o|
24
+ o.banner = <<~BANNER
25
+ \e[1mUsage\e[0m: \e[1mtrabox subscribe\e[0m [OPTIONS]
26
+
27
+ Overwrite configuration
28
+
29
+ BANNER
30
+
31
+ o.on('--log-level LEVEL', String) { |v| v.downcase.to_sym }
32
+ end
33
+
34
+ @parser.parse!(into: opts)
35
+
36
+ opts.transform_keys { |k| k.to_s.underscore.to_sym }
37
+ end
38
+
39
+ def config_overwrite(opts)
40
+ opts.each do |attr, val|
41
+ next if val.nil?
42
+
43
+ Subscribe.config.send("#{attr}=", val)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../common/configuration'
2
+
3
+ module Trabox
4
+ module Command
5
+ module Subscribe
6
+ class << self
7
+ attr_accessor :active
8
+ alias active? active
9
+
10
+ def configure
11
+ return unless active?
12
+
13
+ yield config
14
+ end
15
+
16
+ def config
17
+ @config ||= Configuration.new
18
+ end
19
+
20
+ def config_activate
21
+ @active = true
22
+ end
23
+ end
24
+
25
+ class Configuration < Command::Configuration
26
+ # @!attribute [rw] subscriber
27
+ # @return [Trabox::Subscriber, Class]
28
+ attr_accessor :subscriber
29
+
30
+ def check
31
+ return if @subscriber.respond_to?(:subscribe)
32
+
33
+ raise 'Subscribe Configuration: config.subscriber must be have :subscribe method.'
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ require_relative './subscribe/argument_parser'
2
+ require_relative './subscribe/configuration'
3
+ require_relative './common/runner'
4
+
5
+ module Trabox
6
+ module Command
7
+ module Subscribe
8
+ class << self
9
+ def prepare
10
+ config_activate
11
+
12
+ Runner.load_runner
13
+
14
+ ArgumentParser.parse!
15
+
16
+ config.check
17
+
18
+ Rails.logger.level = config.log_level
19
+ end
20
+ end
21
+
22
+ def self.perform
23
+ prepare
24
+
25
+ subscriber = config.subscriber
26
+
27
+ Metric.service_check('subscribe.service.check', Metric::SERVICE_OK)
28
+
29
+ begin
30
+ subscriber.subscribe
31
+ rescue StandardError => e
32
+ Rails.logger.error e
33
+
34
+ Metric.service_check('subscribe.service.check', Metric::SERVICE_CRITICAL)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'datadog/statsd'
4
+
5
+ module Trabox
6
+ # Traboxのメトリクス
7
+ #
8
+ # - unpublished_event_count: パブリッシュするイベント数
9
+ # - published_event_count: パブリッシュしたイベント数
10
+ # - find_events_error_count: パブリッシュするイベントの取得に失敗した数
11
+ # - publish_event_error_count: イベントのパブリッシュに失敗した数
12
+ # - update_event_record_error_count: パブリッシュしたイベントのカラム更新に失敗した数
13
+ module Metric
14
+ NAMESPACE = ENV.fetch('METRIC_NAMESPACE', 'trabox')
15
+ LOG_PREFIX = '[metric]'
16
+ SERVICE_OK = Datadog::Statsd::OK
17
+ SERVICE_CRITICAL = Datadog::Statsd::CRITICAL
18
+
19
+ class << self
20
+ attr_reader :statsd
21
+
22
+ # Datadog::Statsd.new arguments
23
+ def setup(*args, **kwargs)
24
+ @statsd = Datadog::Statsd.new(*args, **kwargs)
25
+ end
26
+
27
+ def service_check(name, status, opts = {})
28
+ name = metric_name(name)
29
+
30
+ @statsd&.service_check(name, status, opts)
31
+
32
+ status = case status
33
+ when SERVICE_OK
34
+ 'ok'
35
+ when SERVICE_CRITICAL
36
+ 'critical'
37
+ else
38
+ 'undefined'
39
+ end
40
+
41
+ Rails.logger.debug "#{LOG_PREFIX} type=service_check name=#{name} status=#{status} opts=#{opts}"
42
+ end
43
+
44
+ def count(name, count, opts = {})
45
+ name = metric_name(name)
46
+
47
+ @statsd&.count(name, count, opts)
48
+
49
+ Rails.logger.debug "#{LOG_PREFIX} type=count name=#{name} count=#{count} opts=#{opts}"
50
+ end
51
+
52
+ def increment(name, opts = {})
53
+ count name, 1, opts
54
+ end
55
+
56
+ def decrement(name, opts = {})
57
+ count name, -1, opts
58
+ end
59
+
60
+ def distribution(name, value, opts = {})
61
+ name = metric_name(name)
62
+
63
+ @statsd&.distribution name, value, opts
64
+
65
+ Rails.logger.debug "#{LOG_PREFIX} type=distribution name=#{name} value=#{value} opts=#{opts}"
66
+ end
67
+
68
+ def distribution_time(name, opts = {}, &block)
69
+ name = metric_name(name)
70
+
71
+ @statsd&.distribution_time name, opts, &block
72
+
73
+ Rails.logger.debug "#{LOG_PREFIX} type=distribution_time name=#{name} opts=#{opts}"
74
+ end
75
+
76
+ def event(title, text, opts = {})
77
+ @statsd&.event title, text, opts
78
+
79
+ Rails.logger.debug "#{LOG_PREFIX} type=event title=#{title} opts=#{opts}"
80
+ end
81
+
82
+ def gauge(name, value, opts = {})
83
+ name = metric_name(name)
84
+
85
+ @statsd&.gauge name, value, opts
86
+
87
+ Rails.logger.debug "#{LOG_PREFIX} type=gauge name=#{name} value=#{value} opts=#{opts}"
88
+ end
89
+
90
+ def histogram(name, value, opts = {})
91
+ name = metric_name(name)
92
+
93
+ @statsd&.histogram name, value, opts
94
+
95
+ Rails.logger.debug "#{LOG_PREFIX} type=histogram name=#{name} value=#{value} opts=#{opts}"
96
+ end
97
+
98
+ def set(name, value, opts = {})
99
+ name = metric_name(name)
100
+
101
+ @statsd&.set name, value, opts
102
+
103
+ Rails.logger.debug "#{LOG_PREFIX} type=set name=#{name} value=#{value} opts=#{opts}"
104
+ end
105
+
106
+ def time(name, opts = {}, &block)
107
+ name = metric_name(name)
108
+
109
+ @statsd&.time name, opts, &block
110
+
111
+ Rails.logger.debug "#{LOG_PREFIX} type=time name=#{name} opts=#{opts}"
112
+ end
113
+
114
+ def timing(name, ms, opts = {})
115
+ name = metric_name(name)
116
+
117
+ @statsd&.timing name, ms, opts
118
+
119
+ Rails.logger.debug "#{LOG_PREFIX} type=timing name=#{name} ms=#{ms} opts=#{opts}"
120
+ end
121
+
122
+ def batch
123
+ yield self
124
+ end
125
+
126
+ def close(**kwargs)
127
+ @statsd&.close(**kwargs)
128
+
129
+ Rails.logger.debug "#{LOG_PREFIX} type=close opts=#{kwargs}"
130
+ end
131
+
132
+ def flush(**kwargs)
133
+ @statsd&.flush(**kwargs)
134
+
135
+ Rails.logger.debug "#{LOG_PREFIX} type=flush opts=#{kwargs}"
136
+ end
137
+
138
+ private
139
+
140
+ def metric_name(name)
141
+ "#{NAMESPACE}.#{name}"
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,89 @@
1
+ require 'google/cloud/pubsub'
2
+
3
+ # Google::Cloud::PubSub::Topicのラッパー
4
+ module Trabox
5
+ module Publisher
6
+ module Google
7
+ module Cloud
8
+ class PubSub
9
+ include Trabox::Publisher
10
+
11
+ LOG_PREFIX = '[google pubsub]'.freeze
12
+
13
+ class OrderingKey
14
+ # @param key [Proc(ActiveRecord), String]
15
+ def initialize(key)
16
+ @key = if key.is_a?(Proc)
17
+ key
18
+ elsif key.instance_of?(String)
19
+ ->(*) { key }
20
+ else
21
+ raise ArgumentError, 'OrderingKey.new should be set to Proc or String.'
22
+ end
23
+ end
24
+
25
+ # @return [String]
26
+ def call(*args)
27
+ key = @key.call(*args)
28
+
29
+ raise 'OrderingKey#call should be returned String type.' unless key.instance_of?(String)
30
+
31
+ key
32
+ end
33
+ end
34
+
35
+ # @param topic_id [String]
36
+ # @param message_ordering [Boolean] enable_message_ordering
37
+ # @param ordering_key [OrderingKey]
38
+ def initialize(topic_id, message_ordering: true, ordering_key: nil)
39
+ raise ArgumentError, 'topic_id must be specified.' if topic_id.blank?
40
+
41
+ unless ordering_key.nil? || ordering_key.is_a?(OrderingKey)
42
+ raise ArgumentError,
43
+ 'ordering_key must be specified OrderingKey class or nil.'
44
+ end
45
+
46
+ # @type [Google::Cloud::PubSub::Project]
47
+ @pubsub = ::Google::Cloud::PubSub.new
48
+
49
+ # @type [Google::Cloud::PubSub::Topic]
50
+ @topic = @pubsub.topic topic_id
51
+
52
+ @ordering_key = ordering_key
53
+
54
+ raise "Topic-ID='#{topic_id}' does not exist." if @topic.nil?
55
+
56
+ @topic.enable_message_ordering! if message_ordering
57
+ rescue StandardError => e
58
+ Rails.logger.error "#{LOG_PREFIX} #{e.message}"
59
+ raise
60
+ end
61
+
62
+ # @param event [ActiveRecord] publishするイベント
63
+ # @return message_id [String]
64
+ def publish(event)
65
+ raise ArgumentError, 'event should be set to trabox:model' unless event.respond_to?(:event_data)
66
+
67
+ message = event.event_data
68
+
69
+ raise ArgumentError, 'published message must not be blank' if message.blank?
70
+
71
+ published_message = if @ordering_key
72
+ @topic.publish message, ordering_key: @ordering_key.call(event)
73
+ else
74
+ @topic.publish message
75
+ end
76
+
77
+ Rails.logger.debug "#{LOG_PREFIX} message published. " \
78
+ "message_id=#{published_message.message_id} ordering_key=#{published_message.ordering_key}"
79
+
80
+ published_message.message_id
81
+ rescue StandardError => e
82
+ Rails.logger.error "#{LOG_PREFIX} #{e.message}"
83
+ raise
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,8 @@
1
+ require 'trabox/publisher/google/cloud_pubsub'
2
+
3
+ module Trabox
4
+ module Publisher
5
+ # @param event [ActiveRecord] publishするイベント
6
+ def publish(event); end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module Trabox
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,27 @@
1
+ # 未publishなレコードやpublish完了時のレコード操作を追加するためのモジュール
2
+ module Trabox
3
+ module Relay
4
+ module Relayable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # @param limit [Integer]
9
+ scope :unpublished, lambda { |limit: DEFAULT_SELECT_LIMIT|
10
+ where(published_at: nil).limit(limit).order(created_at: :asc)
11
+ }
12
+ end
13
+
14
+ # message_idとpublished_atを更新する
15
+ # publishした結果からpublishした時間を取得できないため、Timeクラスを使用する
16
+ #
17
+ # @param message_id [String]
18
+ def published_done!(message_id)
19
+ raise ArgumentError if message_id.blank?
20
+
21
+ update!(message_id: message_id, published_at: Time.current.to_formatted_s(:iso8601))
22
+
23
+ Rails.logger.debug "Event record updated. message_id=#{message_id}"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # Relayableモジュールをincludeしているモデル一覧を取得するモジュール
2
+ module Trabox
3
+ module Relay
4
+ module RelayableModels
5
+ # @return [Array<Class>]
6
+ def self.list
7
+ if @models.nil?
8
+ load_models
9
+
10
+ @models = ApplicationRecord.descendants.filter do |klass|
11
+ klass.ancestors.include?(Relayable)
12
+ end
13
+ end
14
+
15
+ Rails.logger.debug "Relayed event models: #{@models.map { |model| model.name.underscore }}"
16
+
17
+ @models
18
+ end
19
+
20
+ def self.load_models
21
+ return if Rails.application.config.eager_load
22
+
23
+ # test, developmentモードは遅延読み込みのためmodelsをロードする
24
+ Rails.logger.debug 'Load models'
25
+
26
+ Dir["#{Rails.root}/app/models/**/*.rb"].each do |file|
27
+ require_dependency file
28
+ end
29
+ end
30
+
31
+ private_class_method :load_models
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,62 @@
1
+ module Trabox
2
+ module Relay
3
+ class Relayer
4
+ # @param publisher [Trabox::PubSub::Publisher]
5
+ # @param limit [Integer] SELECT文のLIMIT
6
+ # @param lock [Boolean, String] ActiveRecord lock argument
7
+ def initialize(publisher, limit: DEFAULT_SELECT_LIMIT, lock: true)
8
+ raise ArgumentError unless publisher.respond_to?(:publish)
9
+
10
+ @publisher = publisher
11
+ @limit = limit
12
+ @lock = lock
13
+ end
14
+
15
+ def perform
16
+ RelayableModels.list.each do |model|
17
+ model.transaction do
18
+ unpublished_events = begin
19
+ model.lock(@lock).unpublished limit: @limit
20
+ rescue StandardError
21
+ Metric.increment('find_events_error_count',
22
+ tags: ["event-type:#{model.name.underscore}"])
23
+ raise
24
+ end
25
+
26
+ unpublished_events.each do |event|
27
+ Metric.increment('unpublished_event_count',
28
+ tags: ["event-type:#{event.class.name.underscore}", "event-id:#{event.id}"])
29
+
30
+ publish_and_commit(event)
31
+
32
+ Metric.increment('published_event_count',
33
+ tags: ["event-type:#{event.class.name.underscore}", "event-id:#{event.id}"])
34
+ end
35
+
36
+ Rails.logger.info "Published events. (#{model.name.underscore}=#{unpublished_events.size})"
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def publish_and_commit(event)
44
+ begin
45
+ message_id = @publisher.publish event
46
+ rescue StandardError
47
+ Metric.increment('published_event_error_count',
48
+ tags: ["event-type:#{event.class.name.underscore}", "event-id:#{event.id}"])
49
+ raise
50
+ end
51
+
52
+ begin
53
+ event.published_done! message_id
54
+ rescue StandardError
55
+ Metric.increment('update_event_record_error_count',
56
+ tags: ["event-type:#{event.class.name.underscore}", "event-id:#{event.id}"])
57
+ raise
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,9 @@
1
+ require 'trabox/relay/relayable'
2
+ require 'trabox/relay/relayer'
3
+ require 'trabox/relay/relayable_models'
4
+
5
+ module Trabox
6
+ module Relay
7
+ DEFAULT_SELECT_LIMIT = 3
8
+ end
9
+ end
@@ -0,0 +1,63 @@
1
+ module Trabox
2
+ module Subscriber
3
+ module Google
4
+ module Cloud
5
+ class PubSub
6
+ include Subscriber
7
+
8
+ # @param subscription_id [String]
9
+ # @param listen_options [Hash] listen method options
10
+ # @param before_listen_acknowledge_callbacks [Array<Proc>]
11
+ # @param after_listen_acknowledge_callbacks [Array<Proc>]
12
+ # @param error_listen_callbacks [Array<Proc>]
13
+ def initialize(subscription_id,
14
+ listen_options: {},
15
+ before_listen_acknowledge_callbacks: [],
16
+ after_listen_acknowledge_callbacks: [],
17
+ error_listen_callbacks: [])
18
+
19
+ @listen_options = listen_options
20
+ @before_listen_acknowledge_callbacks = before_listen_acknowledge_callbacks
21
+ @after_listen_acknowledge_callbacks = after_listen_acknowledge_callbacks
22
+ @error_listen_callbacks = error_listen_callbacks
23
+
24
+ @pubsub = ::Google::Cloud::PubSub.new
25
+
26
+ @subscription = @pubsub.subscription subscription_id
27
+
28
+ raise "Subscription-ID='#{subscription_id}' does not exist." if @subscription.nil?
29
+
30
+ Rails.logger.info "Subscription '#{subscription_id}': message ordering is #{@subscription.message_ordering?}."
31
+ end
32
+
33
+ def subscribe
34
+ subscriber = @subscription.listen(**@listen_options) do |received_message|
35
+ @before_listen_acknowledge_callbacks.each do |cb|
36
+ cb.call(received_message)
37
+ end
38
+
39
+ received_message.acknowledge!
40
+
41
+ @after_listen_acknowledge_callbacks.each do |cb|
42
+ cb.call(received_message)
43
+ end
44
+
45
+ Metric.service_check('subscribe.service.check', Metric::SERVICE_OK)
46
+ end
47
+
48
+ subscriber.on_error do |_|
49
+ Metric.service_check('subscribe.service.check', Metric::SERVICE_CRITICAL)
50
+ end
51
+
52
+ @error_listen_callbacks.each do |cb|
53
+ subscriber.on_error(&cb)
54
+ end
55
+
56
+ Rails.logger.info 'Listening subscrition...'
57
+ subscriber.start.wait!
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,7 @@
1
+ require 'trabox/subscriber/google/cloud_pubsub'
2
+
3
+ module Trabox
4
+ module Subscriber
5
+ def subscribe; end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Trabox
2
+ VERSION = '0.1.1'
3
+ end
data/lib/trabox.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'trabox/version'
2
+ require 'trabox/railtie'
3
+
4
+ require 'trabox/metric'
5
+ require 'trabox/relay'
6
+ require 'trabox/publisher'
7
+ require 'trabox/subscriber'
8
+ require 'trabox/command'
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: trabox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - kosay
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-12-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dogstatsd-ruby
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: google-cloud-pubsub
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mysql2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: optparse
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Transactional-Outbox for Rails
84
+ email:
85
+ - ekr59uv25@gmail.com
86
+ executables:
87
+ - trabox
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - MIT-LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - exe/trabox
95
+ - lib/generators/trabox/configure/USAGE
96
+ - lib/generators/trabox/configure/configure_generator.rb
97
+ - lib/generators/trabox/configure/templates/initializer.rb
98
+ - lib/generators/trabox/model/USAGE
99
+ - lib/generators/trabox/model/model_generator.rb
100
+ - lib/tasks/trabox_tasks.rake
101
+ - lib/trabox.rb
102
+ - lib/trabox/command.rb
103
+ - lib/trabox/command/parser.rb
104
+ - lib/trabox/commands/common/configuration.rb
105
+ - lib/trabox/commands/common/runner.rb
106
+ - lib/trabox/commands/relay.rb
107
+ - lib/trabox/commands/relay/argument_parser.rb
108
+ - lib/trabox/commands/relay/configuration.rb
109
+ - lib/trabox/commands/subscribe.rb
110
+ - lib/trabox/commands/subscribe/argument_parser.rb
111
+ - lib/trabox/commands/subscribe/configuration.rb
112
+ - lib/trabox/metric.rb
113
+ - lib/trabox/publisher.rb
114
+ - lib/trabox/publisher/google/cloud_pubsub.rb
115
+ - lib/trabox/railtie.rb
116
+ - lib/trabox/relay.rb
117
+ - lib/trabox/relay/relayable.rb
118
+ - lib/trabox/relay/relayable_models.rb
119
+ - lib/trabox/relay/relayer.rb
120
+ - lib/trabox/subscriber.rb
121
+ - lib/trabox/subscriber/google/cloud_pubsub.rb
122
+ - lib/trabox/version.rb
123
+ homepage: https://github.com/sarub0b0/trabox
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.3.7
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Transactional-Outbox for Rails
146
+ test_files: []