upfluence-utils 0.13.1 → 0.14.1

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: 185ccb39ffbe42b800bf9949a55d706d94ee5876fc883760ab3c639d3a0caea9
4
- data.tar.gz: d6a2b76fdc2ad0eeec49b77d887c3b460f61d16cecd562c5c2f032e5fb3b8511
3
+ metadata.gz: 568c4033f087730c94a75c756587432873ee149b4d2462ea04b1b03169d9b551
4
+ data.tar.gz: e824e37e1cd2c901342f0089e859af8d62a908aa1c755d2527dc1a20309449f2
5
5
  SHA512:
6
- metadata.gz: 117c839c07924e783815ba743613d9dcf8be403f5cdc2b4dbc46e6843f10786fe689dab160fdb2d50990e101775e942596f169986b99bc3b3bd66dbd15d88502
7
- data.tar.gz: 3c6fe69b7f7265f3d0f392125dbe24aba0a0c3f69fef3f873d6cef7fd56a342363f8e7732a16d30ebd4d0cac8feeb15e776dd1b0a8a716d0db202e4709c3d838
6
+ metadata.gz: 60d4fd0ba8383217d60a73eadf48920d15259c77be69922f7a65f313b9a297dc62274c598655e739f3403a51e0d0f2cdd8f116e714528cc261dfb1a59ea47536
7
+ data.tar.gz: c6cbf56699eb8e3d540dea322fda7920a35c5bb06162d1483aaeb337dae7ea16f1f30137d052aab41497b8596f73efdac21f10a87961060194fdcd00809e1106
@@ -0,0 +1,29 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ uses: upfluence/actions/.github/workflows/lib-ruby-test.yml@master
12
+ secrets: inherit
13
+
14
+ release:
15
+ needs: test
16
+ if: github.ref == 'refs/heads/master'
17
+ runs-on: ubuntu-24.04
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v6
21
+ with:
22
+ fetch-depth: 0
23
+ - name: Bump version
24
+ id: bump-version
25
+ uses: upfluence/actions/bump-version@master
26
+ - name: Create release
27
+ uses: upfluence/actions/create-github-release@master
28
+ with:
29
+ version: ${{ steps.bump-version.outputs.version }}
data/Makefile ADDED
@@ -0,0 +1,4 @@
1
+ .PHONY: release
2
+
3
+ release:
4
+ BUNDLE_FROZEN=false bundle exec rake release:rubygem_push
@@ -0,0 +1,17 @@
1
+ module Upfluence
2
+ module HTTP
3
+ module Middleware
4
+ class ActiveRecord
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ ::ActiveRecord::Base.connection_pool.with_connection do
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -26,56 +26,63 @@ module Upfluence
26
26
  module HTTP
27
27
  class Server
28
28
  REQUEST_CONTEXT_KEY = :uhttp_request_context
29
+ SHUTDOWN_SIGNALS = %w[INT TERM].freeze
29
30
  DEFAULT_MIDDLEWARES = []
