Skip to content

Commit 35d3957

Browse files
Decrement requests_active only after the response body is closed.
1 parent ab6e1ff commit 35d3957

File tree

6 files changed

+192
-5
lines changed

6 files changed

+192
-5
lines changed

examples/utilization/falcon.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ class SimpleApp
1414
def call(env)
1515
# Simulate some work
1616
sleep(rand * 0.1)
17-
17+
18+
# Delay after response is sent - use to verify whether this counts toward active requests:
19+
# if response_finished = env["rack.response_finished"]
20+
# response_finished << proc{sleep 10}
21+
# end
22+
1823
return [200, {"content-type" => "text/plain"}, ["Hello, World!"]]
1924
end
2025
end
@@ -47,7 +52,7 @@ def call(env)
4752
[
4853
Async::Service::Supervisor::UtilizationMonitor.new(
4954
path: File.expand_path("utilization.shm", __dir__),
50-
interval: 5.0
55+
interval: 1.0
5156
)
5257
]
5358
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "protocol/http/body/wrapper"
7+
8+
module Falcon
9+
module Body
10+
# Wraps a response body and decrements a metric after the body is closed.
11+
#
12+
# Runs close on the underlying body first (which invokes rack.response_finished),
13+
# then decrements the metric. Use this so requests_active stays elevated until
14+
# the request is fully finished (including response_finished callbacks).
15+
class RequestFinished < Protocol::HTTP::Body::Wrapper
16+
# Wrap a response body with a metric. If the body is nil or empty, decrements immediately.
17+
#
18+
# @parameter message [Protocol::HTTP::Response] The response whose body to wrap.
19+
# @parameter metric [Async::Utilization::Metric] The metric to decrement when the body is closed.
20+
# @returns [Protocol::HTTP::Response] The message (modified in place).
21+
def self.wrap(message, metric)
22+
if body = message&.body and !body.empty?
23+
message.body = new(body, metric)
24+
else
25+
metric.decrement
26+
end
27+
28+
message
29+
end
30+
31+
# @parameter body [Protocol::HTTP::Body::Readable] The body to wrap.
32+
# @parameter metric [Async::Utilization::Metric] The metric to decrement on close.
33+
def initialize(body, metric)
34+
super(body)
35+
36+
@metric = metric
37+
end
38+
39+
def rewindable?
40+
false
41+
end
42+
43+
def rewind
44+
false
45+
end
46+
47+
def close(error = nil)
48+
super
49+
50+
@metric&.decrement
51+
@metric = nil
52+
end
53+
end
54+
end
55+
end

lib/falcon/server.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
require "async/http/cache"
1212
require "async/utilization"
13+
require_relative "body/request_finished"
1314
require_relative "middleware/verbose"
1415
require "protocol/rack"
1516

@@ -62,11 +63,16 @@ def accept(...)
6263
end
6364

6465
# Handle a request and track request statistics.
66+
#
67+
# Uses manual increment/decrement so requests_active stays elevated until the
68+
# response body is closed (including rack.response_finished). The
69+
# Body::RequestFinished wrapper runs the decrement after the body closes,
70+
# so response_finished callbacks are counted as active.
6571
def call(...)
6672
@requests_total_metric.increment
67-
@requests_active_metric.track do
68-
super
69-
end
73+
@requests_active_metric.increment
74+
75+
return Body::RequestFinished.wrap(super, @requests_active_metric)
7076
end
7177

7278
# Generates a human-readable string representing the current statistics.

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- `requests_active` is decremented after the response body is closed, including `rack.response_finished` callbacks.
6+
37
## v0.55.0
48

59
- **Breaking**: Drop dependency on `async-container-supervisor`, you should migrate to `async-service-supervisor` instead.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2026, by Samuel Williams.
5+
6+
require "falcon/body/request_finished"
7+
require "protocol/http/body/buffered"
8+
require "protocol/http/response"
9+
require "async/utilization"
10+
require "sus/fixtures/async"
11+
12+
describe Falcon::Body::RequestFinished do
13+
include Sus::Fixtures::Async::ReactorContext
14+
15+
let(:registry) { Async::Utilization::Registry.new }
16+
let(:metric) { registry.metric(:requests_active) }
17+
let(:body) { Protocol::HTTP::Body::Buffered.wrap("Hello World") }
18+
let(:response) { Protocol::HTTP::Response[200, {"content-type" => "text/plain"}, body] }
19+
20+
with ".wrap" do
21+
with "non-empty body" do
22+
it "wraps body and decrements metric when body is closed" do
23+
metric.increment
24+
expect(metric.value).to be == 1
25+
26+
wrapped = subject.wrap(response, metric)
27+
expect(wrapped).to be == response
28+
expect(response.body).to be_a(subject)
29+
expect(metric.value).to be == 1
30+
31+
response.body.close
32+
expect(metric.value).to be == 0
33+
end
34+
35+
it "decrements only once on multiple close calls" do
36+
metric.increment
37+
subject.wrap(response, metric)
38+
39+
response.body.close
40+
response.body.close
41+
42+
expect(metric.value).to be == 0
43+
end
44+
end
45+
46+
with "empty body" do
47+
let(:body) { Protocol::HTTP::Body::Buffered.new }
48+
49+
it "decrements immediately" do
50+
metric.increment
51+
expect(metric.value).to be == 1
52+
53+
subject.wrap(response, metric)
54+
expect(metric.value).to be == 0
55+
expect(response.body).to be_equal(body)
56+
end
57+
end
58+
59+
with "nil body" do
60+
let(:response) { Protocol::HTTP::Response[204, {}, nil] }
61+
62+
it "decrements immediately" do
63+
metric.increment
64+
expect(metric.value).to be == 1
65+
66+
subject.wrap(response, metric)
67+
expect(metric.value).to be == 0
68+
end
69+
end
70+
71+
with "nil message" do
72+
it "decrements immediately" do
73+
metric.increment
74+
subject.wrap(nil, metric)
75+
expect(metric.value).to be == 0
76+
end
77+
end
78+
end
79+
80+
with "#rewindable?" do
81+
it "returns false" do
82+
subject.wrap(response, metric)
83+
expect(response.body).not_to be(:rewindable?)
84+
end
85+
end
86+
87+
with "#rewind" do
88+
it "returns false" do
89+
subject.wrap(response, metric)
90+
expect(response.body.rewind).to be == false
91+
end
92+
end
93+
end

test/falcon/server.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,28 @@
125125
expect(response.read).to be =~ /Hello World!/
126126
end
127127
end
128+
129+
with "utilization tracking" do
130+
let(:utilization_registry) {Async::Utilization::Registry.new}
131+
132+
def make_server(endpoint)
133+
::Falcon::Server.new(middleware, endpoint, utilization_registry: utilization_registry)
134+
end
135+
136+
let(:app) do
137+
lambda do |env|
138+
[200, {}, ["OK"]]
139+
end
140+
end
141+
142+
it "decrements requests_active after response body is consumed" do
143+
expect(utilization_registry.metric(:requests_active).value).to be == 0
144+
145+
response = client.get("/")
146+
expect(response.read).to be == "OK"
147+
148+
expect(utilization_registry.metric(:requests_active).value).to be == 0
149+
expect(utilization_registry.metric(:requests_total).value).to be == 1
150+
end
151+
end
128152
end

0 commit comments

Comments
 (0)