upfluence-utils 0.12.20 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1153c2254907a24cfdf4158ecba796e11c64d33c16e1c768dc1d8bbbb373ffd
4
- data.tar.gz: 18af6544045d8f1d8a9378be19e6e09daacc6ed5102b8a8a4af60af7516f1ff1
3
+ metadata.gz: a28871a5df31d6b75af4475a60543e0810fd0094dea6d724afbd848a71607761
4
+ data.tar.gz: ac59cc58a69b360c825e79abec46da346eff81e43ca87e9d4243c49fc89dfb3e
5
5
  SHA512:
6
- metadata.gz: ac346ac44165b16a01c5f3e0ffd4a32bc1c78337d50b033b422cf23dcd6b3662cade01d40b51ac242331cf629f94c8f8bc442cc79e6690d302160942beacd28c
7
- data.tar.gz: 446c23bf81c04fead4cc60a6b70c8729372250654a59c3deedccc40e4c59c2c1c34ff181818a445a6115d4042d94231f48633ff5d4514f37979a63aedc3afa42
6
+ metadata.gz: bdc88bee6a3b265e0edebd6697f7ce7f350c83f5746c285e02057ee89e2a5e52ce215ff35e0e5e6de2b36db0722d3fa96e5955a627610d02acfcfb29f32ced9a
7
+ data.tar.gz: 420de9021031ed18c35dd8bcca08892a6db87a4e1aaa4efb4495c338fbe66c157103919e6ae4eedf263a1ec8e50aa29bf20b08d9149f4633b6edfffa76e897f8
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'upfluence/thread'
4
+
5
+ module Upfluence
6
+ CONTEXT_KEY = :upfluence_context
7
+
8
+ class Context
9
+ attr_reader :deadline
10
+
11
+ def initialize
12
+ @deadline = nil
13
+ end
14
+
15
+ def timeout
16
+ return nil if @deadline.nil?
17
+
18
+ [(@deadline - Time.now).to_f, 0.0].max
19
+ end
20
+
21
+ def with_deadline(deadline, override: false)
22
+ previous = @deadline
23
+ @deadline = override || previous.nil? ? deadline : [previous, deadline].min
24
+
25
+ yield
26
+ ensure
27
+ @deadline = previous
28
+ end
29
+
30
+ def with_timeout(duration, override: false)
31
+ with_deadline(Time.now + duration, override: override) { yield }
32
+ end
33
+
34
+ class ThreadWrapper
35
+ class << self
36
+ def wrap_thread(thr, block)
37
+ parent_deadline = thr[CONTEXT_KEY]&.deadline
38
+
39
+ return block.call unless parent_deadline
40
+
41
+ Upfluence.context.with_deadline(parent_deadline) { block.call }
42
+ end
43
+ end
44
+ end
45
+
46
+ ::Upfluence::Thread.register_wrapper :context, ThreadWrapper
47
+ end
48
+
49
+ class << self
50
+ def context
51
+ Thread.current[CONTEXT_KEY] ||= Context.new
52
+ end
53
+ end
54
+ end
@@ -1,4 +1,4 @@
1
- require 'sinatra'
1
+ require 'sinatra/base'
2
2
  require 'upfluence/http/endpoint/api_endpoint'
3
3
 
4
4
  module Upfluence
@@ -1,4 +1,4 @@
1
- require 'sinatra'
1
+ require 'sinatra/base'
2
2
  require 'active_record'
3
3
  require 'active_support/hash_with_indifferent_access'
4
4
  require 'upfluence/http/endpoint/validation_error'
@@ -1,14 +1,20 @@
1
+ require 'upfluence/context'
2
+
1
3
  module Upfluence
2
4
  module HTTP
3
5
  module Middleware
4
6
  class RequestStapler
5
- def initialize(app)
7
+ def initialize(app, timeout: nil)
8
+ @timeout = timeout
6
9
  @app = app
7
10
  end
8
11
 
9
12
  def call(env)
10
13
  Server.request = Rack::Request.new(env)
