Skip to content

Task/10283 load tracks and videos from s3 by selecting a time range - #841

Merged
randy-seng merged 23 commits into
mainfrom
task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range
Sep 29, 2026
Merged

randy-seng merged 23 commits into
mainfrom
task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range

Conversation

@randy-seng

@randy-seng randy-seng commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

What this does

In S3 mode, loading tracks and videos used to have no user-facing entry point — this branch gives it one. The user picks a UTC start/end time range in a new LoadWindowDialog, S3ListObjects lists the bucket under the configured prefix (de-duplicating repeated keys — RustFS, unlike AWS S3, can report the same object
more than once), select_in_window filters to objects whose filename timestamp falls in the range, and DownloadObjects fetches the selected keys concurrently into a local staging directory (user_source) before the existing local-file loading path takes over. For tracks, S3TrackFileProvider additionally reads each
downloaded .ottrk's own metadata to resolve and fetch its companion video — the video is not inferred from the track's filename, since a companion may be any supported video type.

The load is all-or-nothing: a missing video, an unreadable track file, or a user cancel abandons the whole batch rather than leaving a partial load in the repositories. Cancellation is handled by racing a watch_for_cancel task against the download tasks inside one asyncio.TaskGroup, so a cancel reaches downloads already in flight instead of only ones that haven't started yet (previously, checking only between asyncio.gather slots meant a cancel on a multi-gigabyte transfer could go unnoticed for a long time).

Staged downloads are wiped on startup, on shutdown (best-effort), and on project reset — they're explicitly not a cache, since nothing reuses them across runs and the Video objects pointing at them don't survive a restart either.

Why

RunningProgressbar was renamed to CompletionProgress and gained complete(item) and is_cancelled because the existing progressbar interface assumed work advances by iterating a sequence in order. Concurrent downloads complete out of order, so the abstraction had to change to "report each item as it finishes and poll for cancellation" instead.

AskForLoadWindow is a new port in the application layer rather than a method on UiFactory — adding an abstract method there would make OTCloud's UnimplementedUiFactory abstract and break it on instantiation. It also had to live outside plugin_s3 so the front-end implementing it doesn't need to depend on the plugin that consumes it.

LoadWindow is deliberately a separate type from DateRange (which filters tracks already loaded) — this window decides what gets fetched in the first place, and is clamped rather than rejected when a selection exceeds the configured maximum duration (continuous recording produces far more data than can be held at once).

Matching is strict start-in-range: a chunk that started before the window but runs into it is not loaded. This keeps selection predictable at the cost of a leading partial chunk, and is called out explicitly in select_in_window's docstring since it's an easy thing to assume works differently.

MinIO was replaced with RustFS in test fixtures in an earlier commit on this branch (58c1279a) because MinIO stopped publishing free Docker Hub images and later removed the repository entirely, breaking the testcontainers-based acceptance/integration suite.

Notes for the reviewer

Start with OTAnalytics/plugin_s3/download_objects.py — the TaskGroup/cancellation logic is the trickiest part of this change and is exercised directly by tests/unit/OTAnalytics/plugin_s3/test_download_objects.py (test_cancelling_stops_the_downloads_still_in_flight,test_cancelling_mid_download_aborts_it_promptly).

OTAnalytics/plugin_s3/s3_file_providers.py is the other file worth close attention: it's where track/video resolution, error translation (MissingVideoForTrackFile vs UnreadableTrackFile vs a generic bucket/network/filesystem failure), and user-facing messaging all meet. The rest —
filename_timestamp.py, select_objects.py, cleanup.py, the nicegui_application.py wiring — is comparatively mechanical. domain/progress.py's rename (RunningProgressbar → CompletionProgress) touches load_track_files.py and nicegui_gui/nicegui/progressbar.py as call sites; nothing behavioral changed there beyond the new complete/is_cancelled members.

Risk and rollout

