Skip to content

Commit e3879eb

Browse files
authored
update reopen up stream (#218)
* update reopen up stream * update reopen up stream * update reopen up stream * update test
1 parent 123f14d commit e3879eb

File tree

5 files changed

+245
-37
lines changed

5 files changed

+245
-37
lines changed

Networking/Sources/MsQuicSwift/QuicStream.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ private class StreamHandle {
207207

208208
case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
209209
logger.trace("Peer send aborted")
210-
// TODO: check if we need to close the stream completely
211210

212211
case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
213212
logger.trace("Stream shutdown complete")

Networking/Sources/Networking/Connection.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,37 @@ public final class Connection<Handler: StreamHandler>: Sendable, ConnectionInfoP
234234
return
235235
}
236236
if let upKind = Handler.PresistentHandler.StreamKind(rawValue: byte) {
237-
// TODO: handle duplicated UP streams
237+
// Check for duplicate UP streams
238+
let existingStream = presistentStreams.read { presistentStreams in
239+
presistentStreams[upKind]
240+
}
241+
if let existingStream {
242+
if existingStream.stream.id < stream.stream.id {
243+
// The new stream has a higher ID, so reset the existing one
244+
existingStream.close(abort: false)
245+
logger.debug(
246+
"Reset older UP stream with lower ID",
247+
metadata: ["existingStreamId": "\(existingStream.stream.id)", "newStreamId": "\(stream.stream.id)"]
248+
)
249+
} else {
250+
// The existing stream has a higher ID or is equal, so reset the new one
251+
stream.close(abort: false)
252+
logger.debug(
253+
"Duplicate UP stream detected, closing new stream with lower or equal ID",
254+
metadata: ["existingStreamId": "\(existingStream.stream.id)", "newStreamId": "\(stream.stream.id)"]
255+
)
256+
return // Exit without replacing the existing stream
257+
}
258+
}
259+
260+
// Write the new stream as the active one for this UP kind
238261
presistentStreams.write { presistentStreams in
239262
presistentStreams[upKind] = stream
240263
}
241264
runPresistentStreamLoop(stream: stream, kind: upKind)
242265
return
243266
}
267+
244268
if let ceKind = Handler.EphemeralHandler.StreamKind(rawValue: byte) {
245269
logger.debug("stream opened. kind: \(ceKind)")
246270

Networking/Sources/Networking/Peer.swift

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public enum PeerRole: Sendable, Hashable {
1616
// case proxy // not yet specified
1717
}
1818

19-
struct ReconnectState {
19+
struct BackoffState {
2020
var attempt: Int
2121
var delay: TimeInterval
2222

@@ -25,7 +25,6 @@ struct ReconnectState {
2525
delay = 1
2626
}
2727

28-
// Initializer with custom values
2928
init(attempt: Int = 0, delay: TimeInterval = 1) {
3029
self.attempt = attempt
3130
self.delay = delay
@@ -234,9 +233,10 @@ final class PeerImpl<Handler: StreamHandler>: Sendable {
234233

235234
fileprivate let connections: ThreadSafeContainer<ConnectionStorage> = .init(.init())
236235
fileprivate let streams: ThreadSafeContainer<[UniqueId: Stream<Handler>]> = .init([:])
237-
fileprivate let reconnectStates: ThreadSafeContainer<[NetAddr: ReconnectState]> = .init([:])
236+
fileprivate let reconnectStates: ThreadSafeContainer<[NetAddr: BackoffState]> = .init([:])
237+
fileprivate let reopenStates: ThreadSafeContainer<[UniqueId: BackoffState]> = .init([:])
238238

239-
let reconnectMaxRetryAttempts = 5
239+
let maxRetryAttempts = 5
240240
let presistentStreamHandler: Handler.PresistentHandler
241241
let ephemeralStreamHandler: Handler.EphemeralHandler
242242

@@ -297,39 +297,70 @@ final class PeerImpl<Handler: StreamHandler>: Sendable {
297297
let state = reconnectStates.read { reconnectStates in
298298
reconnectStates[address] ?? .init()
299299
}
300-
if state.attempt < reconnectMaxRetryAttempts {
301-
reconnectStates.write { reconnectStates in
302-
if var state = reconnectStates[address] {
303-
state.applyBackoff()
304-
reconnectStates[address] = state
305-
}
300+
301+
guard state.attempt < maxRetryAttempts else {
302+
logger.warning("reconnecting to \(address) exceeded max attempts")
303+
return
304+
}
305+
306+
reconnectStates.write { reconnectStates in
307+
if var state = reconnectStates[address] {
308+
state.applyBackoff()
309+
reconnectStates[address] = state
306310
}
307-
Task {
308-
try await Task.sleep(for: .seconds(state.delay))
309-
try connections.write { connections in
310-
if connections.byAddr[address] != nil {
311-
logger.warning("reconnecting to \(address) already connected")
312-
return
313-
}
314-
let quicConn = try QuicConnection(
315-
handler: PeerEventHandler(self),
316-
registration: clientConfiguration.registration,
317-
configuration: clientConfiguration
318-
)
319-
try quicConn.connect(to: address)
320-
let conn = Connection(
321-
quicConn,
322-
impl: self,
323-
role: role,
324-
remoteAddress: address,
325-
initiatedByLocal: true
326-
)
327-
connections.byAddr[address] = conn
328-
connections.byId[conn.id] = conn
311+
}
312+
Task {
313+
try await Task.sleep(for: .seconds(state.delay))
314+
try connections.write { connections in
315+
if connections.byAddr[address] != nil {
316+
logger.warning("reconnecting to \(address) already connected")
317+
return
329318
}
319+
let quicConn = try QuicConnection(
320+
handler: PeerEventHandler(self),
321+
registration: clientConfiguration.registration,
322+
configuration: clientConfiguration
323+
)
324+
try quicConn.connect(to: address)
325+
let conn = Connection(
326+
quicConn,
327+
impl: self,
328+
role: role,
329+
remoteAddress: address,
330+
initiatedByLocal: true
331+
)
332+
connections.byAddr[address] = conn
333+
connections.byId[conn.id] = conn
334+
}
335+
}
336+
}
337+
338+
func reopenUpStream(connection: Connection<Handler>, kind: Handler.PresistentHandler.StreamKind) {
339+
let state = reopenStates.read { states in
340+
states[connection.id] ?? .init()
341+
}
342+
343+
guard state.attempt < maxRetryAttempts else {
344+
logger.warning("Reopen attempt for stream \(kind) on connection \(connection.id) exceeded max attempts")
345+
return
346+
}
347+
348+
reopenStates.write { states in
349+
if var state = states[connection.id] {
350+
state.applyBackoff()
351+
states[connection.id] = state
352+
}
353+
}
354+
355+
Task {
356+
try await Task.sleep(for: .seconds(state.delay))
357+
do {
358+
logger.debug("Attempting to reopen UP stream of kind \(kind) for connection \(connection.id)")
359+
try connection.createPreistentStream(kind: kind)
360+
} catch {
361+
logger.error("Failed to reopen UP stream for connection \(connection.id): \(error)")
362+
reopenUpStream(connection: connection, kind: kind)
330363
}
331-
} else {
332-
logger.warning("reconnect attempt exceeded max attempts")
333364
}
334365
}
335366

@@ -546,6 +577,10 @@ private struct PeerEventHandler<Handler: StreamHandler>: QuicEventHandler {
546577
}
547578
if let conn {
548579
conn.streamStarted(stream: stream)
580+
// Check
581+
impl.reopenStates.write { states in
582+
states[conn.id] = nil
583+
}
549584
}
550585
}
551586

@@ -562,12 +597,25 @@ private struct PeerEventHandler<Handler: StreamHandler>: QuicEventHandler {
562597
let stream = impl.streams.read { streams in
563598
streams[quicStream.id]
564599
}
600+
565601
if let stream {
566602
let connection = impl.connections.read { connections in
567603
connections.byId[stream.connectionId]
568604
}
569605
if let connection {
570606
connection.streamClosed(stream: stream, abort: !status.isSucceeded)
607+
if shouldReopenStream(connection: connection, stream: stream, status: status) {
608+
do {
609+
if let kind = stream.kind {
610+
// impl.reopenUpStream(connection: connection, kind: kind);
611+
do {
612+
try connection.createPreistentStream(kind: kind)
613+
} catch {
614+
logger.error("Attempt to recreate the persistent stream failed: \(error)")
615+
}
616+
}
617+
}
618+
}
571619
} else {
572620
logger.warning(
573621
"Stream closed but connection is gone?", metadata: ["streamId": "\(stream.id)"]
@@ -579,4 +627,18 @@ private struct PeerEventHandler<Handler: StreamHandler>: QuicEventHandler {
579627
)
580628
}
581629
}
630+
631+
// TODO: Add all the cases about reopen up stream
632+
private func shouldReopenStream(connection: Connection<Handler>, stream: Stream<Handler>, status: QuicStatus) -> Bool {
633+
// Only reopen if the stream is a persistent UP stream and the closure was unexpected
634+
if connection.isClosed || connection.needReconnect || stream.kind == nil {
635+
return false
636+
}
637+
switch QuicStatusCode(rawValue: status.rawValue) {
638+
case .connectionIdle, .badCert:
639+
return false
640+
default:
641+
return !status.isSucceeded
642+
}
643+
}
582644
}

Networking/Tests/NetworkingTests/PeerTests.swift

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,124 @@ struct PeerTests {
143143
typealias EphemeralHandler = MockEphemeralStreamHandler
144144
}
145145

146+
@Test
147+
func reopenUpStream() async throws {
148+
let handler2 = MockPresentStreamHandler()
149+
var messageData = Data("reopen up stream".utf8)
150+
let peer1 = try Peer(
151+
options: PeerOptions<MockStreamHandler>(
152+
role: .validator,
153+
listenAddress: NetAddr(ipAddress: "127.0.0.1", port: 0)!,
154+
genesisHeader: Data32(),
155+
secretKey: Ed25519.SecretKey(from: Data32.random()),
156+
presistentStreamHandler: MockPresentStreamHandler(),
157+
ephemeralStreamHandler: MockEphemeralStreamHandler(),
158+
serverSettings: .defaultSettings,
159+
clientSettings: .defaultSettings
160+
)
161+
)
162+
let peer2 = try Peer(
163+
options: PeerOptions<MockStreamHandler>(
164+
role: .validator,
165+
listenAddress: NetAddr(ipAddress: "127.0.0.1", port: 0)!,
166+
genesisHeader: Data32(),
167+
secretKey: Ed25519.SecretKey(from: Data32.random()),
168+
presistentStreamHandler: handler2,
169+
ephemeralStreamHandler: MockEphemeralStreamHandler(),
170+
serverSettings: .defaultSettings,
171+
clientSettings: .defaultSettings
172+
)
173+
)
174+
try? await Task.sleep(for: .milliseconds(100))
175+
176+
let connection = try peer1.connect(
177+
to: peer2.listenAddress(), role: .validator
178+
)
179+
try? await Task.sleep(for: .milliseconds(100))
180+
181+
peer1.broadcast(
182+
kind: .uniqueA, message: .init(kind: .uniqueA, data: messageData)
183+
)
184+
try? await Task.sleep(for: .milliseconds(100))
185+
let lastReceivedData = await handler2.lastReceivedData
186+
#expect(lastReceivedData == messageData)
187+
188+
try? await Task.sleep(for: .milliseconds(100))
189+
// Simulate abnormal close stream
190+
let stream = connection.presistentStreams.read { presistentStreams in
191+
presistentStreams[.uniqueA]
192+
}
193+
stream!.close(abort: true)
194+
// Wait to simulate downtime & reopen up stream 3~5s
195+
try? await Task.sleep(for: .milliseconds(3000))
196+
messageData = Data("reopen up stream data".utf8)
197+
peer1.broadcast(
198+
kind: .uniqueA, message: .init(kind: .uniqueA, data: messageData)
199+
)
200+
try await Task.sleep(for: .milliseconds(1000))
201+
let lastReceivedData2 = await handler2.lastReceivedData
202+
#expect(lastReceivedData2 == messageData)
203+
}
204+
205+
@Test
206+
func regularClosedStream() async throws {
207+
let handler2 = MockPresentStreamHandler()
208+
var messageData = Data("reopen up stream".utf8)
209+
let peer1 = try Peer(
210+
options: PeerOptions<MockStreamHandler>(
211+
role: .validator,
212+
listenAddress: NetAddr(ipAddress: "127.0.0.1", port: 0)!,
213+
genesisHeader: Data32(),
214+
secretKey: Ed25519.SecretKey(from: Data32.random()),
215+
presistentStreamHandler: MockPresentStreamHandler(),
216+
ephemeralStreamHandler: MockEphemeralStreamHandler(),
217+
serverSettings: .defaultSettings,
218+
clientSettings: .defaultSettings
219+
)
220+
)
221+
let peer2 = try Peer(
222+
options: PeerOptions<MockStreamHandler>(
223+
role: .validator,
224+
listenAddress: NetAddr(ipAddress: "127.0.0.1", port: 0)!,
225+
genesisHeader: Data32(),
226+
secretKey: Ed25519.SecretKey(from: Data32.random()),
227+
presistentStreamHandler: handler2,
228+
ephemeralStreamHandler: MockEphemeralStreamHandler(),
229+
serverSettings: .defaultSettings,
230+
clientSettings: .defaultSettings
231+
)
232+
)
233+
try? await Task.sleep(for: .milliseconds(100))
234+
235+
let connection = try peer1.connect(
236+
to: peer2.listenAddress(), role: .validator
237+
)
238+
try? await Task.sleep(for: .milliseconds(100))
239+
240+
peer1.broadcast(
241+
kind: .uniqueA, message: .init(kind: .uniqueA, data: messageData)
242+
)
243+
try? await Task.sleep(for: .milliseconds(100))
244+
let lastReceivedData = await handler2.lastReceivedData
245+
#expect(lastReceivedData == messageData)
246+
247+
try? await Task.sleep(for: .milliseconds(100))
248+
// Simulate regular close stream
249+
let stream = connection.presistentStreams.read { presistentStreams in
250+
presistentStreams[.uniqueA]
251+
}
252+
stream!.close(abort: false)
253+
// Wait to simulate downtime
254+
try? await Task.sleep(for: .milliseconds(3000))
255+
messageData = Data("close up stream".utf8)
256+
peer1.broadcast(
257+
kind: .uniqueA, message: .init(kind: .uniqueA, data: messageData)
258+
)
259+
try await Task.sleep(for: .milliseconds(1000))
260+
let lastReceivedData2 = await handler2.lastReceivedData
261+
#expect(lastReceivedData2 != messageData)
262+
}
263+
146264
@Test
147265
func concurrentPeerConnection() async throws {
148266
let peer1 = try Peer(
@@ -611,10 +729,11 @@ struct PeerTests {
611729
data: Data("Message from peer \(i)".utf8)
612730
)
613731
peer.broadcast(kind: message.kind, message: message)
732+
try? await Task.sleep(for: .milliseconds(50))
614733
}
615734

616735
// Wait for message propagation
617-
try? await Task.sleep(for: .milliseconds(100))
736+
try? await Task.sleep(for: .milliseconds(1000))
618737

619738
// everyone should receive two messages
620739
for (idx, handler) in handlers.enumerated() {

Utils/Sources/Utils/UniqueId.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ extension UniqueId: Equatable {
2121
public static func == (lhs: UniqueId, rhs: UniqueId) -> Bool {
2222
lhs.id == rhs.id
2323
}
24+
25+
public static func < (lhs: UniqueId, rhs: UniqueId) -> Bool {
26+
lhs.id < rhs.id
27+
}
2428
}
2529

2630
extension UniqueId: Hashable {

0 commit comments

Comments
 (0)