11
- @app.call(env)
14
+
15
+ return @app.call(env) unless @timeout
16
+
17
+ Upfluence.context.with_timeout(@timeout) { @app.call(env) }
12
18
  end
13
19
  end
14
20
  end
@@ -3,7 +3,7 @@ require 'rack/etag'
3
3
  require 'rack/timeout/base'
4
4
  require 'prometheus/client'
5
5
  require 'prometheus/client/push'
6
- require "prometheus/middleware/exporter"
6
+ require 'prometheus/middleware/exporter'
7
7
 
8
8
  require 'upfluence/environment'
9
9
  require 'upfluence/error_logger'
@@ -48,20 +48,78 @@ module Upfluence
48
48
  Instrumentation::GCInstrumenter.new,
49
49
  Instrumentation::ActiveRecordPoolInstrumenter.new
50
50
  ],
51
+ admin_port: ENV.fetch('ADMIN_PORT', nil),
51
52
  debug: ENV.fetch('DEBUG', nil)
52
53
  }
53
54
 
54
55
  def initialize(options = {}, &block)
55
56
  @options = DEFAULT_OPTIONS.dup.merge(options)
56
57
  opts = @options
57
- base_handler = nil
58
58
 
59
59
  if opts[:base_handler_klass]
60
- base_handler = opts[:base_handler_klass].new(@options[:interfaces])
60
+ @base_handler = opts[:base_handler_klass].new(opts[:interfaces])
61
61
  end
62
62
 
63
- @builder = Builder.new do
64
- use Middleware::RequestStapler
63
+ @admin_builder = admin_builder(opts) if opts[:admin_port]
64
+ @production_builder = production_builder(opts, &block)
65
+ @handler = Rack::Handler.get(opts[:server])
66
+ end
67
+
68
+ def serve
69
+ ENV['RACK_ENV'] = Upfluence.env.to_s
70
+
71
+ Thread.new { run_prometheus_exporter } if @options[:push_gateway_url]
72
+
73
+ @options[:instrumentations].each(&:start)
74
+
75
+ if @admin_builder
76
+ admin_port = @options[:admin_port].to_i
77
+
78
+ Thread.new do
79
+ run_builder(@admin_builder, Port: admin_port)
80
+ end
81
+
82
+ Upfluence.logger.info(
83
+ "Admin server listening on #{@options[:Host]}:#{admin_port}"
84
+ )
85
+ end
86
+
87
+ run_builder(@production_builder)
88
+ end
89
+
90
+ class << self
91
+ def request
92
+ Thread.current[REQUEST_CONTEXT_KEY]
93
+ end
94
+
95
+ def request=(req)
96
+ Thread.current[REQUEST_CONTEXT_KEY] = req
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def run_builder(builder, **overrides)
103
+ opts = @options.merge(overrides)
104
+
105
+ @handler.run(builder, **opts) do |server|
106
+ server.threaded = opts[:threaded] if server.respond_to? :threaded=
107
+
108
+ # Thin does not recognize the max_thread argument, howerver it has a
109
+ # threadpool_size setter. Puma on the other hand recognize max_thread.
110
+ if server.respond_to?(:threadpool_size=) && opts[:max_threads]
111
+ server.threadpool_size = opts[:max_threads]
112
+ end
113
+ end
114
+ end
115
+
116
+ def production_builder(opts, &block)
117
+ base_handler = @base_handler
118
+ has_admin = !opts[:admin_port].nil?
119
+ mount = method(:mount_admin_endpoints)
120
+
121
+ Builder.new do
122
+ use Middleware::RequestStapler, timeout: opts[:request_timeout]
65
123
  use Middleware::Logger
66
124
  use Middleware::Prometheus
67
125
  use Middleware::ApplicationHeaders, base_handler
@@ -72,7 +130,6 @@ module Upfluence
72
130
  end
73
131
 
74
132
  use Upfluence.error_logger.middleware
75
- use Prometheus::Middleware::Exporter if opts[:prometheus_endpoint]
76
133
 
