feat(nginx)(sphere-sdk-#370): expose /api/v0/dag/import + /api/v0/dag/export#15
feat(nginx)(sphere-sdk-#370): expose /api/v0/dag/import + /api/v0/dag/export#15vrogojin wants to merge 2 commits into
Conversation
…/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
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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;
}
| 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; | ||
| } |
There was a problem hiding this comment.
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;
}
|
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:
Verification (both domain aliases): 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:
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 reloadRecommend: 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).
Summary
Part 1 of unicity-sphere/sphere-sdk#370. Adds two
locationblocks to the nginx ACL on the Kubo API surface so sphere-sdk's CAR-batched fast paths can replace its per-block/dag/putloop and per-block/block/getBFS 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/putcalls (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/importPOST 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
:8080and returns 404). Both/dag/importand/dag/exportare 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
404from 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
/dag/import/dag/exportclient_max_body_size100M?arg=<cid>. Sized 2×/dag/put's 50M cap.proxy_read_timeout300s300sproxy_send_timeout300s300sproxy_bufferingoff/dag/putpattern — Kubo emits its own CORS headers and we pass them through.What's NOT in this PR
MAX_PINS_PER_SECOND=100env var lives innostr-pinner/nostr_pinner.pyand only gates the Nostr-driven pin queue. Direct SDK/dag/puttraffic was never throttled by it — the "throttling under burst" root cause cited in sphere-sdk #370 needs more investigation. Once/dag/importis live and soak A/B has run we'll have data to drive any limiter change.unicity-ipfs1.dyndns.org→ipfs-kubo:443. No URL-path ACL there.Acceptance check (post-deploy)
Once these return
400, sphere-sdk PR #371's capability probe will start returningdagImport: true/dagExport: trueon the next process start, and the fast paths will activate automatically.Test plan
locationblocks, no changes to existing routes/dag/putblock + Kubo HTTP API docs (https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-dag-import, https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-dag-export)unicity-ipfs1.dyndns.org, verify the curl acceptance checks passdocs/uxf/SOAK-RUNBOOK-370.mdRefs