Skip to content

Commit dd35f5d

Browse files
committed
Retry stale PTS snapshots without resetting account state
1 parent 2874573 commit dd35f5d

6 files changed

Lines changed: 40 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- Failed TCP endpoints share monotonic cooldown and a single recovery probe across contexts; duplicate connects reuse the existing connection, and closed interfaces release read buffers.
1111
- WEB resume requests coalesce per carrier. Stale callbacks cannot restart disabled/replaced profiles; duplicate failures count once and short-lived sessions retain backoff history.
1212
- Removed the global WebSocket fallback cancellation and the mid-transaction state replay timeout. State replay finishes atomically instead of committing a prefix and resetting synchronization.
13+
- Stale PTS snapshots retry difference synchronization without a full account reset. WebSocket dial requests are one-shot across fallback gaps and use a monotonic fallback clock.
14+
- Fetch completion logs identify the location and episode; TCP attempts have IDs for correlation and log the effective connection deadline.
1315
- Download EOF handling accepts empty boundary responses and out-of-order partial responses, without treating requested ranges as received bytes.
1416
- Memory-pressure cache trimming retains contexts owned by open peer views.
1517
- Recovery tests exercise production EOF handling, endpoint health, WEB manager lifecycle and the Network.framework interface against a loopback echo server.

‎Tests/NetworkRecovery/run.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def extract(path, start, end):
1919
' private func commitPendingReadyPart(', '\n }').replace('private func', 'func', 1)
2020
trim = extract('submodules/TelegramCore/Sources/State/AccountViewTracker.swift',
2121
' func trimCachedData()', '\n }')
22+
ws_connect = extract('submodules/TelegramCore/Sources/Network/MTWebSocketConnectionInterface.swift',
23+
' func connect(timeout: Double)', '\n }')
2224
fixture = '''import Foundation
2325
final class Logger {
2426
static let shared = Logger()
@@ -44,6 +46,17 @@ def extract(path, start, end):
4446
let queue = ImmediateQueue()
4547
var cachedDataContexts: [Int: PeerContext] = [:]
4648
''' + trim + '\n}\n'
49+
fixture += '''
50+
struct Selector { var resets = 0; mutating func reset() { resets += 1 } }
51+
final class WebSocketDial {
52+
var connection: Int?
53+
var connectTimeout: Double = 0
54+
var didRequestConnect = false
55+
var reportedDisconnection = false
56+
var endpointSelector = Selector()
57+
var dials = 0
58+
func dialCurrentCandidate() { dials += 1 }
59+
''' + ws_connect + '\n}\n'
4760

4861
tests = '''
4962
var health = NetworkEndpointHealth()
@@ -103,6 +116,14 @@ def extract(path, start, end):
103116
tracker.trimCachedData()
104117
precondition(tracker.cachedDataContexts.count == 1 && tracker.cachedDataContexts[1] === active)
105118
print("Endpoint cooldown, probe ownership, EOF permutations and active cache ownership: passed")
119+
let ws = WebSocketDial()
120+
for _ in 0..<100 { ws.connect(timeout: 12) }
121+
precondition(ws.dials == 1 && ws.endpointSelector.resets == 1)
122+
let closed = WebSocketDial()
123+
closed.reportedDisconnection = true
124+
closed.connect(timeout: 12)
125+
precondition(closed.dials == 0)
126+
print("WebSocket connect coalescing across candidate gaps and terminal close: passed")
106127
'''
107128
with tempfile.TemporaryDirectory(prefix='telegram-recovery-') as tmp:
108129
file = pathlib.Path(tmp) / 'main.swift'

‎submodules/FetchManagerImpl/Sources/FetchManagerImpl.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ private final class FetchManagerCategoryContext {
295295
return .single(type)
296296
}
297297
|> deliverOnMainQueue).start(next: { _ in
298-
Logger.shared.log("FetchManager", "Completed fetching \(entry.resourceReference.resource.id.stringRepresentation)")
298+
Logger.shared.log("FetchManager", "Completed fetching \(entry.resourceReference.resource.id.stringRepresentation) [\(entry.id.location), episode \(entry.episode)]")
299299
entryCompleted(id)
300300
})
301301
} else {
@@ -460,7 +460,7 @@ private final class FetchManagerCategoryContext {
460460
return .single(type)
461461
}
462462
|> deliverOnMainQueue).start(next: { _ in
463-
Logger.shared.log("FetchManager", "Completed fetching \(entry.resourceReference.resource.id.stringRepresentation)")
463+
Logger.shared.log("FetchManager", "Completed fetching \(entry.resourceReference.resource.id.stringRepresentation) [\(entry.id.location), episode \(entry.episode)]")
464464
entryCompleted(topEntryId)
465465
})
466466
}

