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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tobox
4
+ VERSION = "0.1.0"
5
+ end
@@ -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: []