bitchat/bitchatTests/Services/BLEOutboundFragmentTransferSchedulerTests.swift
krish rathi 6414a59851
fix(ble): don't spend the fragment scheduler's slot budget on blocked requests (#1530)
reservePendingStarts() decremented availableSlots for every dequeued pending
transfer before checking whether it would actually be admitted. A request
blocked because its transferId is already active (a resend of in-flight
content sitting at the front of the queue) still consumed a slot even
though it was deferred back into the queue rather than started -- so a
single blocked front-of-queue item could zero out the budget and end the
loop before ever reaching a later, unrelated, genuinely startable pending
transfer. That transfer then sat starved until some other transfer
happened to complete and trigger another pass, rather than starting
immediately when real capacity was already available.

Move the decrement to the point where a transfer is actually admitted into
activeTransfers, so only genuine starts spend the budget.
2026-07-30 18:14:57 +01:00

351 lines
16 KiB
Swift

import BitFoundation
import Foundation
import Testing
@testable import bitchat
struct BLEOutboundFragmentTransferSchedulerTests {
@Test
func submitStartsPublicMessageWithoutTransferReservation() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let request = makeRequest(type: MessageType.message.rawValue, transferId: nil)
let result = scheduler.submit(request, maxConcurrentTransfers: 1)
if case let .start(_, reservedTransferId) = result {
#expect(reservedTransferId == nil)
#expect(scheduler.activeCount == 0)
#expect(scheduler.pendingCount == 0)
} else {
Issue.record("Expected non-file fragments to start without reserving a transfer slot")
}
}
@Test
func explicitTransferIDReservesEncryptedPrivateFileFragments() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let request = makeRequest(
type: MessageType.noiseEncrypted.rawValue,
transferId: "private-media"
)
let result = scheduler.submit(request, maxConcurrentTransfers: 1)
if case let .start(_, reservedTransferId) = result {
#expect(reservedTransferId == "private-media")
#expect(scheduler.activeCount == 1)
} else {
Issue.record("Expected encrypted private media to reserve its progress slot")
}
}
@Test
func submitQueuesFileTransferWhenSlotsAreFull() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let first = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "first")
let second = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "second")
guard case let .start(_, firstReservation?) = scheduler.submit(first, maxConcurrentTransfers: 1) else {
Issue.record("Expected first file transfer to reserve a slot")
return
}
#expect(firstReservation == "first")
let result = scheduler.submit(second, maxConcurrentTransfers: 1)
if case let .queued(_, transferId, position) = result {
#expect(transferId == "second")
#expect(position == .back)
#expect(scheduler.activeCount == 1)
#expect(scheduler.pendingCount == 1)
} else {
Issue.record("Expected second file transfer to queue while slots are full")
}
}
@Test
func strictDirectTransferIsRejectedWithoutBeingQueuedWhenSlotsAreFull() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let active = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "active")
let strict = makeRequest(
type: MessageType.fileTransfer.rawValue,
transferId: "strict",
requireDirectPeerLink: true
)
guard case .start = scheduler.submit(active, maxConcurrentTransfers: 1) else {
Issue.record("Expected active transfer to reserve the only slot")
return
}
let result = scheduler.submit(strict, maxConcurrentTransfers: 1)
if case let .rejectedStrict(request, transferId) = result {
#expect(request.requireDirectPeerLink)
#expect(transferId == "strict")
#expect(scheduler.activeCount == 1)
#expect(scheduler.pendingCount == 0)
} else {
Issue.record("Expected strict transfer to reject instead of entering the pending queue")
}
}
@Test
func submitQueuesDuplicateActiveTransferAtFront() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let request = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "same")
_ = scheduler.submit(request, maxConcurrentTransfers: 2)
let result = scheduler.submit(request, maxConcurrentTransfers: 2)
if case let .queued(_, transferId, position) = result {
#expect(transferId == "same")
#expect(position == .front)
#expect(scheduler.activeCount == 1)
#expect(scheduler.pendingCount == 1)
} else {
Issue.record("Expected duplicate active transfer to queue at the front")
}
}
@Test
func resendWithoutTransferIdOfActiveBroadcastContentIsDropped() {
// Field bug: a gossip-sync replay re-fragmented a 41KB voice file
// that was still being broadcast, sending two complete fragment
// streams. The resend path has no explicit transferId; drop it while
// a covering transfer of the same bytes is in flight.
var scheduler = BLEOutboundFragmentTransferScheduler()
let original = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "app-id", payload: "voice-file")
let resend = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: nil, payload: "voice-file")
_ = scheduler.submit(original, maxConcurrentTransfers: 2)
let result = scheduler.submit(resend, maxConcurrentTransfers: 2)
if case let .droppedDuplicate(_, activeTransferId) = result {
#expect(activeTransferId == "app-id")
#expect(scheduler.activeCount == 1)
#expect(scheduler.pendingCount == 0)
} else {
Issue.record("Expected the transferId-less resend of in-flight broadcast content to be dropped")
}
}
@Test
func directedResendToAnUncoveredAudienceStillRuns() {
// The in-flight copy is directed to one peer; a resend of the same
// bytes to a different peer is not redundant.
var scheduler = BLEOutboundFragmentTransferScheduler()
let toFirstPeer = makeRequest(
type: MessageType.fileTransfer.rawValue,
transferId: "app-id",
payload: "shared-file",
directedPeer: PeerID(str: "1122334455667788")
)
let toSecondPeer = makeRequest(
type: MessageType.fileTransfer.rawValue,
transferId: nil,
payload: "shared-file",
directedPeer: PeerID(str: "8877665544332211")
)
_ = scheduler.submit(toFirstPeer, maxConcurrentTransfers: 2)
let result = scheduler.submit(toSecondPeer, maxConcurrentTransfers: 2)
if case .start = result {
#expect(scheduler.activeCount == 2)
} else {
Issue.record("Expected a resend directed at an uncovered peer to start")
}
}
@Test
func explicitTransferIdSendIsNeverDroppedAsDuplicate() {
// App-initiated sends carry a transferId the progress UI tracks;
// only transferId-less resend paths are deduplicated.
var scheduler = BLEOutboundFragmentTransferScheduler()
let first = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "send-1", payload: "same-bytes")
let second = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "send-2", payload: "same-bytes")
_ = scheduler.submit(first, maxConcurrentTransfers: 2)
let result = scheduler.submit(second, maxConcurrentTransfers: 2)
if case let .start(_, reservedTransferId?) = result {
#expect(reservedTransferId == "send-2")
} else {
Issue.record("Expected an explicit-transferId send to run despite identical content")
}
}
@Test
func duplicateOfPendingContentIsDroppedAtSubmit() {
// A duplicate must not queue behind a pending copy of the same
// content and resend the whole file when the slot frees.
var scheduler = BLEOutboundFragmentTransferScheduler()
let active = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "active", payload: "file-a")
let queuedContent = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "waiting", payload: "file-b")
let queuedDuplicate = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: nil, payload: "file-b")
_ = scheduler.submit(active, maxConcurrentTransfers: 1)
_ = scheduler.submit(queuedContent, maxConcurrentTransfers: 1)
if case .droppedDuplicate = scheduler.submit(queuedDuplicate, maxConcurrentTransfers: 1) {
// Dropped immediately: the pending "waiting" transfer covers it.
} else {
Issue.record("Expected the duplicate of pending content to be dropped at submit")
}
#expect(scheduler.pendingCount == 1)
}
@Test
func resendAfterCompletionIsAllowed() {
// Duplicate suppression only covers in-flight transfers: a peer that
// requests the file after the stream completed must get a resend.
var scheduler = BLEOutboundFragmentTransferScheduler()
let original = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "app-id", payload: "voice-file")
let resend = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: nil, payload: "voice-file")
_ = scheduler.submit(original, maxConcurrentTransfers: 1)
let didActivate = scheduler.activateReservedTransfer(id: "app-id", totalFragments: 1, workItems: [])
#expect(didActivate)
#expect(scheduler.markFragmentSent(transferId: "app-id") == .complete(sentFragments: 1, totalFragments: 1))
if case .start = scheduler.submit(resend, maxConcurrentTransfers: 1) {
#expect(scheduler.activeCount == 1)
} else {
Issue.record("Expected a resend after completion to start")
}
}
@Test
func cancelActiveTransferReturnsScheduledWorkItems() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let request = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "active")
_ = scheduler.submit(request, maxConcurrentTransfers: 1)
let workItem = DispatchWorkItem {}
let didActivate = scheduler.activateReservedTransfer(id: "active", totalFragments: 2, workItems: [workItem])
#expect(didActivate)
if case let .active(transferId, workItems) = scheduler.cancelTransfer("active") {
#expect(transferId == "active")
#expect(workItems.count == 1)
#expect(scheduler.activeCount == 0)
} else {
Issue.record("Expected active transfer cancellation to return its work items")
}
}
@Test
func completedTransferFreesSlotForPendingTransfer() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let first = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "first")
let second = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "second")
_ = scheduler.submit(first, maxConcurrentTransfers: 1)
let didActivate = scheduler.activateReservedTransfer(id: "first", totalFragments: 2, workItems: [])
#expect(didActivate)
_ = scheduler.submit(second, maxConcurrentTransfers: 1)
#expect(scheduler.markFragmentSent(transferId: "first") == .progress(sentFragments: 1, totalFragments: 2))
#expect(scheduler.markFragmentSent(transferId: "first") == .complete(sentFragments: 2, totalFragments: 2))
let starts = scheduler.reservePendingStarts(maxConcurrentTransfers: 1)
#expect(starts.count == 1)
if case let .start(_, reservedTransferId?) = starts.first {
#expect(reservedTransferId == "second")
#expect(scheduler.activeCount == 1)
#expect(scheduler.pendingCount == 0)
} else {
Issue.record("Expected pending transfer to reserve the freed slot")
}
}
@Test
func blockedDuplicateAtFrontOfQueueDoesNotStarveALaterUnrelatedPendingTransfer() {
// Bug: reservePendingStarts spent the slot budget on a pending
// request the moment it was dequeued, before checking whether that
// request would actually be admitted. A resend of still-active
// content sitting at the front of the queue therefore consumed a
// slot even though it was deferred back to the queue rather than
// started -- starving an unrelated, genuinely startable transfer
// right behind it until some other transfer happened to complete.
var scheduler = BLEOutboundFragmentTransferScheduler()
let t1 = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "t1", payload: "file-a")
let t2 = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "t2", payload: "file-b")
let dupT1 = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "t1", payload: "file-a")
let unrelated = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "t3", payload: "file-c")
_ = scheduler.submit(t1, maxConcurrentTransfers: 2)
_ = scheduler.submit(t2, maxConcurrentTransfers: 2)
#expect(scheduler.activeCount == 2)
// Both slots are full, so a resend of "t1" (still active) and an
// unrelated transfer both land in the pending queue, in that order.
_ = scheduler.submit(dupT1, maxConcurrentTransfers: 2)
_ = scheduler.submit(unrelated, maxConcurrentTransfers: 2)
#expect(scheduler.pendingCount == 2)
// "t2" finishes; "t1" stays active, so the queued "t1" resend at the
// front of the queue is still blocked when we reserve pending starts.
let didActivate = scheduler.activateReservedTransfer(id: "t2", totalFragments: 1, workItems: [])
#expect(didActivate)
#expect(scheduler.markFragmentSent(transferId: "t2") == .complete(sentFragments: 1, totalFragments: 1))
let starts = scheduler.reservePendingStarts(maxConcurrentTransfers: 2)
let startedTransferIds: [String] = starts.compactMap {
if case let .start(_, reservedTransferId) = $0 { return reservedTransferId }
return nil
}
#expect(startedTransferIds == ["t3"], "the unrelated pending transfer must start in the same pass despite the blocked front item")
#expect(scheduler.activeCount == 2, "t1 (still running) and the newly-started t3")
#expect(scheduler.pendingCount == 1, "only the blocked t1 resend remains queued")
}
@Test
func removeAllReturnsActiveWorkItemsAndDropsPendingTransfers() {
var scheduler = BLEOutboundFragmentTransferScheduler()
let active = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "active")
let pending = makeRequest(type: MessageType.fileTransfer.rawValue, transferId: "pending")
let workItem = DispatchWorkItem {}
_ = scheduler.submit(active, maxConcurrentTransfers: 1)
let didActivate = scheduler.activateReservedTransfer(id: "active", totalFragments: 1, workItems: [workItem])
#expect(didActivate)
_ = scheduler.submit(pending, maxConcurrentTransfers: 1)
let removed = scheduler.removeAll()
#expect(removed.count == 1)
#expect(removed.first?.id == "active")
#expect(removed.first?.workItems.count == 1)
#expect(scheduler.activeCount == 0)
#expect(scheduler.pendingCount == 0)
}
private func makeRequest(
type: UInt8,
transferId: String?,
payload: String? = nil,
directedPeer: PeerID? = nil,
requireDirectPeerLink: Bool = false
) -> BLEOutboundFragmentTransferRequest {
BLEOutboundFragmentTransferRequest(
packet: BitchatPacket(
type: type,
senderID: Data([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77]),
recipientID: nil,
timestamp: 0x0102030405,
payload: Data((payload ?? transferId ?? "payload").utf8),
signature: nil,
ttl: 3
),
pad: false,
maxChunk: nil,
directedPeer: directedPeer,
transferId: transferId,
requireDirectPeerLink: requireDirectPeerLink
)
}
}