tobox 0.1.0
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/CHANGELOG.md +10 -0
- data/LICENSE.txt +191 -0
- data/README.md +384 -0
- data/exe/tobox +14 -0
- data/lib/tobox/application.rb +32 -0
- data/lib/tobox/cli.rb +145 -0
- data/lib/tobox/configuration.rb +122 -0
- data/lib/tobox/fetcher.rb +139 -0
- data/lib/tobox/plugins/datadog/configuration.rb +34 -0
- data/lib/tobox/plugins/datadog/integration.rb +39 -0
- data/lib/tobox/plugins/datadog/patcher.rb +26 -0
- data/lib/tobox/plugins/datadog.rb +102 -0
- data/lib/tobox/plugins/sentry.rb +143 -0
- data/lib/tobox/plugins/zeitwerk.rb +52 -0
- data/lib/tobox/pool/fiber_pool.rb +49 -0
- data/lib/tobox/pool/threaded_pool.rb +55 -0
- data/lib/tobox/pool.rb +19 -0
- data/lib/tobox/version.rb +5 -0
- data/lib/tobox/worker.rb +49 -0
- data/lib/tobox.rb +41 -0
- metadata +84 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobox
|
4
|
+
module Plugins
|
5
|
+
module Sentry
|
6
|
+
class Configuration
|
7
|
+
# Set this option to true if you want Sentry to only capture the last job
|
8
|
+
# retry if it fails.
|
9
|
+
attr_accessor :report_after_retries
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@report_after_retries = false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class EventHandler
|
17
|
+
TOBOX_NAME = "tobox"
|
18
|
+
|
19
|
+
def initialize(config)
|
20
|
+
@config = config
|
21
|
+
@db_table = @config[:table]
|
22
|
+
@db_scheme = URI(@config[:database_uri]).scheme if @config[:database_uri]
|
23
|
+
@max_attempts = @config[:max_attempts]
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_start(event)
|
27
|
+
return unless ::Sentry.initialized?
|
28
|
+
|
29
|
+
::Sentry.clone_hub_to_current_thread
|
30
|
+
|
31
|
+
scope = ::Sentry.get_current_scope
|
32
|
+
|
33
|
+
scope.set_contexts(
|
34
|
+
id: event[:id],
|
35
|
+
type: event[:type],
|
36
|
+
attempts: event[:attempts],
|
37
|
+
created_at: event[:created_at],
|
38
|
+
run_at: event[:run_at],
|
39
|
+
last_error: event[:last_error]&.byteslice(0..1000),
|
40
|
+
version: Tobox::VERSION,
|
41
|
+
db_adapter: @db_scheme
|
42
|
+
)
|
43
|
+
scope.set_tags(
|
44
|
+
outbox: @db_table,
|
45
|
+
event_id: event[:id],
|
46
|
+
event_type: event[:type]
|
47
|
+
)
|
48
|
+
|
49
|
+
scope.set_transaction_name("#{TOBOX_NAME}/#{event[:type]}") unless scope.transaction_name
|
50
|
+
|
51
|
+
transaction = start_transaction(scope.transaction_name, event[:metadata].to_h["sentry_trace"])
|
52
|
+
|
53
|
+
return unless transaction
|
54
|
+
|
55
|
+
scope.set_span(transaction)
|
56
|
+
|
57
|
+
# good for thread pool, good for fiber pool
|
58
|
+
store_transaction(event, transaction)
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_finish(event)
|
62
|
+
return unless ::Sentry.initialized?
|
63
|
+
|
64
|
+
transaction = retrieve_transaction(event)
|
65
|
+
|
66
|
+
return unless transaction
|
67
|
+
|
68
|
+
finish_transaction(transaction, 200)
|
69
|
+
|
70
|
+
scope = ::Sentry.get_current_scope
|
71
|
+
scope.clear
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_error(event, error)
|
75
|
+
return unless ::Sentry.initialized?
|
76
|
+
|
77
|
+
transaction = retrieve_transaction(event)
|
78
|
+
|
79
|
+
return unless transaction
|
80
|
+
|
81
|
+
capture_exception(event, error)
|
82
|
+
|
83
|
+
finish_transaction(transaction, 500)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def start_transaction(transaction_name, sentry_trace)
|
89
|
+
options = { name: transaction_name, op: "tobox" }
|
90
|
+
transaction = ::Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace
|
91
|
+
::Sentry.start_transaction(transaction: transaction, **options)
|
92
|
+
end
|
93
|
+
|
94
|
+
def finish_transaction(transaction, status)
|
95
|
+
transaction.set_http_status(status)
|
96
|
+
transaction.finish
|
97
|
+
end
|
98
|
+
|
99
|
+
def store_transaction(event, transaction)
|
100
|
+
store = (Thread.current[:tobox_sentry_transactions] ||= {})
|
101
|
+
|
102
|
+
store[event[:id]] = transaction
|
103
|
+
end
|
104
|
+
|
105
|
+
def retrieve_transaction(event)
|
106
|
+
return unless (store = Thread.current[:tobox_sentry_transactions])
|
107
|
+
|
108
|
+
store.delete(event[:id])
|
109
|
+
end
|
110
|
+
|
111
|
+
def capture_exception(event, error)
|
112
|
+
return unless ::Sentry.configuration.tobox.report_after_retries && event[:attempts] >= @max_attempts
|
113
|
+
|
114
|
+
::Sentry.capture_exception(
|
115
|
+
error,
|
116
|
+
hint: { background: false }
|
117
|
+
)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class << self
|
122
|
+
def load_dependencies(*)
|
123
|
+
require "uri"
|
124
|
+
require "sentry-ruby"
|
125
|
+
end
|
126
|
+
|
127
|
+
def configure(config)
|
128
|
+
event_handler = EventHandler.new(config)
|
129
|
+
config.on_before_event(&event_handler.method(:on_start))
|
130
|
+
config.on_after_event(&event_handler.method(:on_finish))
|
131
|
+
config.on_error_event(&event_handler.method(:on_error))
|
132
|
+
|
133
|
+
::Sentry::Configuration.attr_reader(:tobox)
|
134
|
+
::Sentry::Configuration.add_post_initialization_callback do
|
135
|
+
@tobox = Plugins::Sentry::Configuration.new
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
register_plugin :sentry, Sentry
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobox
|
4
|
+
module Plugins
|
5
|
+
module Zeitwerk
|
6
|
+
module ConfigurationMethods
|
7
|
+
def zeitwerk_loader(loader = nil, &blk)
|
8
|
+
if loader
|
9
|
+
@zeitwerk_loader = loader
|
10
|
+
elsif blk
|
11
|
+
@zeitwerk_loader ||= ::Zeitwerk::Loader.new
|
12
|
+
yield(@zeitwerk_loader)
|
13
|
+
elsif !(loader || blk)
|
14
|
+
@zeitwerk_loader
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def freeze
|
19
|
+
loader = @zeitwerk_loader
|
20
|
+
|
21
|
+
return super unless loader
|
22
|
+
|
23
|
+
if @config[:environment] == "production"
|
24
|
+
loader.setup
|
25
|
+
::Zeitwerk::Loader.eager_load_all
|
26
|
+
else
|
27
|
+
loader.enable_reloading
|
28
|
+
loader.setup
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def load_dependencies(*)
|
37
|
+
require "zeitwerk"
|
38
|
+
end
|
39
|
+
|
40
|
+
def configure(config)
|
41
|
+
loader = config.zeitwerk_loader
|
42
|
+
|
43
|
+
return unless loader
|
44
|
+
|
45
|
+
config.on_before_event { |*| loader.reload }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
register_plugin :zeitwerk, Zeitwerk
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "fiber_scheduler"
|
5
|
+
|
6
|
+
module Tobox
|
7
|
+
class FiberPool < Pool
|
8
|
+
class KillError < Interrupt; end
|
9
|
+
|
10
|
+
def initialize(_configuration)
|
11
|
+
Sequel.extension(:fiber_concurrency)
|
12
|
+
super
|
13
|
+
@error_handlers = Array(@configuration.lifecycle_events[:error])
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
@fiber_thread = Thread.start do
|
18
|
+
Thread.current.name = "tobox-fibers-thread"
|
19
|
+
|
20
|
+
FiberScheduler do
|
21
|
+
@workers.each_with_index do |wk, _idx|
|
22
|
+
Fiber.schedule do
|
23
|
+
wk.work
|
24
|
+
rescue KillError
|
25
|
+
# noop
|
26
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
27
|
+
@error_handlers.each { |hd| hd.call(:tobox_error, e) }
|
28
|
+
raise e
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def stop
|
36
|
+
shutdown_timeout = @configuration[:shutdown_timeout]
|
37
|
+
|
38
|
+
super
|
39
|
+
|
40
|
+
begin
|
41
|
+
Timeout.timeout(shutdown_timeout) { @fiber_thread.value }
|
42
|
+
rescue Timeout::Error
|
43
|
+
# hard exit
|
44
|
+
@fiber_thread.raise(KillError)
|
45
|
+
@fiber_thread.value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobox
|
4
|
+
class ThreadedPool < Pool
|
5
|
+
class KillError < Interrupt; end
|
6
|
+
|
7
|
+
def initialize(_configuration)
|
8
|
+
@threads = []
|
9
|
+
super
|
10
|
+
@error_handlers = Array(@configuration.lifecycle_events[:error])
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
@workers.each_with_index do |wk, idx|
|
15
|
+
th = Thread.start do
|
16
|
+
Thread.current.name = "tobox-worker-#{idx}"
|
17
|
+
|
18
|
+
begin
|
19
|
+
wk.work
|
20
|
+
rescue KillError
|
21
|
+
# noop
|
22
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
23
|
+
@error_handlers.each { |hd| hd.call(:tobox_error, e) }
|
24
|
+
raise e
|
25
|
+
end
|
26
|
+
|
27
|
+
@threads.delete(Thread.current)
|
28
|
+
end
|
29
|
+
@threads << th
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
shutdown_timeout = @configuration[:shutdown_timeout]
|
35
|
+
|
36
|
+
deadline = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
37
|
+
|
38
|
+
super
|
39
|
+
Thread.pass # let workers finish
|
40
|
+
|
41
|
+
# soft exit
|
42
|
+
while Process.clock_gettime(::Process::CLOCK_MONOTONIC) - deadline < shutdown_timeout
|
43
|
+
return if @threads.empty?
|
44
|
+
|
45
|
+
sleep 0.5
|
46
|
+
end
|
47
|
+
|
48
|
+
# hard exit
|
49
|
+
@threads.each { |th| th.raise(KillError) }
|
50
|
+
while (th = @threads.pop)
|
51
|
+
th.value # waits
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/tobox/pool.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobox
|
4
|
+
class Pool
|
5
|
+
def initialize(configuration)
|
6
|
+
@configuration = configuration
|
7
|
+
@num_workers = configuration[:concurrency]
|
8
|
+
@workers = Array.new(@num_workers) { Worker.new(configuration) }
|
9
|
+
start
|
10
|
+
end
|
11
|
+
|
12
|
+
def stop
|
13
|
+
@workers.each(&:finish!)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
autoload :ThreadedPool, File.join(__dir__, "pool", "threaded_pool")
|
18
|
+
autoload :FiberPool, File.join(__dir__, "pool", "fiber_pool")
|
19
|
+
end
|
data/lib/tobox/worker.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tobox
|
4
|
+
class Worker
|
5
|
+
def initialize(configuration)
|
6
|
+
@wait_for_events_delay = configuration[:wait_for_events_delay]
|
7
|
+
@handlers = configuration.handlers || {}
|
8
|
+
@fetcher = Fetcher.new(configuration)
|
9
|
+
@finished = false
|
10
|
+
|
11
|
+
return unless (message_to_arguments = configuration.arguments_handler)
|
12
|
+
|
13
|
+
define_singleton_method(:message_to_arguments, &message_to_arguments)
|
14
|
+
end
|
15
|
+
|
16
|
+
def finish!
|
17
|
+
@finished = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def work
|
21
|
+
do_work until @finished
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def do_work
|
27
|
+
return if @finished
|
28
|
+
|
29
|
+
sum_fetched_events = @fetcher.fetch_events do |event|
|
30
|
+
event_type = event[:type].to_sym
|
31
|
+
args = message_to_arguments(event)
|
32
|
+
|
33
|
+
if @handlers.key?(event_type)
|
34
|
+
@handlers[event_type].each do |handler|
|
35
|
+
handler.call(args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
return if @finished
|
41
|
+
|
42
|
+
sleep(@wait_for_events_delay) if sum_fetched_events.zero?
|
43
|
+
end
|
44
|
+
|
45
|
+
def message_to_arguments(event)
|
46
|
+
event
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/tobox.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sequel"
|
4
|
+
|
5
|
+
require_relative "tobox/version"
|
6
|
+
|
7
|
+
require "mutex_m"
|
8
|
+
|
9
|
+
module Tobox
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
module Plugins
|
13
|
+
@plugins = {}
|
14
|
+
@plugins.extend(Mutex_m)
|
15
|
+
|
16
|
+
# Loads a plugin based on a name. If the plugin hasn't been loaded, tries to load
|
17
|
+
# it from the load path under "httpx/plugins/" directory.
|
18
|
+
#
|
19
|
+
def self.load_plugin(name)
|
20
|
+
h = @plugins
|
21
|
+
unless (plugin = h.synchronize { h[name] })
|
22
|
+
require "tobox/plugins/#{name}"
|
23
|
+
raise "Plugin #{name} hasn't been registered" unless (plugin = h.synchronize { h[name] })
|
24
|
+
end
|
25
|
+
plugin
|
26
|
+
end
|
27
|
+
|
28
|
+
# Registers a plugin (+mod+) in the central store indexed by +name+.
|
29
|
+
#
|
30
|
+
def self.register_plugin(name, mod)
|
31
|
+
h = @plugins
|
32
|
+
h.synchronize { h[name] = mod }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require_relative "tobox/configuration"
|
38
|
+
require_relative "tobox/fetcher"
|
39
|
+
require_relative "tobox/worker"
|
40
|
+
require_relative "tobox/pool"
|
41
|
+
require_relative "tobox/application"
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tobox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- HoneyryderChuck
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-09-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sequel
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.35'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.35'
|
27
|
+
description: Transactional outbox pattern implementation in ruby
|
28
|
+
email:
|
29
|
+
- cardoso_tiago@hotmail.com
|
30
|
+
executables:
|
31
|
+
- tobox
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- CHANGELOG.md
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- exe/tobox
|
39
|
+
- lib/tobox.rb
|
40
|
+
- lib/tobox/application.rb
|
41
|
+
- lib/tobox/cli.rb
|
42
|
+
- lib/tobox/configuration.rb
|
43
|
+
- lib/tobox/fetcher.rb
|
44
|
+
- lib/tobox/plugins/datadog.rb
|
45
|
+
- lib/tobox/plugins/datadog/configuration.rb
|
46
|
+
- lib/tobox/plugins/datadog/integration.rb
|
47
|
+
- lib/tobox/plugins/datadog/patcher.rb
|
48
|
+
- lib/tobox/plugins/sentry.rb
|
49
|
+
- lib/tobox/plugins/zeitwerk.rb
|
50
|
+
- lib/tobox/pool.rb
|
51
|
+
- lib/tobox/pool/fiber_pool.rb
|
52
|
+
- lib/tobox/pool/threaded_pool.rb
|
53
|
+
- lib/tobox/version.rb
|
54
|
+
- lib/tobox/worker.rb
|
55
|
+
homepage: https://gitlab.com/honeyryderchuck/tobox
|
56
|
+
licenses: []
|
57
|
+
metadata:
|
58
|
+
homepage_uri: https://gitlab.com/honeyryderchuck/tobox
|
59
|
+
allowed_push_host: https://rubygems.org
|
60
|
+
source_code_uri: https://gitlab.com/honeyryderchuck/tobox
|
61
|
+
bug_tracker_uri: https://gitlab.com/honeyryderchuck/tobox/issues
|
62
|
+
documentation_uri: https://gitlab.com/honeyryderchuck/tobox
|
63
|
+
changelog_uri: https://gitlab.com/honeyryderchuck/tobox/-/blob/master/CHANGELOG.md
|
64
|
+
rubygems_mfa_required: 'true'
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 2.6.0
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubygems_version: 3.3.7
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Transactional outbox pattern implementation in ruby
|
84
|
+
test_files: []
|