Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions Sources/Phoenix/PhoenixChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private extension PhoenixChannel {

future?.resolve((ref, reply))
return reply
} catch let error as JoinTimeOutError {
} catch let error as JoinTimeoutError {
state.access { $0.didFailJoin(clearJoinRef: true) }?.fail(TimeoutError())
tasks.cancel(forKey: "rejoin")

Expand All @@ -237,10 +237,16 @@ private extension PhoenixChannel {
String(describing: TimeoutError())
)

await sendLeaveAfterJoinTimeout(
joinRef: error.joinRef,
timeout: timeout
)
if let joinRef = error.joinRef {
tasks.storedNewTask(key: "leave-\(joinRef)") { [weak self] in
guard let self else { return }
await sendLeaveAfterJoinTimeout(
joinRef: joinRef,
timeout: timeout
)
}
}

scheduleRejoinIfPossible(timeout: timeout)

throw TimeoutError()
Expand Down Expand Up @@ -269,10 +275,10 @@ private extension PhoenixChannel {
}

func sendLeaveAfterJoinTimeout(
joinRef: Ref?,
joinRef: Ref,
timeout: TimeInterval?
) async {
guard let joinRef else { return }
defer { tasks.cancel(forKey: "leave-\(joinRef)") }

let timeout = timeout ?? TimeInterval(nanoseconds: socket.timeout)
let push = Push(
Expand Down Expand Up @@ -425,7 +431,7 @@ private struct State: @unchecked Sendable {
do {
message = try await socket.request(push)
} catch is TimeoutError {
throw JoinTimeOutError(joinRef: push.ref)
throw JoinTimeoutError(joinRef: push.ref)
}

let (ref, isOk, payload) = try message.refAndReply
Expand Down Expand Up @@ -696,6 +702,6 @@ private struct State: @unchecked Sendable {

private struct NotReadyToJoinError: Error {}

private struct JoinTimeOutError: Error {
private struct JoinTimeoutError: Error {
let joinRef: Ref?
}
47 changes: 32 additions & 15 deletions Sources/Phoenix/PhoenixSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ extension PhoenixSocket {

do {
if let timeout = Self.reconnectDelay(attempts: attempts) {
try await Task.sleep(nanoseconds: NSEC_PER_SEC * UInt64(timeout))
try await Task.sleep(nanoseconds: timeout.nanoseconds)
}

guard case .waitingToReconnect = _connectionState.value,
Comment thread
atdrendel marked this conversation as resolved.
Expand All @@ -451,12 +451,21 @@ extension PhoenixSocket {

try Task.checkCancellation()

_connectionState.value = .open(ws)
pushes.resume()
listen()
flush()
scheduleHeartbeat()
switch _connectionState.value {
case let .connecting(_ws) where _ws.id == ws.id:
_connectionState.value = .open(ws)
pushes.resume()
listen()
flush()
scheduleHeartbeat()

case let .closing(_ws) where _ws.id == ws.id && shouldReconnect:
_connectionState.value = .closed(connectionAttempts: 0)
await doConnect()

default:
break
}
} catch {
guard !Task.isCancelled else { return }
_connectionState.value = .closed(connectionAttempts: attempts + 1)
Expand Down Expand Up @@ -501,14 +510,20 @@ extension PhoenixSocket {
) async {
let timeout = TimeInterval(nanoseconds: timeout ?? self.timeout)

func cancelAllInputOutput() {
switch _connectionState.value {
case let .connecting(ws) where ws.id == id:
os_log(
"close: %@",
log: .phoenix,
type: .error,
String(describing: error)
)
_connectionState.value = .closing(ws)
pushes.pause(error: error)
tasks.cancelAll()
}
tasks.cancelAll(where: { $0 != "reconnect" })
try? await ws.close(closeCode(from: error), timeout)

switch _connectionState.value {
case let .connecting(ws) where ws.id == id,
let .open(ws) where ws.id == id:
case let .open(ws) where ws.id == id:
os_log(
"close: %@",
log: .phoenix,
Expand All @@ -517,7 +532,8 @@ extension PhoenixSocket {
)

_connectionState.value = .closing(ws)
cancelAllInputOutput()
pushes.pause(error: error)
tasks.cancelAll()
try? await ws.close(closeCode(from: error), timeout)
_connectionState.value = .closed(connectionAttempts: 0)

Expand All @@ -538,15 +554,16 @@ private extension PhoenixSocket {
id += 1
return id
}

return try await makeWebSocket(
id, // id
url(), // url
.init(), // options
{}, // onOpen
{ [id] close in
Task { [weak self] in
guard let self, !Task.isCancelled else { return }
guard let self,
!Task.isCancelled
else { return }
await doCloseFromServer(
id: id,
error: WebSocketError.closeCodeAndReason(
Expand Down
77 changes: 44 additions & 33 deletions Tests/PhoenixTests/PhoenixChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,52 +187,63 @@ final class PhoenixChannelTests: XCTestCase {

func testRetriesJoinWithBackoffAfterTimeout() async throws {
try await withSocket { socket in
await socket.connect()
let channel = await self.makeChannel(
rejoinDelay: [0, 0.001, 0.1, 100],
socket
)
try await self.serialized {
await socket.connect()
let channel = await self.makeChannel(
rejoinDelay: [0, 0.001, 0.1, 100],
socket
)

let start = Date()
let start = Date.now
let joinFuture = AsyncThrowingFuture<Void>(timeout: 2)
await self.yield(3)

Task {
var attempt = 0
for await msg in self.outgoingMessages {
let message = try! Message.decode(msg)
let messagesTask = self.task {
var attempt = 0
for await msg in self.outgoingMessages {
let message = try! Message.decode(msg)

// Ignore leave messages
if message.event == .leave { continue }
XCTAssert(message.event == .join)
// Ignore leave messages
if message.event == .leave { continue }
XCTAssert(message.event == .join)

defer { attempt += 1 }
defer { attempt += 1 }

switch attempt {
case 0:
break
switch attempt {
case 0:
break

case 1:
break
case 1:
break

case 2:
try self.sendReply(for: message)
case 2:
try self.sendReply(for: message)
joinFuture.resolve()

default:
XCTFail()
default:
XCTFail()
}
}
}
}
defer { messagesTask.cancel() }

Task {
await self.wait()
try await channel.join(timeout: 0.01)
}
await self.yield(2)

await AssertTrueEventually(channel.isJoined)
let joinTask = self.task {
try await channel.join(timeout: 0.01)
}
defer { joinTask.cancel() }

XCTAssertGreaterThanOrEqual(
Date().timeIntervalSince(start),
0.11
)
try await joinFuture.value
await self.yield()
XCTAssertTrue(channel.isJoined)

let stop = Date.now
XCTAssertGreaterThanOrEqual(
stop.timeIntervalSince(start),
0.11
)
}
}
}

Expand Down
Loading
Loading