Only active when DATA_TRANSFER_MODE=s3 (transfer_mode_from_env()); local mode is untouched. The download concurrency is configurable (S3Config.download_concurrency) — worth confirming the default is sane for the target deployment's bucket and network.

select_in_window's strict start-in-range matching means a recording chunk started just before a window boundary is silently excluded, not partially loaded — expected behavior per the docstring, but worth flagging to whoever signs off on acceptance criteria.

Adds minio and testcontainers as dev dependencies (for spinning up RustFS in tests) and expands pytest testpaths to include tests/integration.

Testing

Acceptance (tests/acceptance/test_load_from_s3.py) and integration (tests/integration/OTAnalytics/plugin_s3/test_load_from_rustfs.py) tests spin up a real RustFS container via testcontainers and exercise the dialog end-to-end against
a live S3 API.

OP#10283

@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch 3 times, most recently from ea11596 to 1f65bd2 Compare September 10, 2026 12:18
@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch from c8e3561 to 41bb10b Compare September 14, 2026 14:02
@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch 2 times, most recently from 1784aa6 to 41bb10b Compare September 14, 2026 22:31
@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch from 41bb10b to 805be28 Compare September 16, 2026 15:46
@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch 2 times, most recently from ef4479e to fed67a9 Compare September 21, 2026 12:46
@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch from fed67a9 to 58c1279 Compare September 24, 2026 09:44
@randy-seng
randy-seng requested a review from schubsen September 25, 2026 15:12
@randy-seng randy-seng self-assigned this Sep 25, 2026
@briemla
briemla requested review from briemla and removed request for schubsen September 28, 2026 18:21
Base automatically changed from task/10282-keep-the-web-ui-responsive-while-loading-track-files to main September 29, 2026 09:08
randy-seng and others added 16 commits September 29, 2026 11:08
S3Connection was folded into S3Store on OP#10256, so the composition root
and the MinIO integration test now build a store from the config instead of
passing a connection and a config separately.

The nicegui application test also has to set the S3 endpoint URL, which
became a required environment variable on OP#10256 after these tests were
written.
The message goes straight to the user, and it claimed the file did not say
which video it belonged to whichever way reading it had failed. A file that
never arrived was reported as one whose metadata was lacking, which sends
whoever reads it looking inside a file that is not there. That is how the
recent CI failure came to be read as a format problem rather than a missing
one.

Missing, unopenable, unparseable and lacking video metadata now each say so.
The parser's own complaint is left out of the unparseable case: ijson quotes
the offending line and points a caret into it, which is a diagram nobody can
read in a popup, and the traceback still carries it to the log.

Also ignore the directory S3_USER_SOURCE stages downloads in, so a stray
`git add -A` cannot commit a bucket's worth of tracks and videos.
MinIO stopped publishing free images to Docker Hub and later removed the
minio/minio repository entirely, breaking the acceptance and integration
tests that spin one up via testcontainers. testcontainers-python has no
dedicated RustFS module, so RustFsContainer wraps the official
rustfs/rustfs image as a generic container the same way
testcontainers.minio.MinioContainer wraps MinIO's; the minio Python
client still works unchanged since RustFS speaks the same S3 API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HKAcf8GCJDLgziwYz3uyC2
S3-compatible services such as rustfs report the same object several
times in one listing. The repeats were downloaded and loaded again,
and publishing a video's metadata twice is rejected.
watch_for_cancel previously only polled while asyncio.gather awaited
the downloads, so a cancel could go unnoticed until a slot freed
between downloads. Running the downloads and the watcher in the same
TaskGroup lets the watcher's cancellation reach downloads still in
flight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@randy-seng
randy-seng force-pushed the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch 2 times, most recently from 37d6b65 to d5775a6 Compare September 29, 2026 09:08
@randy-seng
randy-seng merged commit 7078929 into main Sep 29, 2026
26 checks passed
@randy-seng
randy-seng deleted the task/10283-load-tracks-and-videos-from-s3-by-selecting-a-time-range branch September 29, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants