trabox 0.1.1

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