tobox 0.1.0

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