Skip to content

Commit 8d18ffd

Browse files
committed
Track progress events instead of byte changes for stall detection
We can't get low level TCP socket transfer progress from the Node HTTP module, you can only see an internal I/O buffer being filled, you then get silence until that buffer is completely drained (transmitted) and starts to be filled again, this means you consistently get false positives for stall detection.
1 parent 6913195 commit 8d18ffd

File tree

4 files changed

+37
-37
lines changed

4 files changed

+37
-37
lines changed

lib/StallDetector.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export class StallDetector {
77

88
private intervalId: ReturnType<typeof setInterval> | null = null
99
private lastProgressTime = 0
10-
private lastProgressValue = 0
1110
private isActive = false
1211

1312
constructor(options: StallDetectionOptions, onStallDetected: (reason: string) => void) {
@@ -24,7 +23,6 @@ export class StallDetector {
2423
}
2524

2625
this.lastProgressTime = Date.now()
27-
this.lastProgressValue = 0
2826
this.isActive = true
2927

3028
log(
@@ -57,14 +55,12 @@ export class StallDetector {
5755

5856
/**
5957
* Update progress information
60-
* @param progressValue The current progress value (bytes uploaded)
58+
* @param _progressValue The current progress value (bytes uploaded) - currently unused but kept for future use
6159
*/
62-
updateProgress(progressValue: number): void {
63-
// Only update progress time if the value has actually changed
64-
if (progressValue !== this.lastProgressValue) {
65-
this.lastProgressTime = Date.now()
66-
this.lastProgressValue = progressValue
67-
}
60+
updateProgress(_progressValue: number): void {
61+
// Only track that a progress event occurred, not the actual value
62+
// This avoids false positives with NodeHttpStack's buffer behavior
63+
this.lastProgressTime = Date.now()
6864
}
6965

7066
/**

lib/upload.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,6 @@ export class BaseUpload {
119119
// upload options or HEAD response)
120120
private _uploadLengthDeferred: boolean
121121

122-
123-
124122
constructor(file: UploadInput, options: UploadOptions) {
125123
// Warn about removed options from previous versions
126124
if ('resume' in options) {
@@ -288,7 +286,7 @@ export class BaseUpload {
288286
*
289287
* @api private
290288
*/
291-
private async _startParallelUpload(): Promise<void> {
289+
private async _startParallelUpload(): Promise<void> {
292290
const totalSize = this._size
293291
let totalProgress = 0
294292
this._parallelUploads = []
@@ -379,8 +377,6 @@ export class BaseUpload {
379377
// @ts-expect-error `value` is unknown and not an UploadInput
380378
const upload = new BaseUpload(value, options)
381379

382-
383-
384380
upload.start()
385381

386382
// Store the upload in an array, so we can later abort them if necessary.
@@ -778,8 +774,6 @@ export class BaseUpload {
778774
await this.options.onUploadUrlAvailable()
779775
}
780776

781-
782-
783777
await this._saveUploadInUrlStorage()
784778

785779
// Upload has already been completed and we do not need to send additional
@@ -876,11 +870,10 @@ export class BaseUpload {
876870
}
877871
// Don't call _retryOrEmitError here - let the natural error flow handle it
878872
})
879-
} else {
880-
log(
881-
'tus: stall detection is enabled but the HTTP stack does not support progress events, it will be disabled for this upload',
882-
)
883873
}
874+
log(
875+
'tus: stall detection is enabled but the HTTP stack does not support progress events, it will be disabled for this upload',
876+
)
884877
}
885878
return undefined
886879
}

test/spec/test-parallel-uploads.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ describe('tus', () => {
658658
// Track all requests to detect if fresh uploads are created
659659
const allRequests = []
660660
const originalCreateRequest = testStack.createRequest.bind(testStack)
661-
testStack.createRequest = function(method, url) {
661+
testStack.createRequest = (method, url) => {
662662
allRequests.push({ method, url })
663663
return originalCreateRequest(method, url)
664664
}
@@ -773,15 +773,13 @@ describe('tus', () => {
773773
await options.onSuccess.toBeCalled()
774774

775775
// Final verification: count how many POST requests to /uploads we made
776-
const postToUploads = allRequests.filter(r =>
777-
r.method === 'POST' && r.url === 'https://tus.io/uploads'
776+
const postToUploads = allRequests.filter(
777+
(r) => r.method === 'POST' && r.url === 'https://tus.io/uploads',
778778
)
779779

780780
// Should only be 3 POSTs: 2 partial + 1 final concatenation
781781
// If the bug exists, we'd see 4 POSTs (an extra one from the retry)
782782
expect(postToUploads.length).toBe(3)
783783
})
784-
785-
786784
})
787785
})

test/spec/test-stall-detection.js

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class StallTestHttpStack extends TestHttpStack {
7070
return req
7171
}
7272

73-
_setupMethodStall(req, method) {
73+
_setupMethodStall(req, _method) {
7474
const originalAbort = req.abort.bind(req)
7575

7676
req.send = async function (body) {
@@ -92,7 +92,7 @@ class StallTestHttpStack extends TestHttpStack {
9292
return this._requestPromise
9393
}
9494

95-
req.abort = function() {
95+
req.abort = function () {
9696
if (this._rejectRequest) {
9797
this._rejectRequest(new Error('request aborted'))
9898
}
@@ -234,7 +234,7 @@ async function testStallDetectionForMethod(method, uploadOptions = {}) {
234234
const originalLog = console.log
235235
let loggedMessage = ''
236236
console.log = (message) => {
237-
loggedMessage += message + '\n'
237+
loggedMessage += `${message}\n`
238238
}
239239

240240
upload.start()
@@ -444,8 +444,10 @@ describe('tus-stall-detection', () => {
444444
expect(options.onProgress.calls.count()).toBeGreaterThan(0)
445445
})
446446

447-
it('should detect stalls when progress value does not change', async () => {
447+
it('should NOT detect stalls when progress value does not change but events are still fired', async () => {
448+
const file = getBlob('hello world')
448449
const { upload, options, testStack } = createTestUpload({
450+
file,
449451
stallDetection: {
450452
enabled: true,
451453
checkInterval: 50,
@@ -454,14 +456,18 @@ describe('tus-stall-detection', () => {
454456
retryDelays: null,
455457
})
456458

457-
// Create a progress sequence that gets stuck at 300 bytes
459+
// Create a progress sequence that gets stuck at 5 bytes
460+
// but still fires progress events (simulating NodeHttpStack buffer behavior)
458461
const progressSequence = [
459462
{ bytes: 0, delay: 10 },
460-
{ bytes: 100, delay: 10 },
461-
{ bytes: 200, delay: 10 },
462-
{ bytes: 300, delay: 10 },
463-
// Repeat the same value to trigger value-based stall detection
464-
...Array(12).fill({ bytes: 300, delay: 30 }),
463+
{ bytes: 2, delay: 10 },
464+
{ bytes: 5, delay: 10 },
465+
// Repeat the same value - with the new behavior, this should NOT trigger stall detection
466+
// as long as progress events are still being fired
467+
...Array(12).fill({ bytes: 5, delay: 30 }),
468+
// Eventually progress continues
469+
{ bytes: 8, delay: 10 },
470+
{ bytes: 11, delay: 10 },
465471
]
466472

467473
testStack.setNextProgressSequence(progressSequence)
@@ -472,8 +478,15 @@ describe('tus-stall-detection', () => {
472478
const patchReq = await testStack.nextRequest()
473479
expect(patchReq.method).toBe('PATCH')
474480

475-
const error = await options.onError.toBeCalled()
476-
expect(error.message).toContain('stalled: no progress')
481+
// Complete the upload successfully
482+
patchReq.respondWith({
483+
status: 204,
484+
responseHeaders: { 'Upload-Offset': '11' },
485+
})
486+
487+
// The upload should complete successfully without stall detection
488+
await options.onSuccess.toBeCalled()
489+
expect(options.onError.calls.count()).toBe(0)
477490
expect(options.onProgress.calls.count()).toBeGreaterThan(0)
478491
})
479492

0 commit comments

Comments
 (0)