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