Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

Commit b07edb7

Browse files
Implement Swift request idle progress
1 parent 5db00c4 commit b07edb7

5 files changed

Lines changed: 260 additions & 62 deletions

File tree

swift/vox-runtime/Sources/VoxRuntime/Channel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ public enum ChannelError: Error, Equatable {
636636
case reset
637637
case requestClosed
638638
case cancelled
639+
case timedOut
639640
case connectionClosed
640641
case unknown
641642
}

swift/vox-runtime/Sources/VoxRuntime/Driver+Incoming.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ extension Driver {
193193
channels: call.channels
194194
)
195195
case .response(let response):
196+
// r[impl rpc.request.scope.terminal]
197+
// r[impl rpc.request.scope.channels]
196198
// r[impl rpc.response]
197199
let payload = [UInt8](response.ret)
198200
guard let pending = await state.claimPendingResponse(request.id, reason: "response")
@@ -226,6 +228,8 @@ extension Driver {
226228
pending.responseTx(.success(payload))
227229
case .cancel:
228230
// r[impl rpc.cancel]
231+
// r[impl rpc.request.scope.terminal]
232+
// r[impl rpc.request.scope.channels]
229233
let responseContext = await state.removeInFlight(request.id)
230234
await terminateRequestChannels(
231235
connectionId: responseContext.connectionId,
@@ -242,16 +246,32 @@ extension Driver {
242246
channelId: channel.id,
243247
payload: itemBytes
244248
)
249+
await markChannelRequestProgress(
250+
connectionId: msg.connectionId,
251+
channelId: channel.id
252+
)
245253
case .close:
246254
try await handleClose(connectionId: msg.connectionId, channelId: channel.id)
255+
await markChannelRequestProgress(
256+
connectionId: msg.connectionId,
257+
channelId: channel.id
258+
)
247259
case .reset:
248260
await deliverChannelReset(connectionId: msg.connectionId, channelId: channel.id)
261+
await markChannelRequestProgress(
262+
connectionId: msg.connectionId,
263+
channelId: channel.id
264+
)
249265
case .grantCredit(let credit):
250266
await deliverChannelCredit(
251267
connectionId: msg.connectionId,
252268
channelId: channel.id,
253269
bytes: credit.additional
254270
)
271+
await markChannelRequestProgress(
272+
connectionId: msg.connectionId,
273+
channelId: channel.id
274+
)
255275
}
256276
}
257277
}
@@ -423,6 +443,8 @@ extension Driver {
423443
channelIds: [UInt64],
424444
error: ChannelError
425445
) async {
446+
// r[impl rpc.request.scope.channels]
447+
// r[impl rpc.channel.lifecycle]
426448
for channelId in channelIds {
427449
await deliverChannelReset(
428450
connectionId: connectionId,
@@ -477,6 +499,11 @@ extension Driver {
477499

478500
for (_, pending) in responses {
479501
pending.timeoutTask?.cancel()
502+
await terminateRequestChannels(
503+
connectionId: pending.request.connectionId,
504+
channelIds: pending.request.channels,
505+
error: .connectionClosed
506+
)
480507
pending.responseTx(.failure(.connectionClosed))
481508
}
482509
}
@@ -489,6 +516,11 @@ extension Driver {
489516

490517
for (_, pending) in responses {
491518
pending.timeoutTask?.cancel()
519+
await terminateRequestChannels(
520+
connectionId: pending.request.connectionId,
521+
channelIds: pending.request.channels,
522+
error: .connectionClosed
523+
)
492524
pending.responseTx(.failure(.connectionClosed))
493525
}
494526
}

swift/vox-runtime/Sources/VoxRuntime/Driver+Outgoing.swift

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,29 @@ extension Driver {
102102
let msg = queued.taskMessage
103103
let connectionId = queued.connectionId
104104
let wireMsg: Message
105+
let progressChannelId: UInt64?
105106
switch msg {
106107
case .data(let channelId, let payload):
107108
wireMsg = messageData(channelId: channelId, item: payload, connectionId: connectionId)
109+
progressChannelId = channelId
108110
case .close(let channelId):
109111
wireMsg = messageChannelClose(channelId: channelId, connectionId: connectionId)
112+
progressChannelId = channelId
110113
case .grantCredit(let channelId, let bytes):
111114
wireMsg = messageCredit(
112115
channelId: channelId,
113116
additional: bytes,
114117
connectionId: connectionId
115118
)
119+
progressChannelId = channelId
116120
case .schema(let methodId, let direction, let schemas):
117121
wireMsg = messageSchema(
118122
methodId: methodId,
119123
direction: direction,
120124
schemas: schemas,
121125
connectionId: connectionId
122126
)
127+
progressChannelId = nil
123128
case .response(let requestId, let payload, let methodId, let responseSchemaClosure):
124129
// Advertise the response schema at THIS sequential send point (not in the
125130
// concurrent dispatch task): under pipelining many responses for a method
@@ -160,8 +165,15 @@ extension Driver {
160165
return
161166
}
162167
wireMsg = response
168+
progressChannelId = nil
163169
}
164170
try await sendOrEnqueue(wireMsg)
171+
if let progressChannelId {
172+
await markChannelRequestProgress(
173+
connectionId: connectionId,
174+
channelId: progressChannelId
175+
)
176+
}
165177
}
166178

167179
/// Handle a command from a lane or connection handle.
@@ -245,37 +257,12 @@ extension Driver {
245257
return
246258
}
247259

248-
guard let timeout else {
249-
return
250-
}
251-
252-
let timeoutNs = Self.timeoutToNanoseconds(timeout)
253-
let capturedState = state
254-
let capturedConduit = conduit
255-
let capturedConnectionId = connectionId
256-
let timeoutTask = Task {
257-
do {
258-
try await Task.sleep(nanoseconds: timeoutNs)
259-
} catch {
260-
return
261-
}
262-
guard let pending = await capturedState.claimPendingResponse(
263-
requestId,
264-
reason: "timeout"
265-
) else {
266-
return
267-
}
268-
pending.timeoutTask?.cancel()
269-
warnLog("request timed out request_id=\(requestId) timeout_s=\(timeout)")
270-
pending.responseTx(.failure(.timeout))
271-
try? await capturedConduit.send(
272-
messageCancel(requestId: requestId, connectionId: capturedConnectionId)
260+
if let timeout {
261+
await installRequestIdleTimeout(
262+
requestId: requestId,
263+
timeout: timeout
273264
)
274265
}
275-
let installed = await state.setPendingTimeoutTask(requestId, timeoutTask: timeoutTask)
276-
if !installed {
277-
timeoutTask.cancel()
278-
}
279266
case .openLane(let settings, let metadata, let dispatcher, let responseTx):
280267
let isClosed = await state.isConnectionClosed()
281268
guard !isClosed else {
@@ -396,38 +383,12 @@ extension Driver {
396383

397384
pendingCalls.removeFirst()
398385

399-
guard let timeout = call.timeout else {
400-
continue
401-
}
402-
403-
let timeoutNs = Self.timeoutToNanoseconds(timeout)
404-
let capturedState = state
405-
let capturedConduit = conduit
406-
let capturedConnectionId = call.connectionId
407-
let requestId = call.requestId
408-
let timeoutTask = Task {
409-
do {
410-
try await Task.sleep(nanoseconds: timeoutNs)
411-
} catch {
412-
return
413-
}
414-
guard let pending = await capturedState.claimPendingResponse(
415-
requestId,
416-
reason: "timeout"
417-
) else {
418-
return
419-
}
420-
pending.timeoutTask?.cancel()
421-
warnLog("request timed out request_id=\(requestId) timeout_s=\(timeout)")
422-
pending.responseTx(.failure(.timeout))
423-
try? await capturedConduit.send(
424-
messageCancel(requestId: requestId, connectionId: capturedConnectionId)
386+
if let timeout = call.timeout {
387+
await installRequestIdleTimeout(
388+
requestId: call.requestId,
389+
timeout: timeout
425390
)
426391
}
427-
let installed = await state.setPendingTimeoutTask(requestId, timeoutTask: timeoutTask)
428-
if !installed {
429-
timeoutTask.cancel()
430-
}
431392
}
432393
}
433394

@@ -450,4 +411,63 @@ extension Driver {
450411
pendingTaskMessages.removeFirst()
451412
}
452413
}
414+
415+
// r[impl rpc.timeout.idle-progress]
416+
func installRequestIdleTimeout(
417+
requestId: UInt64,
418+
timeout: TimeInterval
419+
) async {
420+
let timeoutNs = Self.timeoutToNanoseconds(timeout)
421+
let timeoutTask = Task { [weak self] in
422+
do {
423+
try await Task.sleep(nanoseconds: timeoutNs)
424+
} catch {
425+
return
426+
}
427+
await self?.expireRequestForIdleTimeout(requestId: requestId, timeout: timeout)
428+
}
429+
let replacement = await state.replacePendingTimeoutTask(
430+
requestId,
431+
timeoutTask: timeoutTask
432+
)
433+
guard replacement.installed else {
434+
timeoutTask.cancel()
435+
return
436+
}
437+
replacement.previous?.cancel()
438+
}
439+
440+
// r[impl rpc.timeout.idle-progress]
441+
func markChannelRequestProgress(connectionId: UInt64, channelId: UInt64) async {
442+
let contexts = await state.pendingTimeoutContexts(
443+
connectionId: connectionId,
444+
channelId: channelId
445+
)
446+
for context in contexts {
447+
await installRequestIdleTimeout(
448+
requestId: context.requestId,
449+
timeout: context.timeout
450+
)
451+
}
452+
}
453+
454+
// r[impl rpc.timeout.idle-progress]
455+
// r[impl rpc.request.scope.terminal]
456+
// r[impl rpc.request.scope.channels]
457+
func expireRequestForIdleTimeout(requestId: UInt64, timeout: TimeInterval) async {
458+
guard let pending = await state.claimPendingResponse(requestId, reason: "timeout") else {
459+
return
460+
}
461+
pending.timeoutTask?.cancel()
462+
warnLog("request timed out request_id=\(requestId) timeout_s=\(timeout)")
463+
await terminateRequestChannels(
464+
connectionId: pending.request.connectionId,
465+
channelIds: pending.request.channels,
466+
error: .timedOut
467+
)
468+
pending.responseTx(.failure(.timeout))
469+
try? await conduit.send(
470+
messageCancel(requestId: requestId, connectionId: pending.request.connectionId)
471+
)
472+
}
453473
}

swift/vox-runtime/Sources/VoxRuntime/DriverState.swift

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import Foundation
22

3+
struct PendingTimeoutContext: Sendable {
4+
let requestId: UInt64
5+
let timeout: TimeInterval
6+
}
7+
38
/// Actor that holds mutable driver state to avoid NSLock in async contexts.
49
actor DriverState {
510
private let retainFinalizedRequests = true
@@ -106,15 +111,52 @@ actor DriverState {
106111
}
107112
}
108113

109-
func setPendingTimeoutTask(_ requestId: UInt64, timeoutTask: Task<Void, Never>) -> Bool {
114+
func replacePendingTimeoutTask(
115+
_ requestId: UInt64,
116+
timeoutTask: Task<Void, Never>
117+
) -> (installed: Bool, previous: Task<Void, Never>?) {
110118
guard var pending = pendingResponses[requestId] else {
111-
return false
119+
return (false, nil)
112120
}
121+
let previous = pending.timeoutTask
113122
pending.timeoutTask = timeoutTask
114123
pendingResponses[requestId] = pending
115-
return true
124+
return (true, previous)
125+
}
126+
127+
// r[impl rpc.timeout.idle-progress]
128+
func pendingTimeoutContext(_ requestId: UInt64) -> PendingTimeoutContext? {
129+
guard let pending = pendingResponses[requestId],
130+
let timeout = pending.request.timeout
131+
else {
132+
return nil
133+
}
134+
return PendingTimeoutContext(
135+
requestId: requestId,
136+
timeout: timeout
137+
)
138+
}
139+
140+
// r[impl rpc.timeout.idle-progress]
141+
func pendingTimeoutContexts(
142+
connectionId: UInt64,
143+
channelId: UInt64
144+
) -> [PendingTimeoutContext] {
145+
pendingResponses.compactMap { requestId, pending in
146+
guard pending.request.connectionId == connectionId,
147+
pending.request.channels.contains(channelId),
148+
let timeout = pending.request.timeout
149+
else {
150+
return nil
151+
}
152+
return PendingTimeoutContext(
153+
requestId: requestId,
154+
timeout: timeout
155+
)
156+
}
116157
}
117158

159+
// r[impl rpc.request.scope]
118160
func addInFlight(
119161
_ requestId: UInt64,
120162
connectionId: UInt64,
@@ -145,6 +187,7 @@ actor DriverState {
145187
return .inserted
146188
}
147189

190+
// r[impl rpc.request.scope.terminal]
148191
func removeInFlight(_ requestId: UInt64) -> (
149192
removed: Bool,
150193
connectionId: UInt64,

0 commit comments

Comments
 (0)