77
134
  use Rack::ContentLength
78
135
  use Rack::Chunked
@@ -86,54 +143,46 @@ module Upfluence
86
143
  use(*m)
87
144
  end
88
145
 
146
+ mount.call(self, opts) unless has_admin
147
+
89
148
  map '/healthcheck' do
90
149
  run(opts[:healthcheck_endpoint] || Endpoint::Healthcheck.new)
91
150
  end
92
151
 
93
- if opts[:base_processor_klass] && base_handler
94
- map '/base' do
95
- run_thrift(opts[:base_processor_klass], base_handler)
96
- end
97
- end
98
-
99
- map('/debug') { run(Endpoint::Profiler.new) } if opts[:debug]
100
-
101
152
  instance_eval(&block)
102
153
  end
103
-
104
- @handler = Rack::Handler.get(@options[:server])
105
154
  end
106
155
 
107
- def serve
108
- ENV['RACK_ENV'] = Upfluence.env.to_s
109
-
110
- Thread.new { run_prometheus_exporter } if @options[:push_gateway_url]
111
-
112
- @options[:instrumentations].each(&:start)
156
+ def admin_builder(opts)
157
+ mount = method(:mount_admin_endpoints)
113
158
 
114
- @handler.run(@builder, **@options) do |server|
115
- server.threaded = @options[:threaded] if server.respond_to? :threaded=
159
+ Builder.new do
160
+ use Rack::ContentLength
116
161
 
117
- # Thin does not recognize the max_thread argument, howerver it has a
118
- # threadpool_size setter. Puma on the other hand recognize max_thread.
119
- if server.respond_to?(:threadpool_size=) && @options[:max_threads]
120
- server.threadpool_size = @options[:max_threads]
162
+ map '/healthcheck' do
163
+ run(opts[:healthcheck_endpoint] || Endpoint::Healthcheck.new)
121
164
  end
165
+
166
+ mount.call(self, opts)
122
167
  end
123
168
  end
124
169
 
125
- class << self
126
- def request
127
- Thread.current[REQUEST_CONTEXT_KEY]
128
- end
170
+ def mount_admin_endpoints(builder, opts)
171
+ base_handler = @base_handler
129
172
 
130
- def request=(req)
131
- Thread.current[REQUEST_CONTEXT_KEY] = req
173
+ builder.instance_eval do
174
+ use Prometheus::Middleware::Exporter if opts[:prometheus_endpoint]
175
+
176
+ if opts[:base_processor_klass] && base_handler
177
+ map '/base' do
178
+ run_thrift(opts[:base_processor_klass], base_handler)
179
+ end
180
+ end
181
+
182
+ map('/debug') { run(Endpoint::Profiler.new) } if opts[:debug]
132
183
  end
133
184
  end
134
185
 
135
- private
136
-
137
186
  def run_prometheus_exporter