‎submodules/TelegramCore/Sources/Network/MTWebSocketConnectionInterface.swift‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ final class MTWebSocketFallbackCoordinator {
5151
/// fallback is engaged it answers true exactly once per wait, and false to everything else until
5252
/// that attempt has reported success or failure.
5353
func shouldAttemptWebSocket() -> Bool {
54-
let timestamp = CFAbsoluteTimeGetCurrent()
54+
let timestamp = ProcessInfo.processInfo.systemUptime
5555
var grantedProbe = false
5656
let state = self.state.modify { state in
5757
var state = state
@@ -86,7 +86,7 @@ final class MTWebSocketFallbackCoordinator {
8686
}
8787

8888
func recordAllEndpointsFailed() {
89-
let timestamp = CFAbsoluteTimeGetCurrent()
89+
let timestamp = ProcessInfo.processInfo.systemUptime
9090
var engagedFallback = false
9191
var probeFailed = false
9292
let state = self.state.modify { state in
@@ -200,6 +200,7 @@ final class MTWebSocketConnectionInterface: NSObject, MTTcpConnectionInterface {
200200

201201
private var connection: NWConnection?
202202
private var reportedDisconnection = false
203+
private var didRequestConnect = false
203204
private var currentInterfaceIsWifi = true
204205

205206
private var connectTimeout: Double = 12.0
@@ -316,10 +317,10 @@ final class MTWebSocketConnectionInterface: NSObject, MTTcpConnectionInterface {
316317
}
317318

318319
func connect(timeout: Double) {
319-
if self.connection != nil {
320-
assertionFailure("A connection already exists")
321-
return
322-
}
320+
// The interface owns one MTProto stream, including the jitter gap where its
321+
// NWConnection is temporarily nil. Repeated requests must not reset candidates.
322+
guard !self.didRequestConnect, !self.reportedDisconnection else { return }
323+
self.didRequestConnect = true
323324
self.connectTimeout = timeout
324325
self.endpointSelector.reset()
325326
self.dialCurrentCandidate()

‎submodules/TelegramCore/Sources/Network/NetworkFrameworkTcpConnectionInterface.swift‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInt
278278
return
279279
}
280280
self.connectTimeoutTimer = nil
281-
Logger.shared.log("Network", "NW connect to \(self.endpointDescription) timed out after \(timeout)s")
281+
Logger.shared.log("Network", "NW connect to \(self.endpointDescription) timed out after \(connectTimeout)s (attempt \(self.attempt))")
282282
if self.isCountedInFlight {
283283
Impl.endpointHealth.failed(endpoint: self.endpointDescription, attempt: self.attempt, now: ProcessInfo.processInfo.systemUptime)
284284
}
@@ -306,7 +306,7 @@ final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInt
306306
}
307307
self.isCountedInFlight = true
308308
Impl.inFlightConnectCount += 1
309-
Logger.shared.log("Network", "NW connect starting to \(self.endpointDescription), \(Impl.inFlightConnectCount) in flight")
309+
Logger.shared.log("Network", "NW connect starting to \(self.endpointDescription), \(Impl.inFlightConnectCount) in flight (attempt \(self.attempt))")
310310

311311
connection.start(queue: self.queue.queue)
312312

@@ -545,7 +545,7 @@ final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInt
545545
// connection that dies after minutes of traffic look identical in the count above, and
546546
// they are not the same problem — the first is a retry loop with nothing throttling it.
547547
if let error = error, self.isCountedInFlight {
548-
Logger.shared.log("Network", "NW connect to \(self.endpointDescription) failed: \(error)")
548+
Logger.shared.log("Network", "NW connect to \(self.endpointDescription) failed: \(error) (attempt \(self.attempt))")
549549
}
550550
self.leaveInFlight()
551551

‎submodules/TelegramCore/Sources/State/AccountStateManager.swift‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -915,8 +915,11 @@ public final class AccountStateManager {
915915

916916
return (difference, replayedState, false, false)
917917
} else {
918-
Logger.shared.log("State", "replayFinalState returned nil, triggering state reset")
919-
return (nil, nil, true, true)
918+
// A concurrent state/channel update can invalidate the snapshot.
919+
// Retry the difference from current PTS; this is not a reason to
920+
// discard the chat list and advance the account to updates.getState.
921+
Logger.shared.log("State", "replayFinalState snapshot changed, retrying difference")
922+
return (nil, nil, false, false)
920923
}
921924
}
922925
}

0 commit comments

Comments
 (0)