30
31
  DEFAULT_OPTIONS = {
31
- server: :puma,
32
- Port: ENV.fetch('PORT', 8080),
33
- Host: '0.0.0.0',
34
- threaded: true,
35
- interfaces: [],
36
- push_gateway_url: ENV.fetch('PUSH_GATEWAY_URL', nil),
37
- push_gateway_interval: 15, # sec
38
- prometheus_endpoint: ENV.fetch('PUSH_GATEWAY_URL', nil).eql?(nil),
39
- app_name: ENV.fetch('APP_NAME', 'uhttp-rb-server'),
40
- unit_name: ENV.fetch('UNIT_NAME','uhttp-rb-server-anonymous'),
41
- base_processor_klass: nil,
42
- base_handler_klass: nil,
43
- max_threads: ENV.fetch('HTTP_SERVER_MAX_THREADS', 5).to_i,
44
- request_timeout: ENV['HTTP_SERVER_REQUEST_TIMEOUT']&.to_i,
45
- middlewares: [],
46
- instrumentations: [
32
+ server: :puma,
33
+ Port: ENV.fetch('PORT', 8080),
34
+ Host: '0.0.0.0',
35
+ threaded: true,
36
+ interfaces: [],
37
+ push_gateway_url: ENV.fetch('PUSH_GATEWAY_URL', nil),
38
+ push_gateway_interval: 15, # sec
39
+ prometheus_endpoint: ENV.fetch('PUSH_GATEWAY_URL', nil).eql?(nil),
40
+ app_name: ENV.fetch('APP_NAME', 'uhttp-rb-server'),
41
+ unit_name: ENV.fetch('UNIT_NAME', 'uhttp-rb-server-anonymous'),
42
+ base_processor_klass: nil,
43
+ base_handler_klass: nil,
44
+ max_threads: ENV.fetch('HTTP_SERVER_MAX_THREADS', 5).to_i,
45
+ request_timeout: ENV['HTTP_SERVER_REQUEST_TIMEOUT']&.to_i,
46
+ enable_active_record_middleware: true,
47
+ middlewares: [],
48
+ instrumentations: [
47
49
  Instrumentation::PumaInstrumenter.new,
48
50
  Instrumentation::GCInstrumenter.new,
49
51
  Instrumentation::ActiveRecordPoolInstrumenter.new
50
52
  ],
51
- admin_port: ENV.fetch('ADMIN_PORT', nil),
52
- debug: ENV.fetch('DEBUG', nil)
53
+ admin_port: ENV.fetch('ADMIN_PORT', nil),
54
+ debug: ENV.fetch('DEBUG', nil)
53
55
  }
54
56
 
55
57
  def initialize(options = {}, &block)
56
58
  @options = DEFAULT_OPTIONS.dup.merge(options)
57
59
  opts = @options
58
60
 
59
- if opts[:base_handler_klass]
60
- @base_handler = opts[:base_handler_klass].new(opts[:interfaces])
61
- end
61
+ @base_handler = opts[:base_handler_klass].new(opts[:interfaces]) if opts[:base_handler_klass]
62
62
 
63
63
  @admin_builder = admin_builder(opts) if opts[:admin_port]
64
64
  @production_builder = production_builder(opts, &block)
65
65
  @handler = Rack::Handler.get(opts[:server])
66
+ @stoppables = []
66
67
  end
67
68
 
68
69
  def serve
69
70
  ENV['RACK_ENV'] = Upfluence.env.to_s
71
+ @stopping = false
72
+ signal_handlers = install_signal_handlers
73
+ admin_thread = nil
70
74
 
71
- Thread.new { run_prometheus_exporter } if @options[:push_gateway_url]
75
+ register_stoppable(prometheus_exporter_thread) if @options[:push_gateway_url]
72
76
 
73
- @options[:instrumentations].each(&:start)
77
+ @options[:instrumentations].each do |instrumentation|
78
+ instrumentation.start
79
+ register_stoppable(instrumentation) if instrumentation.respond_to?(:stop)
80
+ end
74
81
 
75
82
  if @admin_builder
76
83
  admin_port = @options[:admin_port].to_i
77
84
 
78
- Thread.new do
85
+ admin_thread = Thread.new do
79
86
  run_builder(@admin_builder, Port: admin_port)
80
87
  end
81
88
 
@@ -85,6 +92,10 @@ module Upfluence
85
92
  end
86
93
 
87
94
  run_builder(@production_builder)
95
+ ensure
96
+ stop
97
+ admin_thread&.join
98
+ restore_signal_handlers(signal_handlers) if signal_handlers
88
99
  end
89
100
 
90
101
  class << self
@@ -103,68 +114,100 @@ module Upfluence
103
114
  opts = @options.merge(overrides)
104
115
 
105
116
  @handler.run(builder, **opts) do |server|
117
+ register_stoppable(server)
118
+ register_booted_handler(server)
119
+ configure_puma_instrumenter(server) if builder.equal?(@production_builder)
106
120
  server.threaded = opts[:threaded] if server.respond_to? :threaded=
107
121
 
108
122
  # Thin does not recognize the max_thread argument, howerver it has a
109
123
  # 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]
124
+ server.threadpool_size = opts[:max_threads] if server.respond_to?(:threadpool_size=) && opts[:max_threads]
125
+ end
126
+ end
127
+
128
+ def register_booted_handler(server)
129
+ return unless server.respond_to?(:events)
130
+
131
+ server.events.after_booted { install_signal_handlers }
132
+ end
133
+
134
+ def configure_puma_instrumenter(server)
135
+ @options[:instrumentations].select { |instrumentation| instrumentation.respond_to?(:server=) }.each do |instrumentation|
136
+ instrumentation.server = server
137
+ end
138
+ end
139
+
140
+ def install_signal_handlers
141
+ SHUTDOWN_SIGNALS.to_h do |signal|
142
+ previous_handler = Signal.trap(signal) do
143
+ Thread.new { stop }
112
144
  end
145
+
146
+ [signal, previous_handler]
113
147
  end
114
148
  end
115
149
 
116
- def production_builder(opts, &block)
117
- base_handler = @base_handler
118
- has_admin = !opts[:admin_port].nil?
119
- mount = method(:mount_admin_endpoints)
150
+ def restore_signal_handlers(handlers)
151
+ handlers.each { |signal, handler| Signal.trap(signal, handler) }
152
+ end
120
153
 
121
- Builder.new do
122
- use Middleware::RequestStapler, timeout: opts[:request_timeout]
123
- use Middleware::Logger
124
- use Middleware::Prometheus
125
- use Middleware::ApplicationHeaders, base_handler
126
- use Middleware::HandleException
154
+ def register_stoppable(stoppable)
155
+ return stoppable.stop if @stopping
156
+
157
+ @stoppables << stoppable
158
+ end
159
+
160
+ def stop
161
+ @stopping = true
162
+ stoppables = @stoppables
163
+ @stoppables = []
164
+
165
+ stoppables.each(&:stop)
166
+ end
127
167
 
128
- if opts[:request_timeout]
129
- use Rack::Timeout, service_timeout: opts[:request_timeout]
168
+ def prometheus_exporter_thread
169
+ Thread.new { run_prometheus_exporter }.tap do |thread|
170
+ thread.define_singleton_method(:stop) do
171
+ kill
172
+ join
130
173
  end
174
+ end
175
+ end
131
176
 
132
- use Upfluence.error_logger.middleware
177
+ def production_builder(opts, &block)
178
+ has_admin = !opts[:admin_port].nil?
179
+ builder = Builder.new
133
180
 
134
- use Rack::ContentLength
135
- use Rack::Chunked
136
- use Rack::Lint if Upfluence.env.development?
137
- use Rack::TempfileReaper
138
- use Rack::ETag
139
- use Middleware::CORS if Upfluence.env.development?
181
+ mount_middlewares(builder, opts)
182
+
183
+ builder.instance_eval do
184
+ if opts[:enable_active_record_middleware] &&
185
+ defined?(ActiveRecord::Base) &&
186
+ ActiveRecord::Base.configurations.any?
187
+ require 'upfluence/http/middleware/active_record'
188
+
189
+ use Middleware::ActiveRecord
190
+ end
140
191
 
141
192
  (DEFAULT_MIDDLEWARES + opts[:middlewares]).each do |m|
142
193
  m = [m] unless m.is_a?(Array)
143
194
  use(*m)
144
195
  end
196
+ end
145
197
 
146
- mount.call(self, opts) unless has_admin
147
-
148
- map '/healthcheck' do
149
- run(opts[:healthcheck_endpoint] || Endpoint::Healthcheck.new)
150
- end
198
+ mount_admin_endpoints(builder, opts) unless has_admin
199
+ builder.instance_eval(&block)
151
200
 
152
- instance_eval(&block)
153
- end
201
+ builder
154
202
  end
155
203
 
156
204
  def admin_builder(opts)
157
- mount = method(:mount_admin_endpoints)
205
+ builder = Builder.new
158
206
 
159
- Builder.new do
160
- use Rack::ContentLength
161
-
162
- map '/healthcheck' do
163
- run(opts[:healthcheck_endpoint] || Endpoint::Healthcheck.new)
164
- end
207
+ mount_middlewares(builder, opts)
208
+ mount_admin_endpoints(builder, opts)
165
209
 
166
- mount.call(self, opts)
167
- end
210
+ builder
168
211
  end
169
212
 
170
213
  def mount_admin_endpoints(builder, opts)
@@ -183,6 +226,35 @@ module Upfluence
183
226
  end
184
227
  end
185
228
 
229
+ def mount_middlewares(builder, opts)
230
+ base_handler = @base_handler
231
+
232
+ builder.instance_eval do
233
+ use Middleware::RequestStapler, timeout: opts[:request_timeout]
234
+ use Middleware::Logger
235
+ use Middleware::Prometheus
236
+ use Middleware::ApplicationHeaders, base_handler
237
+ use Middleware::HandleException
238
+
239
+ use Rack::Timeout, service_timeout: opts[:request_timeout] if opts[:request_timeout]
240
+
241
+ use Upfluence.error_logger.middleware
242
+
243
+ use Rack::ContentLength
244
+ use Rack::Chunked
245
+ use Rack::Lint if Upfluence.env.development?
246
+ use Rack::TempfileReaper
247
+ use Rack::ETag
248
+ use Middleware::CORS if Upfluence.env.development?
249
+ end
250
+
251
+ builder.instance_eval do
252
+ map '/healthcheck' do
253
+ run(opts[:healthcheck_endpoint] || Endpoint::Healthcheck.new)
254
+ end
255
+ end
256
+ end
257
+
186
258
  def run_prometheus_exporter
187
259
  push = Prometheus::Client::Push.new(
188
260
  @options[:app_name],
@@ -30,7 +30,7 @@ module Upfluence
30
30
  rescue => e
31
31
  Upfluence.error_logger.notify(e)
32
32
  ensure
33
- sleep @interval
33
+ sleep @interval unless @stop_thread
34
34
  end
35
35
  end
36
36
  end
@@ -6,6 +6,8 @@ module Upfluence
6
6
  class PumaInstrumenter < PeriodicInstrumenter
7
7
  KEYS = %i[backlog_thread running_thread busy_thread pool_capacity requests_count].freeze
8
8
 
9
+ attr_writer :server
10
+
9
11
  def prefix
10
12
  'puma'
11
13
  end
@@ -17,15 +19,17 @@ module Upfluence
17
19
  end
18
20
 
19
21
  def values
20
- stats = Puma.stats_hash
22
+ return {} unless @server
23
+
24
+ stats = @server.stats
21
25
 
22
26
  {
23
- requests_count: [{ value: stats[:requests_count] }],
24
- backlog_thread: [{ value: stats[:backlog] }],
25
- running_thread: [{ value: stats[:running] }],
26
- busy_thread: [{ value: stats[:busy_threads] }],
27
- pool_capacity: [{ value: stats[:max_threads] }]
28
- }
27
+ requests_count: stats[:requests_count],
28
+ backlog_thread: stats[:backlog],
29
+ running_thread: stats[:running],
30
+ busy_thread: stats[:busy_threads],
31
+ pool_capacity: stats[:max_threads]
32
+ }.compact.transform_values { |value| [{ value: value }] }
29
33
  rescue NoMethodError
30
34
  {}
31
35
  end
@@ -1,5 +1,5 @@
1
1
  module Upfluence
2
2
  module Utils
3
- VERSION = '0.13.1'.freeze
3
+ VERSION = '0.14.1'.freeze
4
4
  end
5
5
  end
@@ -26,8 +26,8 @@ Gem::Specification.new do |spec|
26
26
  spec.add_runtime_dependency 'sentry-ruby', '< 6.0'
27
27
  spec.add_runtime_dependency 'sinatra-contrib'
28
28
  spec.add_runtime_dependency 'activesupport'
29
- spec.add_runtime_dependency 'puma'
30
- spec.add_runtime_dependency 'rack'
29
+ spec.add_runtime_dependency 'puma', '>= 7.0'
30
+ spec.add_runtime_dependency 'rack', '< 3.0'
31
31
  spec.add_runtime_dependency 'stackprof'
32
32
  spec.add_runtime_dependency 'prometheus-client', '~> 2.1'
33
33
  spec.add_runtime_dependency 'userializer'
@@ -35,4 +35,6 @@ Gem::Specification.new do |spec|
35
35
  spec.add_runtime_dependency 'loofah'
36
36
  spec.add_runtime_dependency 'rack-timeout'
37
37
  spec.add_runtime_dependency 'mutex_m'
38
+ spec.add_runtime_dependency 'base64'
39
+ spec.add_runtime_dependency 'bigdecimal'
38
40
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upfluence-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Upfluence
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-08-18 00:00:00.000000000 Z
10
+ date: 2026-08-19 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -142,28 +141,28 @@ dependencies:
142
141
  requirements:
143
142
  - - ">="
144
143
  - !ruby/object:Gem::Version
145
- version: '0'
144
+ version: '7.0'
146
145
  type: :runtime
147
146
  prerelease: false
148
147
  version_requirements: !ruby/object:Gem::Requirement
149
148
  requirements:
150
149
  - - ">="
151
150
  - !ruby/object:Gem::Version
152
- version: '0'
151
+ version: '7.0'
153
152
  - !ruby/object:Gem::Dependency
154
153
  name: rack
155
154
  requirement: !ruby/object:Gem::Requirement
156
155
  requirements:
157
- - - ">="
156
+ - - "<"
158
157
  - !ruby/object:Gem::Version
159
- version: '0'
158
+ version: '3.0'
160
159
  type: :runtime
161
160
  prerelease: false
162
161
  version_requirements: !ruby/object:Gem::Requirement
163
162
  requirements:
164
- - - ">="
163
+ - - "<"
165
164
  - !ruby/object:Gem::Version
166
- version: '0'
165
+ version: '3.0'
167
166
  - !ruby/object:Gem::Dependency
168
167
  name: stackprof
169
168
  requirement: !ruby/object:Gem::Requirement
@@ -262,7 +261,34 @@ dependencies:
262
261
  - - ">="
263
262
  - !ruby/object:Gem::Version
264
263
  version: '0'
265
- description:
264
+ - !ruby/object:Gem::Dependency
265
+ name: base64
266
+ requirement: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - ">="
269
+ - !ruby/object:Gem::Version
270
+ version: '0'
271
+ type: :runtime
272
+ prerelease: false
273
+ version_requirements: !ruby/object:Gem::Requirement
274
+ requirements:
275
+ - - ">="
276
+ - !ruby/object:Gem::Version
277
+ version: '0'
278
+ - !ruby/object:Gem::Dependency
279
+ name: bigdecimal
280
+ requirement: !ruby/object:Gem::Requirement
281
+ requirements:
282
+ - - ">="
283
+ - !ruby/object:Gem::Version
284
+ version: '0'
285
+ type: :runtime
286
+ prerelease: false
287
+ version_requirements: !ruby/object:Gem::Requirement
288
+ requirements:
289
+ - - ">="
290
+ - !ruby/object:Gem::Version
291
+ version: '0'
266
292
  email:
267
293
  - dev@upfluence.com
268
294
  executables: []
@@ -270,11 +296,13 @@ extensions: []
270
296
  extra_rdoc_files: []
271
297
  files:
272
298
  - ".github/pull_request_template.md"
299
+ - ".github/workflows/ci.yml"
273
300
  - ".gitignore"
274
301
  - ".rspec"
275
302
  - ".travis.yml"
276
303
  - Gemfile
277
304
  - LICENSE.txt
305
+ - Makefile
278
306
  - README.md
279
307
  - Rakefile
280
308
  - lib/test.rb
@@ -291,6 +319,7 @@ files:
291
319
  - lib/upfluence/http/endpoint/healthcheck.rb
292
320
  - lib/upfluence/http/endpoint/profiler.rb
293
321
  - lib/upfluence/http/endpoint/validation_error.rb
322
+ - lib/upfluence/http/middleware/active_record.rb
294
323
  - lib/upfluence/http/middleware/application_headers.rb
295
324
  - lib/upfluence/http/middleware/cors.rb
296
325
  - lib/upfluence/http/middleware/handle_exception.rb
@@ -331,7 +360,6 @@ homepage: https://github.com/upfluence/upfluence_utils
331
360
  licenses:
332
361
  - MIT
333
362
  metadata: {}
334
- post_install_message:
335
363
  rdoc_options: []
336
364
  require_paths:
337
365
  - lib
@@ -346,8 +374,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
346
374
  - !ruby/object:Gem::Version
347
375
  version: '0'
348
376
  requirements: []
349
- rubygems_version: 3.3.26
350
- signing_key:
377
+ rubygems_version: 3.6.2
351
378
  specification_version: 4
352
379
  summary: Upfluence common utils for Ruby projects
353
380
  test_files: []