Skip to content

fix(import): finalise imports over 2 GiB - #364

Merged
Valtora merged 1 commit into
mainfrom
fix/large-import-file-size
Oct 10, 2026
Merged

Valtora merged 1 commit into
mainfrom
fix/large-import-file-size

Conversation

@Valtora

@Valtora Valtora commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Pull Request

Description

Found during the 2.6.0 soak run, straight after #362. With #362 deployed, the 3.3 GB House of Lords MP4 uploaded in 27 seconds with no event-loop stalls, and then finalize failed:

asyncpg.exceptions.DataError: invalid input for query argument $3: 3292350489 (value out of int32 range)
[SQL: UPDATE recordings SET ... file_size_bytes=$3::INTEGER ...]

Two bugs, both older than 2.6.0:

  1. recordings.file_size_bytes is a 32-bit INTEGER. Finalising any import over 2 GiB (2,147,483,647 bytes) fails, and feat(import): accept audio from common video and media containers #333 makes that the normal case for screen recordings. The suite runs on SQLite, which has no such limit, so nothing caught it. documents.file_size_bytes is already BigInteger.
  2. The finalize error path crashes. After the failed flush, the except Exception block used the session without rolling it back. recording.id raised PendingRollbackError, the cleanup raised again, and the client got a plain-text 500. The recording stayed UPLOADING, with 3.3 GB of parts and a 3.3 GB assembled file left on disk.

Changes:

  • Alembic revision b182c2378829 widens the column to BIGINT, and the model follows document.py (sa_column=Column(BigInteger)). The downgrade sets a size that no longer fits to NULL instead of aborting, since the column is informational and nothing reads it to find the file.
  • Finalize reads the recording's id and audio path before anything can fail, and rolls back first in the error path. The cleanup then runs as written: it moves the parts to the failed directory, marks the chunks failed and deletes the assembled file. The recording now ends ERROR with "Failed to finalize the uploaded recording." as its step, as the HTTPException branch already did, instead of an UPLOADING row with no files.

Upgrade and rollback. This is 2.6.0's only schema change. It runs on the first API start, and on a small recordings table the rewrite takes moments. Rolling the images back to 2.5.1 now needs the schema rolled back first, because the startup guard refuses a database stamped with a revision the image does not know.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behaviour)
  • Documentation update

Checks run

  • Backend tests: NOJOIN_TEST_POSTGRES_URL=... python scripts/check.py: 2,177 passed, none skipped, with every Postgres-gated test run against a throwaway pgvector/pg18 server
  • Python quality: python scripts/check.py (lint, format, whitespace, filesize, heldpins, typecheck, docs, alembic, tests all OK)
  • Frontend lint: no frontend change
  • Frontend unit tests: no frontend change
  • Frontend build: no frontend change
  • Docs validation: in scripts/check.py
  • Alembic validation: python3 scripts/validate_alembic.py, head b182c2378829

Also run against the throwaway Postgres:

  • scripts/check_migrations.py passes all three phases: fresh install, seeded round trip through the new revision, and at head.
  • By hand: a row with file_size_bytes = 3292350489, then alembic downgrade -1. The column is integer and the size NULL. alembic upgrade head restores bigint.

New tests in test_import_audio_task.py:

  • test_an_import_over_2_gib_keeps_its_size_on_postgres reports the assembled file as 3,292,350,489 bytes through a chunked import. On main it fails with the same value out of int32 range as the live stack.
  • test_a_finalize_that_fails_in_the_database_marks_the_recording_failed runs on SQLite and Postgres. It forces a flush failure inside finalize, then checks for a JSON 500, a recording in ERROR with the message, no assembled file, and nothing queued.

Migration impact

  • No database migration in this PR.
  • Adds an Alembic migration. Single checked-in head preserved; no committed revisions deleted or renamed. Upgrade and downgrade behaviour described above.

Documentation impact

  • No documentation change required.
  • Updated the relevant guide(s) in the same PR.

The upgrade and rollback steps go in the 2.6.0 release notes (#360).

Security impact

  • No security-sensitive change.
  • Touches auth, tokens, encryption, capture ownership, or exposure.

Manual verification

Before: the failed finalize on the live stack, quoted above. The stranded recording was discarded through the API, which removed its row and its 6.6 GB.

Pending: the full 3.3 GB import on the live stack rebuilt with this merged, through extraction (#333) and processing, before 2.6.0 is tagged.

recordings.file_size_bytes was a 32-bit INTEGER, so finalising any
import over 2 GiB failed with "value out of int32 range". The error path
then used the session without rolling it back, raised again, and left
the recording UPLOADING with its files gone.

The column becomes BIGINT, as documents.file_size_bytes already is; a
downgrade clears sizes that no longer fit. A failed finalize now rolls
back, cleans up as intended, and marks the recording ERROR.
@Valtora
Valtora merged commit 8a0be06 into main Oct 10, 2026
20 checks passed
@Valtora
Valtora deleted the fix/large-import-file-size branch October 10, 2026 22:38
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.

1 participant