138
187
  push = Prometheus::Client::Push.new(
139
188
  @options[:app_name],
@@ -1,11 +1,82 @@
1
- require 'thread'
2
- require 'timeout'
1
+ require 'upfluence/timeout'
3
2
 
4
3
  module Upfluence
5
4
  class Pool
6
5
  class UnknownResource < RuntimeError; end
7
6
  class NoInstanciationBlock < RuntimeError; end
8
7
 
8
+ class PrometheusMetrics
9
+ def self.standardized_metrics(n)
10
+ require 'prometheus/client'
11
+ new(Prometheus::Client.registry, 'upfluence_iopool_pool', { pool: n })
12
+ end
13
+
14
+ def initialize(registry, prefix, labels)
15
+ label_keys = labels.keys
16
+
17
+ @labels = labels
18
+ @get = register(registry, :counter, :"#{prefix}_get_total", 'Total pool get calls', label_keys)
19
+ @put = register(registry, :counter, :"#{prefix}_put_total", 'Total pool put calls', label_keys)
20
+ @discard = register(registry, :counter, :"#{prefix}_discard_total", 'Total pool discard calls',
21
+ label_keys)
22
+ @get_wait = register(registry, :counter, :"#{prefix}_get_wait_total", 'Pool gets that had to wait',
23
+ label_keys)
24
+ @get_wait_duration = register(registry, :counter, :"#{prefix}_get_wait_millisecond_total",
25
+ 'Total milliseconds waiting for pool get', label_keys)
26
+ @idle = register(registry, :gauge, :"#{prefix}_idle", 'Idle resources in pool', label_keys)
27
+ @checkout = register(registry, :gauge, :"#{prefix}_checkout", 'Checked out resources in pool',
28
+ label_keys)
29
+ @size = register(registry, :gauge, :"#{prefix}_size", 'Total pool size', label_keys)
30
+ end
31
+
32
+ def observe_get
33
+ @get.increment(labels: @labels)
34
+ end
35
+
36
+ def observe_put
37
+ @put.increment(labels: @labels)
38
+ end
39
+
40
+ def observe_discard
41
+ @discard.increment(labels: @labels)
42
+ end
43
+
44
+ def observe_wait(seconds)
45
+ @get_wait.increment(labels: @labels)
46
+ @get_wait_duration.increment(by: (seconds * 1000).to_i, labels: @labels)
47
+ end
48
+
49
+ def update_idle(count)
50
+ @idle.set(count, labels: @labels)
51
+ end
52
+
53
+ def update_checkout(count)
54
+ @checkout.set(count, labels: @labels)
55
+ end
56
+
57
+ def set_size(count)
58
+ @size.set(count, labels: @labels)
59
+ end
60
+
61
+ private
62
+
63
+ def register(registry, type, name, docstring, label_keys)
64
+ registry.public_send(type, name, docstring: docstring, labels: label_keys)
65
+ rescue Prometheus::Client::Registry::AlreadyRegisteredError
66
+ registry.get(name)
67
+ end
68
+ end
69
+
70
+ class NullMetrics
71
+ def observe_get; end
72
+ def observe_put; end
73
+ def observe_discard; end
74
+ def observe_wait(_seconds); end
75
+ def update_idle(_count); end
76
+ def update_checkout(_count); end
77
+ def set_size(_count); end
78
+ end
79
+
9
80
  attr_reader :max
10
81
 
11
82
  def initialize(size = 0, options = {}, &block)
@@ -19,37 +90,63 @@ module Upfluence
19
90
  @resource = ConditionVariable.new
20
91
  @shutdown_block = nil
21
92
  @timeout = options.fetch :timeout, 5
93
+ @metrics = options.fetch(:metrics, NullMetrics.new)
94
+
95
+ @metrics.set_size(size)
22
96
  end
23
97
 
24
98
  def discard(obj)
99
+ @metrics.observe_discard
100
+
25
101
  @mutex.synchronize do
26
102
  remove_from_redeemed obj
103
+ @metrics.update_checkout(@redeemed.length)
27
104
  @resource.broadcast
28
105
  end
29
106
  end
30
107
 
31
108
  def push(obj)
109
+ @metrics.observe_put
110
+
32
111
  @mutex.synchronize do
33
112
  remove_from_redeemed obj
34
113
 
35
114
  @que.push obj
115
+ @metrics.update_idle(@que.length)
116
+ @metrics.update_checkout(@redeemed.length)
36
117
  @resource.broadcast
37
118
  end
38
119
  end
39
120
 
40
121
  def pop(options = {})
41
122
  timeout = options.fetch :timeout, @timeout
42
- deadline = Time.now + timeout
123
+ effective = [timeout, Upfluence.context.timeout].compact.min
124
+ deadline = Time.now + effective
125
+ t0 = nil
126
+
127
+ @metrics.observe_get
43
128
 
44
129
  @mutex.synchronize do
45
130
  loop do
46
- return redeem(@que.pop) unless @que.empty?
131
+ unless @que.empty?
132
+ result = redeem(@que.pop)
133
+ @metrics.update_idle(@que.length)
134
+ @metrics.observe_wait(Time.now - t0) if t0
135
+ return result
136
+ end
47
137
 
48
- connection = redeem(try_create(options))
49
- return connection if connection
138
+ connection = try_create(options)
139
+
140
+ if connection
141
+ result = redeem(connection)
142
+ @metrics.observe_wait(Time.now - t0) if t0
143
+ return result
144
+ end
50
145
 
51
146
  to_wait = deadline - Time.now
52
- raise Timeout::Error, "Waited #{timeout} sec" if to_wait <= 0
147
+ raise ::Timeout::Error.new("Waited #{timeout} sec") if to_wait <= 0
148
+
149
+ t0 ||= Time.now
53
150
  @resource.wait(@mutex, to_wait)
54
151
  end
55
152
  end
@@ -66,15 +163,14 @@ module Upfluence
66
163
  private
67
164
 
68
165
  def remove_from_redeemed(obj)
69
- unless @redeemed.map(&:object_id).include? obj.object_id
70
- raise UnknownResource
71
- end
166
+ raise UnknownResource unless @redeemed.map(&:object_id).include? obj.object_id
72
167
 
73
- @redeemed.reject! { |e| e.object_id == obj.object_id }
168
+ @redeemed.reject! { |e| e.equal?(obj) }
74
169
  end
75
170
 
76
171
  def redeem(obj)
77
172
  @redeemed << obj if obj
173
+ @metrics.update_checkout(@redeemed.length)
78
174
  obj
79
175
  end
80
176
 
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+ require 'upfluence/context'
5
+
6
+ module Upfluence
7
+ module Timeout
8
+ def self.timeout(sec, klass = nil, message = nil, &block)
9
+ effective = [sec, Upfluence.context.timeout].compact.min
10
+
11
+ raise(klass || ::Timeout::Error, message) if effective <= 0
12
+
13
+ Upfluence.context.with_timeout(effective) do
14
+ ::Timeout.timeout(effective, klass, message, &block)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,5 @@
1
+ require 'upfluence/timeout'
2
+
1
3
  module Upfluence
2
4
  module Utils
3
5
  module Thrift
@@ -9,7 +11,7 @@ module Upfluence
9
11
  end
10
12
 
11
13
  def method_missing(method, *args, &block)
12
- ::Timeout.timeout(@duration) { @app.send(method, *args, &block) }
14
+ Upfluence::Timeout.timeout(@duration) { @app.send(method, *args, &block) }
13
15
  rescue ::Timeout::Error
14
16
  raise ::Thrift::ApplicationException.new(
15
17
  ::Thrift::ApplicationException::INTERNAL_ERROR,
@@ -1,5 +1,5 @@
1
1
  module Upfluence
2
2
  module Utils
3
- VERSION = '0.12.20'.freeze
3
+ VERSION = '0.13.0'.freeze
4
4
  end
5
5
  end
data/lib/upfluence.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'upfluence/context'
1
2
  require 'upfluence/utils'
2
3
  require 'upfluence/endpoint/api_endpoint'
3
4
  require 'upfluence/mixin/strong_parameters'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upfluence-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.20
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Upfluence
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-26 00:00:00.000000000 Z
11
+ date: 2026-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -280,6 +280,7 @@ files:
280
280
  - lib/test.rb
281
281
  - lib/upfluence.rb
282
282
  - lib/upfluence/amqp/server.rb
283
+ - lib/upfluence/context.rb
283
284
  - lib/upfluence/endpoint/api_endpoint.rb
284
285
  - lib/upfluence/environment.rb
285
286
  - lib/upfluence/error_logger.rb
@@ -311,6 +312,7 @@ files:
311
312
  - lib/upfluence/resources.rb
312
313
  - lib/upfluence/resources/countries.rb
313
314
  - lib/upfluence/thread.rb
315
+ - lib/upfluence/timeout.rb
314
316
  - lib/upfluence/utils.rb
315
317
  - lib/upfluence/utils/env_helper.rb
316
318
  - lib/upfluence/utils/http/middleware/null.rb