Skip to content

fix(s3): limitar a leitura do corpo do GetObject pelo prazo da tentativa (#22) - #23

Merged
brandao-andre merged 1 commit into
mainfrom
fix/s3-stalled-body-hang
Oct 7, 2026
Merged

brandao-andre merged 1 commit into
mainfrom
fix/s3-stalled-body-hang

Conversation

@brandao-andre

Copy link
Copy Markdown
Contributor

Closes #22

Problema

Para objetos com checksum de objeto inteiro (x-amz-checksum-crc32, FULL_OBJECT), o SDK (responseChecksumValidation: "WHEN_SUPPORTED", padrão) embrulha o corpo do GetObject num stream de validação. Quando o AbortSignal.timeout da tentativa destrói o socket no meio do corpo, esse stream não repassa o erro: transformToByteArray() nunca resolve nem rejeita, nenhuma retentativa acontece e o GET fica pendurado para sempre.

Efeito em cascata: a promessa pendurada prende a entrada de requisição em voo do CachedStore e bloqueia clearCache(). Com isso, a limpeza de um dataset removido por LRU nunca termina, e o open() desse mesmo id, clear() e whenTornDown() ficam parados até reiniciar o processo (o "só voltou depois de reiniciar" da issue).

Correção

S3Store.getObject cria um único AbortSignal.timeout por tentativa e usa esse sinal tanto no client.send quanto na leitura do corpo (helper readBody). Quando o prazo estoura, o body é destruído e a tentativa rejeita com TimeoutError, que já é retentável, então a política normal de retry segue. O timeout passa a limitar requisição e corpo; a documentação da opção foi atualizada.

A validação de checksum do SDK continua ligada. Avaliamos desligá-la (WHEN_REQUIRED, sugestão da issue). Também resolve, mas não é necessário e tem pouco ganho. Medição local, objeto de 5 MB:

p50 por GET CPU por GET
WHEN_SUPPORTED (valida) 7,2 ms 15,8 ms
WHEN_REQUIRED 5,1 ms 11,0 ms

Os números de CPU incluem o servidor de teste, que roda no mesmo processo; só a diferença entre eles importa. São ~2 ms por chunk com zlib.crc32 nativo (Node ≥ 20.15/22.2), cerca de 1% do p50 de S3 dentro da AWS. Em troca, mantemos a detecção de corrupção, que importa sobretudo para chunks sem compressão.

Testes

  • Integração (fake S3 local): resposta com x-amz-checksum-crc32 que para depois de 1 KB.
    • Um teste verifica que a leitura rejeita no prazo.
    • Outro verifica que a primeira tentativa estanca e a segunda devolve os dados, com um onRetry.
    • Os dois travavam antes da correção.
  • Unitário: client falso com body que nunca termina.
    • Um teste verifica que a leitura é abortada, o body é destruído e há retentativa. Falha sem o readBody.
    • Outro verifica que um body que termina no prazo não é destruído depois.
  • npm test (417 ok), npm run lint, npm run format:check, tsc --noEmit.

Fora do escopo

  • HTTPStore: lê o corpo fora do executeWithRetry. Um corpo estancado ali falha no prazo (não trava), mas não é retentado. Merece uma issue separada.
  • nautilus-api: ligar os hooks de observabilidade no S3Store (sugestão 3 da issue).

For objects with a full-object checksum, the AWS SDK wraps the GetObject
body in a validating stream that swallows the abort: when the per-attempt
AbortSignal.timeout destroyed the socket mid-body, transformToByteArray()
never settled, no retry ran, and the read hung forever. The pending
promise also pinned the CachedStore in-flight entry and blocked
clearCache(), hanging dataset teardown and re-open() of that id.

Race the body read against the same per-attempt signal (destroying the
body on abort) so `timeout` bounds request and body; the TimeoutError is
retryable, so the normal retry policy runs. SDK checksum validation stays
on.

Closes #22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused implementation addresses the reported failure mode with appropriate retry behavior and comprehensive regression coverage.

0 open findings

What changed in this PR

Fixes stalled S3 body reads by enforcing one deadline across each request and its body consumption.

Changes:

  • Races body reads against the attempt’s abort signal and preserves retries.
  • Adds unit and integration coverage for stalled checksummed streams.
  • Updates timeout documentation and changelog.
File Description
src/​store/​s3.ts Enforces the deadline while reading S3 response bodies.
src/​store/​store.ts Clarifies timeout semantics.
tests/​unit/​s3-store.test.ts Tests body destruction and listener cleanup.
tests/​integration/​s3.test.ts Tests timeout and retry against a stalled checksummed response.
CHANGELOG.md Documents the resolved hanging-read issue.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@brandao-andre
brandao-andre merged commit 784d8b1 into main Oct 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S3Store: GET pendurado para sempre quando o corpo estanca (checksum do SDK engole o abort)

3 participants