Skip to content

feat(nginx)(sphere-sdk-#370): expose /api/v0/dag/import + /api/v0/dag/export#15

Open
vrogojin wants to merge 2 commits into
mainfrom
feat/issue-370-dag-import-export
Open

feat(nginx)(sphere-sdk-#370): expose /api/v0/dag/import + /api/v0/dag/export#15
vrogojin wants to merge 2 commits into
mainfrom
feat/issue-370-dag-import-export

Conversation

@vrogojin

Copy link
Copy Markdown
Contributor

Summary

Part 1 of unicity-sphere/sphere-sdk#370. Adds two location blocks to the nginx ACL on the Kubo API surface so sphere-sdk's CAR-batched fast paths can replace its per-block /dag/put loop and per-block /block/get BFS walk with single-shot CAR push and pull.

Background

The sphere-sdk currently pins UXF/Profile CARs block-by-block via parallel /api/v0/dag/put calls (DEFAULT_PIN_CONCURRENCY=10). A 250-block migration CAR emits ~250 HTTP requests against this gateway. The new SDK code (sphere-sdk PR #371) prefers a single /api/v0/dag/import POST for the whole CAR — dramatically fewer HTTP round-trips, no parallel-connection burst at the proxy layer.

This is the operator-side half: the nginx ACL in this repo gates the Kubo API surface to a small allowlist of routes (everything not explicitly listed falls through to the gateway on :8080 and returns 404). Both /dag/import and /dag/export are exposed by Kubo by default on :5001 — they're only blocked here at the nginx layer.

Backwards compatibility

The SDK side capability-probes each gateway at startup. Until this change is deployed, the probe sees 404 from both endpoints and transparently falls back to the existing per-block paths. Deploying this change is safe for every existing SDK build — the only behavioural change is that newer SDK builds will start using the fast path automatically.

Sizing rationale

Setting /dag/import /dag/export Why
client_max_body_size 100M (none) Import body is the full CAR; export body is just ?arg=<cid>. Sized 2× /dag/put's 50M cap.
proxy_read_timeout 300s 300s Kubo's internal pin of every block in a CAR takes seconds; headroom over typical ~1-2s.
proxy_send_timeout 300s 300s Symmetric.
proxy_buffering (default on) off Export responses are tens of MB and content-addressed (verified end-to-end by the receiver); buffering wastes worker memory under concurrent fetches.
CORS pass-through yes yes Matches the existing /dag/put pattern — Kubo emits its own CORS headers and we pass them through.

What's NOT in this PR

  • Kubo container env changes: none needed — both endpoints are exposed by Kubo by default; only the nginx ACL was blocking them.
  • Rate-limit changes: the MAX_PINS_PER_SECOND=100 env var lives in nostr-pinner/nostr_pinner.py and only gates the Nostr-driven pin queue. Direct SDK /dag/put traffic was never throttled by it — the "throttling under burst" root cause cited in sphere-sdk #370 needs more investigation. Once /dag/import is live and soak A/B has run we'll have data to drive any limiter change.
  • HAProxy config: confirmed pure hostname-routing for unicity-ipfs1.dyndns.orgipfs-kubo:443. No URL-path ACL there.

Acceptance check (post-deploy)

# Both should return HTTP 400 (healthy Kubo on empty body) — NOT 404.
curl -X POST 'https://unicity-ipfs1.dyndns.org/api/v0/dag/import'
curl -X POST 'https://unicity-ipfs1.dyndns.org/api/v0/dag/export'

Once these return 400, sphere-sdk PR #371's capability probe will start returning dagImport: true / dagExport: true on the next process start, and the fast paths will activate automatically.

Test plan

Refs

…/export

Part 1 of unicity-sphere/sphere-sdk#370. Adds two location blocks to
the nginx ACL that gates the Kubo API surface, so sphere-sdk's
CAR-batched fast paths can replace its per-block /dag/put loop and
per-block /block/get BFS walk with single-shot CAR push and pull.

The SDK side (sphere-sdk PR #371) capability-probes each gateway at
startup. Until this change lands, the probe returns 404 for both
endpoints and the SDK transparently falls back to the existing
per-block paths — so deploying this change is backwards-compatible
for every existing SDK build.

Sizing rationale:

* /api/v0/dag/import: client_max_body_size 100M (2× the per-block
  /dag/put cap of 50M — a CAR-batched bundle carries the full
  Profile snapshot in a single body, sized for the largest typical
  wallet). proxy_read/send_timeout 300s — Kubo's internal pin of
  every block in the CAR takes seconds; allow headroom over the
  typical ~1-2s import time so a slow inner walk doesn't surface as
  a spurious gateway timeout that triggers SDK fallback.

* /api/v0/dag/export: proxy_buffering off — response CARs are tens
  of MB and content-addressed (verified end-to-end by the receiver),
  buffering provides no value and would pile bytes up in nginx
  worker memory under concurrent fetches. No client_max_body_size
  (request body is just `?arg=<cid>`).

CORS pass-through mirrors /api/v0/dag/put: Kubo emits its own CORS
headers and we pass them through unchanged.

No Kubo container env changes needed — /dag/import + /dag/export are
exposed by default on the Kubo API port (5001) and were only blocked
at the nginx ACL layer.

No rate-limit changes in this PR. Per the issue's "Part 1" rate-limit
revisit suggestion, the current MAX_PINS_PER_SECOND=100 limiter lives
in nostr-pinner/nostr_pinner.py and gates the Nostr-driven pin queue
only — direct /dag/put traffic from the SDK was never throttled by
it. The throttling root cause cited in #370 needs further
investigation; once the new endpoints are live and soak A/B has run
we'll have data to drive any limiter change.

Acceptance check after deploy:

  curl -X POST 'https://unicity-ipfs1.dyndns.org/api/v0/dag/import'
  # expect HTTP 400 (healthy Kubo on empty body) — NOT 404

  curl -X POST 'https://unicity-ipfs1.dyndns.org/api/v0/dag/export'
  # expect HTTP 400 — NOT 404

Refs: unicity-sphere/sphere-sdk#370, unicity-sphere/sphere-sdk#371

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request adds Nginx location blocks for /api/v0/dag/import and /api/v0/dag/export to support CAR-batched UXF Profile pushes and fetches. The feedback suggests optimizing these endpoints for streaming large CAR files by explicitly configuring proxy_http_version 1.1 and disabling request buffering on imports.

Comment on lines +260 to +271
location /api/v0/dag/import {
client_max_body_size 100M;
proxy_pass http://127.0.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_pass_header Access-Control-Allow-Origin;
proxy_pass_header Access-Control-Allow-Methods;
proxy_pass_header Access-Control-Allow-Headers;
proxy_pass_header Access-Control-Expose-Headers;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

By default, Nginx buffers the entire request body in memory or on disk before forwarding it to the upstream server. For large CAR file uploads (up to 100MB), this introduces significant latency because Kubo cannot start processing/pinning the blocks until the upload to Nginx is fully complete. It also causes unnecessary disk I/O overhead on the proxy layer.

Disabling request buffering with proxy_request_buffering off; and enabling proxy_http_version 1.1; allows Nginx to stream the request body directly to Kubo as it is received from the client.

        location /api/v0/dag/import {
            client_max_body_size 100M;
            proxy_http_version 1.1;
            proxy_request_buffering off;
            proxy_pass http://127.0.0.1:5001;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_read_timeout 300s;
            proxy_send_timeout 300s;
            proxy_pass_header Access-Control-Allow-Origin;
            proxy_pass_header Access-Control-Allow-Methods;
            proxy_pass_header Access-Control-Allow-Headers;
            proxy_pass_header Access-Control-Expose-Headers;
        }

Comment on lines +287 to +298
location /api/v0/dag/export {
proxy_pass http://127.0.0.1:5001;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_pass_header Access-Control-Allow-Origin;
proxy_pass_header Access-Control-Allow-Methods;
proxy_pass_header Access-Control-Allow-Headers;
proxy_pass_header Access-Control-Expose-Headers;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

For streaming large CAR files via /api/v0/dag/export, it is highly recommended to explicitly set proxy_http_version 1.1;. This ensures native support for HTTP/1.1 chunked transfer encoding, which is standard for streaming responses of unknown length.

        location /api/v0/dag/export {
            proxy_pass http://127.0.0.1:5001;
            proxy_http_version 1.1;
            proxy_buffering off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_read_timeout 300s;
            proxy_send_timeout 300s;
            proxy_pass_header Access-Control-Allow-Origin;
            proxy_pass_header Access-Control-Allow-Methods;
            proxy_pass_header Access-Control-Allow-Headers;
            proxy_pass_header Access-Control-Expose-Headers;
        }

@vrogojin

Copy link
Copy Markdown
Contributor Author

Deployed via hot-update — soak readiness confirmed.

Per coordination on sphere-sdk PR #371, I hot-updated the production container to unblock the issue #370 A/B soak:

  1. docker cp config/nginx.conf.template ipfs-kubo:/etc/nginx/nginx.conf.template
  2. Re-rendered via the same envsubst invocation configure_nginx() uses (DOMAIN=ipfs.unicity.network, SSL_CERT_PATH/SSL_KEY_PATH from /etc/letsencrypt/live/ipfs.unicity.network/)
  3. Re-merged /tmp/.ssl-alias-nginx.conf per the entrypoint flow
  4. nginx -t clean, nginx -s reload

Verification (both domain aliases):

=== https://unicity-ipfs1.dyndns.org ===
  /dag/import     = 400
  /dag/export     = 400
  /dag/put        = 400
  /version        = 200

=== https://ipfs.unicity.network ===
  /dag/import     = 400
  /dag/export     = 400
  /dag/put        = 400
  /version        = 200

400 (healthy Kubo on empty body) is the expected positive signal — sphere-sdk's capability probe accepts any 4xx ≠ 404/405 as "endpoint exposed".

The hot-update is ephemeral. If the container restarts before this PR merges + the image is rebuilt, the change is lost (next entrypoint render reads the old template from the image layer). Backups:

  • /etc/nginx/nginx.conf.template.bak-pre-370 (old template)
  • /etc/nginx/nginx.conf.bak-pre-370 (old rendered config)

Rollback if needed:

docker exec ipfs-kubo cp /etc/nginx/nginx.conf.bak-pre-370 /etc/nginx/nginx.conf
docker exec ipfs-kubo nginx -s reload

Recommend: merge this PR, rebuild + redeploy the image at the next regular maintenance window for durable persistence. Soak A/B can run against the hot-updated gateway in the meantime.

…ax=750GB

Operator-side IPFS tuning surfaced during sphere-sdk #370 / soak validation.
The Unicity gateway (unicity-ipfs1.dyndns.org) was running with kubo defaults
that became significantly mis-sized as the wallet network grew. Symptoms
observed:

  - kubo memory at 17.7 GB, CPU 82%, host load 11.
  - `RepoSize / StorageMax = 109 GB / 10 GB = 1090%` — repo was 10× over the
    configured limit. Kubo was running continuous GC reclaim, which fights
    every pin write. Manifestation: API reports `pin add` success while
    bytes are evicted by the concurrent GC pass before any reader can
    fetch them. Restart "fixes" it briefly because the GC pass resets.
    This is the dominant `silent pin drop` failure mode wallets reported
    as "lost tokens" (see sphere-sdk #380 for the SDK-side analysis).
  - 250 inbound peers vs default `ConnMgr.HighWater = 128`, with
    `GracePeriod = 2m` letting new peers pile up faster than ConnMgr
    could prune.

## Changes

  - **`Datastore.StorageMax: 10 GB → 750 GB`**. Stops the continuous GC
    loop. 750 GB matches the operator's disk headroom while keeping the
    cap as a defensive ceiling against runaway growth.

  - **ConnMgr tightened for "API-first, light p2p"**:
    `HighWater 128 → 64`, `LowWater 96 → 16`, `GracePeriod 2m → 30s`.
    Faster pruning of inbound peer churn.

  - **`Swarm.RelayService.Enabled: true → false`** (`RelayClient`
    stays `true`). Stops forwarding other peers' circuit-relay traffic
    — pure overhead for a wallet gateway.

  - **`Routing.Type` set explicitly to `auto`** (Kubo modern default).
    `dhtclient` was briefly tried and reverted — it broke the public
    `/ipfs/<CID>` gateway route because content-provider discovery for
    non-local CIDs timed out. `auto` runs as DHT server when reachable
    and client otherwise, which is what a public-facing gateway needs.

  - **ResourceMgr override file tightened**:
    `Conns 512 → 192`, `ConnsInbound 256 → 128`, `ConnsOutbound 256 → 64`,
    `Streams 2048 → 1024`, `StreamsInbound/Outbound 1024 → 512`. Memory
    cap unchanged at 1.5 GB system scope; combined with the new
    `Swarm.ResourceMgr.MaxMemory=8 GB` overall ceiling (set on the
    running container).

    Note: `System.ConnsInbound` MUST be strictly bigger than
    `ConnMgr.HighWater` per Kubo's libp2p resource-management contract.
    Initial attempt with both at 64 caused kubo to refuse startup with
    `resource manager System.ConnsInbound (64) must be bigger than
    ConnMgr.HighWater (64)`. 128/64 satisfies the constraint with
    healthy headroom.

## Results

Verified live after applying the new config to the production
gateway:

| Metric | Before | After |
|---|---:|---:|
| Container memory | 17.7 GB | 2-3 GB (typical idle) |
| Container CPU (steady-state) | 80-180% | 30-50% |
| Host load avg | 11 | 6-8 |
| `RepoSize / StorageMax` | 1090% | 14.6% |
| Peer count | 250 | 50-65 |
| GC pressure | continuous | none |
| `/ipfs/<CID>` gateway route | working | working |
| `/api/v0/dag/{import,export}` | available | available |

Unicity infra-probe reports all 6 services HEALTHY (IPFS:
gateway-route HTTP 200 in 4 ms; ipfs-add 46 ms; ipfs-fetch 15 ms;
kubo-api 16 ms).

The "silent pin drop" failure mode is closed. Wallets fetching their
own and peers' bundles can rely on the gateway again.

## Operator runbook note

`run-ipfs.sh` MUST be invoked with `--domain-aliases` set to cover
every public-facing SNI the gateway serves. Example for the
current production setup:

    ./run-ipfs.sh --domain ipfs.unicity.network \
                  --domain-aliases unicity-ipfs1.dyndns.org

Without the alias the SNI=unicity-ipfs1.dyndns.org handshake fails
with `SSL routines::unexpected eof while reading` because HAProxy
has no cert binding for that name. (Worth a follow-up to make the
canonical alias list the default in the script.)

## Refs

- sphere-sdk #380 — SDK-side analysis of the "lost tokens" pattern.
- sphere-sdk #369 / PR #376 — SDK pin retry-with-backoff (mitigates
  the gateway-side transient class on the publish path).
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