Skip to content

Relay GeoServer/GeoNode uploads straight through, never stored - #159

Open
zamuzakki wants to merge 7 commits into
mainfrom
feat/upload-passthrough
Open

zamuzakki wants to merge 7 commits into
mainfrom
feat/upload-passthrough

Conversation

@zamuzakki

Copy link
Copy Markdown
Contributor

This is PR for #155

Problem

Uploads to GeoServer and GeoNode were stored on CloudBench first: the chunks, then the assembled file, then the whole file read into memory to send it. CloudBench has only a small volume (a 10Gi PVC on GeoHosting), so one big upload could fill it. CloudBench connects to any GeoServer/GeoNode, so it can't hand the file over through shared storage either.

Change

Uploads are relayed as they arrive, never stored. The browser still sends 5 MB chunks. CloudBench passes them on in one streaming request to the target, with a Content-Length, and nothing touches its disk.

  • Across workers. Chunks can reach any of the 4 gunicorn workers (now gthread). The worker holding the request to the target listens on a Unix socket. The others pass their chunks to it through that socket. No Redis or other new service is needed.
  • New API:
    • POST /api/upload/relay starts an upload;
    • PUT /api/upload/relay/<id>/chunks/<n> sends a chunk;
    • GET / DELETE /api/upload/relay/<id> returns the status, or cancels.
  • Short drops resume. The browser retries a chunk after 1, 2, 4, 8 and 16 s. A drop longer than about 60 s fails cleanly, and the dialog says to start over: GeoServer and GeoNode can't continue a partial upload. Pause is removed.
  • Checked before any data is sent: the connection, the workspace, the file type and store name, and the GeoNode size limit (/api/v2/upload-size-limits/).
    • New: a GeoServer store name already used by a store of another type is refused. Before this, GeoServer took a GeoPackage into a shapefile store, replacing its files but not its type, so its layer broke while the upload said "completed". A file of the store's own type still replaces its data.
  • GeoNode zips. CloudBench asks each GeoNode how it takes a zipped dataset, using a 1-byte probe that creates nothing. 4.x needs zip_file with a stand-in base_file; newer GeoNodes take the zip as base_file.
  • Cancel. Closing an upload dialog (its Cancel or ×) now cancels the upload. Before, it only hid the dialog while the upload carried on.
  • Removed:
    • the old store-then-send endpoints and their Celery task;
    • useChunkedUpload.
  • Settings:
    • GUNICORN_THREADS (default 8);
    • GUNICORN_MAX_REQUESTS (default 100000);
    • UPLOAD_RELAY_IDLE_TIMEOUT (60 s);
    • UPLOAD_RELAY_RESPONSE_TIMEOUT (1800 s).

Design, rejected options and known limits: docs/dev-guide/upload-passthrough.md.

Testing

  • Unit and API tests for:
    • the relay (order, retries, idle timeout, cancel, backpressure);
    • the GeoServer/GeoNode senders;
    • the up-front checks;
    • the dialogs cancelling on close.
  • Full suite: 1588 Python tests and 489 vitest tests pass. ruff, black and tsc are clean.
  • deployment/upload-test/ is a production-like stack: production settings, 4 gthread workers, and an nginx set up like GeoHosting's ingress. Through it, against a GeoHosting staging GeoNode 4.4 and its GeoServer:
    • GeoServer: GeoTIFF (2 chunks), zipped shapefile (re-uploaded too) and GeoPackage are created and render;
    • a 20 s gap resumes; a 75 s gap fails cleanly with no store left; cancel leaves no store;
    • a bad store name, a missing workspace and a store of another type are refused up front;
    • GeoNode: a zipped shapefile (detected as zip_file), a GeoTIFF and a document are created;
    • CloudBench's data folder stays the same size, byte for byte, throughout.
  • Manual test guide: TEST_GUIDE_upload_passthrough.md.

Known limits

  • Newer GeoNodes (5.x/master): zipped datasets sent as base_file are untested.
  • GeoNode dataset titles (already on main): GeoNode 4.4 ignores a title sent with the upload, so a dataset is titled after its file. Documents keep their title.

Uploading to GeoServer or GeoNode stored the file on CloudBench first:
every chunk on disk, then a second, assembled copy (2x the file on a
10Gi volume), then the whole file read into memory to send it.

Now the worker that starts an upload opens one streaming request to the
target (with Content-Length) and feeds it each chunk as the browser
sends it, holding at most 3 in memory; chunks landing in other gunicorn
workers reach it over a Unix socket (apps/upload/relay.py). Nothing is
written to disk and no Redis is needed.

- A chunk sent again after a dropped connection is acknowledged, not
  sent twice; the browser retries for ~30s. Past the 60s idle timeout,
  or if the target drops, the upload starts over (targets can't
  resume). Pause is gone.
- Checked before anything is sent: connection, workspace (GeoServer),
  GeoNode's upload size limit, file type, store name.
- GeoNode zips go once, as zip_file, with a 1-byte stand-in base_file
  (GeoNode 4.x unzips zip_file only).
- New endpoints /api/upload/relay...; removed the old GeoServer/GeoNode
  complete/start/status flow, including upload/start, which read and
  then deleted any path the caller gave. Also removed the unused
  whole-file endpoints /api/upload/complete and /api/upload.
- gunicorn: gthread workers (GUNICORN_THREADS); max_requests 100000
  (was 1000), as recycling a worker cuts off the uploads it relays.

Design, options considered and test results:
docs/dev-guide/upload-passthrough.md
GeoNode 4.x unzips only a zip sent as zip_file (with a stand-in
base_file); newer GeoNodes dropped zip_file and take the zip as
base_file. CloudBench connects to any GeoNode, which publishes no
version, so before a zipped dataset upload it asks: a 1-byte "zip" as
base_file is refused by 4.x as "No handlers found" (handler lookup
comes first) and by newer GeoNodes as an invalid zip (zip check comes
first). Nothing is created either way; 4.x is assumed when it can't
tell. Checked on GeoHosting staging (4.x).
Closing an upload dialog (its Cancel or x) only hid it: the upload carried
on. It now cancels the upload, and each upload has its own cancel flag so
a cancelled one can't update a reopened dialog. An upload cancelled before
its session was known ends that session too.

GeoServer took a file into a store of another type (a GeoPackage into a
shapefile store), replacing its files but not its type, which broke its
layers while the upload said completed. Such an upload is now refused
before anything is sent; a file of the store's own type still replaces
its data.
deployment/upload-test runs this branch as GeoHosting runs CloudBench:
production settings, 4 gthread workers, the data folder on its own
volume, behind an nginx set up like its ingress (2G body, buffering,
60 s timeouts). Built on the local production image, nothing downloaded.
@zamuzakki
zamuzakki requested a review from meomancer October 13, 2026 06:22
@zamuzakki
zamuzakki force-pushed the feat/upload-passthrough branch from 71c6cbf to c20f487 Compare October 15, 2026 02:38
@zamuzakki
zamuzakki force-pushed the feat/upload-passthrough branch from c20f487 to 7ca8edd Compare October 22, 2026 13:01
GeoNode's multi-layer GeoPackage upload from main (#170) needs the whole
file on disk, so GeoPackage datasets stay staged; their views now check
the session is the user's and stream each layer from disk. Everything
else is still relayed, and relayed datasets get their title/abstract
set once GeoNode has imported them (#167).

This branch has not been deployed

No deployments
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