Add transfer-specific "performing" flag

In the previous commits, two separte crash resolution conspired to introduce
1 tick delay in request handling.

When we're in a libcurl perform, we can't re-enter libcurl. So we added a
check before processing new requests to make sure we weren't "performing" and,
if we were, we'd queue the request (hence the 1 tick delay).

But for another issue, we set the same "performing" check when manually
triggering callbacks. This extended the situations where the above check fired
thus causing the 1-tick delay to happen under more (and even common) situation.

This commit improves this - instead of relying on the global "performing" check
when processing 1 transfer explicitly, we now have a per-transfer performing
check. This prevents the transfer from being deinitialized during a callback
but does not block requests from being started immediately.
This commit is contained in:
Karl Seguin
2026-03-02 17:29:47 +08:00
parent b104c3bfe8
commit 328c681a8f

View File

@@ -496,6 +496,8 @@ fn waitForInterceptedResponse(self: *Client, transfer: *Transfer) !bool {
// cases, the interecptor is expected to call resume to continue the transfer
// or transfer.abort() to abort it.
fn process(self: *Client, transfer: *Transfer) !void {
// libcurl doesn't allow recursive calls, if we're in a `perform()` operation
// then we _have_ to queue this.
if (self.handles.performing == false) {
if (self.handles.get()) |conn| {
return self.makeRequest(conn, transfer);
@@ -791,30 +793,30 @@ fn processMessages(self: *Client) !bool {
if (msg.err) |err| {
requestFailed(transfer, err, true);
} else blk: {
{
self.handles.performing = true;
defer self.handles.performing = false;
// make sure the transfer can't be immediately aborted from a callback
// since we still need it here.
transfer._performing = true;
defer transfer._performing = false;
if (!transfer._header_done_called) {
// In case of request w/o data, we need to call the header done
// callback now.
if (!transfer._header_done_called) {
const proceed = transfer.headerDoneCallback(&msg.conn) catch |err| {
log.err(.http, "header_done_callback", .{ .err = err });
requestFailed(transfer, err, true);
continue;
};
if (!proceed) {
requestFailed(transfer, error.Abort, true);
break :blk;
}
}
transfer.req.done_callback(transfer.ctx) catch |err| {
// transfer isn't valid at this point, don't use it.
log.err(.http, "done_callback", .{ .err = err });
const proceed = transfer.headerDoneCallback(&msg.conn) catch |err| {
log.err(.http, "header_done_callback", .{ .err = err });
requestFailed(transfer, err, true);
continue;
};
if (!proceed) {
requestFailed(transfer, error.Abort, true);
break :blk;
}
}
transfer.req.done_callback(transfer.ctx) catch |err| {
// transfer isn't valid at this point, don't use it.
log.err(.http, "done_callback", .{ .err = err });
requestFailed(transfer, err, true);
continue;
};
transfer.req.notification.dispatch(.http_request_done, &.{
.transfer = transfer,
@@ -944,6 +946,7 @@ pub const Transfer = struct {
// number of times the transfer has been tried.
// incremented by reset func.
_tries: u8 = 0,
_performing: bool = false,
// for when a Transfer is queued in the client.queue
_node: std.DoublyLinkedList.Node = .{},
@@ -1050,7 +1053,7 @@ pub const Transfer = struct {
requestFailed(self, err, true);
const client = self.client;
if (client.handles.performing) {
if (self._performing or client.handles.performing) {
// We're currently in a curl_multi_perform. We cannot call endTransfer
// as that calls curl_multi_remove_handle, and you can't do that
// from a curl callback. Instead, we flag this transfer and all of