Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
dfd4100
feat: support partial (multi-request) scene uploads
LautaroPetaccio Jul 7, 2026
e8b2ea6
test: cover parallel partial-upload staging requests
LautaroPetaccio Jul 7, 2026
9057b33
fix: address partial-upload review findings
LautaroPetaccio Jul 7, 2026
4a16f90
perf: skip the permission check on resume batches of a partial upload
LautaroPetaccio Jul 8, 2026
4108a27
test: pin that losing the name permission mid-upload rejects the fina…
LautaroPetaccio Jul 8, 2026
3667eed
fix: address second-review findings on the partial-upload path
LautaroPetaccio Jul 8, 2026
6cce488
fix: guard GC pending-key projection and store all uploaded files
LautaroPetaccio Jul 9, 2026
91b2ee5
feat: order-aware pending replacement and a per-deployer staging cap
LautaroPetaccio Jul 10, 2026
597d5e3
fix: make deployment ordering and the per-deployer cap atomic
LautaroPetaccio Jul 13, 2026
4cf315e
fix: memory, GC, and cap-accounting hardening for partial uploads
LautaroPetaccio Jul 13, 2026
ab00850
feat: finalization lease so only one request finalizes a completed up…
LautaroPetaccio Jul 13, 2026
0b644ce
refactor: split partial-upload component types into per-component typ…
LautaroPetaccio Jul 13, 2026
5fcc33d
fix: address partial-deployment review findings
LautaroPetaccio Jul 14, 2026
cda077e
fix: harden partial-deployment finalize, GC, and staging hot path
LautaroPetaccio Jul 15, 2026
5a6eb94
Merge branch 'main' into feat/partial-deployments
LautaroPetaccio Jul 22, 2026
02c2f09
fix: correct persisted size and cancellation on the partial finalize …
LautaroPetaccio Jul 22, 2026
70602f7
fix: harden the partial-resume gate, GC re-check index, and duplicate…
LautaroPetaccio Jul 23, 2026
ed1bdb2
fix: harden owner reconciliation, degrade paths, and API-contract acc…
LautaroPetaccio Jul 23, 2026
eda87fc
fix: complete the whitelist casing fix and close partial-deploy edge …
LautaroPetaccio Jul 23, 2026
b84df4d
Merge branch 'main' into feat/partial-deployments
LautaroPetaccio Sep 18, 2026
b1e77de
feat: key partial uploads by entity id and let overlapping uploads co…
LautaroPetaccio Sep 23, 2026
a68f049
fix: close partial upload races and lock pool starvation
LautaroPetaccio Sep 24, 2026
436725a
fix: reject batches from another signer on a live upload
LautaroPetaccio Sep 24, 2026
bc09efa
fix: keep unadmitted batches and gc waits off upload quotas and locks
LautaroPetaccio Sep 24, 2026
d572027
fix: queue gc writers and retry a saturated lock pool
LautaroPetaccio Sep 24, 2026
d7a32b0
fix: build the gc index concurrently and bound gc writer waits
LautaroPetaccio Sep 24, 2026
818929e
fix: retry the lock pool when opening a connection times out
LautaroPetaccio Sep 24, 2026
9d0c036
fix: reuse lock connections after a failed operation
LautaroPetaccio Sep 25, 2026
70f26f8
fix: keep vanilla deploys independent of pending partial uploads
LautaroPetaccio Sep 25, 2026
e8ad942
fix: apply migrations before the server starts serving
LautaroPetaccio Sep 25, 2026
dcd5fc0
fix: tie completion receipts to the entity's latest publication
LautaroPetaccio Sep 25, 2026
d751298
fix: serialize migrations across instances with an advisory lock
LautaroPetaccio Sep 25, 2026
e99706a
feat: reject a partial query flag without the partial form field
LautaroPetaccio Sep 25, 2026
dd59b7b
fix: run migrations on the session that holds the migrations lock
LautaroPetaccio Sep 27, 2026
a6ada4d
fix: keep partial uploads within their fixed lifetime
LautaroPetaccio Sep 27, 2026
91040cd
fix: bound each source's in-flight uploads before reading the body
LautaroPetaccio Sep 28, 2026
a2d7b49
fix: start partial upload lifetimes at their first request's arrival
LautaroPetaccio Sep 28, 2026
306afac
fix: try the exclusive content lock instead of queuing on it
LautaroPetaccio Sep 28, 2026
3280dac
test: scope the deployments counter assertions to that metric
LautaroPetaccio Sep 28, 2026
1f53ca8
feat: make the trusted client ip header configurable
LautaroPetaccio Sep 29, 2026
c2f2e2e
fix: say exactly why a request timed out in 408 responses
LautaroPetaccio Sep 29, 2026
ae2f218
fix: answer full partial-upload quotas with 429 and retry-after
LautaroPetaccio Sep 29, 2026
af09cd4
feat: derive upload concurrency and file limits from the byte budget
LautaroPetaccio Sep 29, 2026
ffa10d9
fix: answer every partial batch for a published entity with 200
LautaroPetaccio Sep 29, 2026
39b38c8
feat: expire partial uploads after 1 hour and clean them every 5 minutes
LautaroPetaccio Sep 29, 2026
014177b
feat: add partial upload lifecycle, quota and cleanup metrics
LautaroPetaccio Sep 30, 2026
64c7125
fix: reject repeated form field names in multipart uploads
LautaroPetaccio Sep 30, 2026
a958049
fix: never create or charge a partial upload past its deadline
LautaroPetaccio Sep 30, 2026
e53f0bc
test: cover categories sent as repeated settings fields
LautaroPetaccio Sep 30, 2026
da53fda
fix: answer 400 for partial uploads that can never fit a quota
LautaroPetaccio Oct 4, 2026
e985749
docs: correct partial upload status codes, rollout and ordering
LautaroPetaccio Oct 4, 2026
dfcdfd7
fix: stop waiting for the content lock when a settings client disconn…
LautaroPetaccio Oct 4, 2026
daf4f8a
chore: drop the unreleased world_scenes updated_at index migration
LautaroPetaccio Oct 4, 2026
c1289f8
chore: tidy partial upload code and test responses
LautaroPetaccio Oct 4, 2026
9b93035
fix: cap post /entities form fields to what a deployment sends
LautaroPetaccio Oct 8, 2026
eb98372
feat: charge partial uploads only for the bytes they store
LautaroPetaccio Oct 8, 2026
daad8d7
fix: validate deployments before taking the content lock
LautaroPetaccio Oct 8, 2026
7131879
fix: answer multipart size and count limits with 413
LautaroPetaccio Oct 8, 2026
67e9015
feat: cap world scene size at max_scene_size
LautaroPetaccio Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.default
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ MAX_CONCURRENT_UPLOADS=40
MAX_IN_FLIGHT_UPLOAD_FILES=40000
MAX_ORPHANED_UPLOAD_DIRECTORIES=40
MULTIPART_UPLOAD_TIMEOUT_MS=300000
# One client source's (cf-connecting-ip) share of the budget above, per process, taken before the body is read.
# Requests without cf-connecting-ip are not limited per source (counted in multipart_upload_unattributed).
# MAX_IN_FLIGHT_UPLOAD_BYTES_PER_SOURCE defaults to one maximum-size upload (350 MiB) and cannot be lower.
MAX_CONCURRENT_UPLOADS_PER_SOURCE=4
# Maximum number of content files a single deployment may declare (defaults to 10000)
MAX_FILE_COUNT=10000
# Per-request concurrency for deployment processing. These bound storage I/O and CID computation.
Expand Down Expand Up @@ -93,3 +97,17 @@ SHARED_SECRET_MAX_ATTEMPTS_PER_MINUTE=3
######################################
# How long to keep UNDEPLOYED scenes before permanent deletion (ms). Default: 7 days
SCENE_EVICTION_TTL_MS=604800000
# How long a partial (multi-request) deployment may stay pending before it is reclaimed (ms). Default 24h.
PENDING_DEPLOYMENT_TTL=86400000
# Max pending uploads per deployer, including expired uploads awaiting cleanup. Default 10.
MAX_PENDING_DEPLOYMENTS_PER_DEPLOYER=10

# Aggregate staged/reserved bytes; expired uploads stay charged until physical cleanup succeeds.
MAX_PENDING_BYTES_PER_DEPLOYER=1073741824
MAX_PENDING_BYTES=53687091200
# Per-deployer accepted batch bytes per fixed one-minute window (including retries).
MAX_PARTIAL_UPLOAD_BYTES_PER_MINUTE=536870912
# Retain successful upload receipts for idempotent completion retries (ms).
COMPLETED_UPLOAD_TTL=86400000
# Separate pool for upload/GC advisory locks; does not consume the application query pool.
CONTENT_LOCK_CONNECTIONS=16
99 changes: 90 additions & 9 deletions docs/database-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,54 @@ The database contains the following main tables:
3. **`world_permissions`** - Stores deployment and streaming permission grants
4. **`world_permission_parcels`** - Stores parcel-level permission restrictions (normalized)
5. **`blocked`** - Stores blocked wallet addresses
6. **`migrations`** - Tracks executed database migrations (internal, managed automatically)
6. **`pending_scenes`** - Stores partial (multi-request) deployments still being uploaded (not yet live)
7. **`migrations`** - Tracks executed database migrations (internal, managed automatically)

### Table: `pending_scenes`

Staging area for partial deployments: a scene's content can be uploaded across several `POST /entities`
requests (with `partial=true`), and the entity only becomes a live `world_scenes` row once every
referenced file is present. Intentionally has **no** foreign key to `worlds` — a half-uploaded world must
not create a `worlds` row (which would leak into listings and world-validity checks) before it goes live.
The authoritative entity bytes live in content storage under the entity id; the `entity` JSONB here is a
copy used by garbage collection. Columns: `entity_id` (PK), `world_name`, `parcels` (TEXT[]), `entity`
(JSONB), `deployer`, `created_at`/`updated_at` (TIMESTAMPTZ), `initialized` (BOOLEAN), and
`reserved_bytes` (BIGINT). Uploads may overlap parcels and never replace another pending row.
`created_at` anchors freshness and the fixed `PENDING_DEPLOYMENT_TTL` (default 24h). Expired rows stay
charged until the eviction job or GC reclaims their objects, then removes their accounting.

### Table: `pending_scene_files`

Primary key `(entity_id, hash)`, with a cascading FK to `pending_scenes`. `size` is the reserved byte
count; `stored` distinguishes successful writes/verified reused content from reservations. Reserves
are atomic under a database admission lock. The pending-row `reserved_bytes` caches the sum so global
admission scans session totals instead of every content receipt. Failed writes remain conservatively
charged; retrying a hash does not reserve its storage twice. Incoming bytes, including retries, are
also counted in `partial_upload_rates` (`deployer` PK, `window_started`, `bytes`).

### Table: `completed_scene_uploads`

Primary key `entity_id`; columns `deployer`, `world_name`, `parcels`, `completed_at`. Publication writes
this receipt and removes pending state in the same transaction as the scene. The original signer gets
a stable completion response even after replacement or undeployment. Receipts expire independently
under `COMPLETED_UPLOAD_TTL` and do not retain content or consume staging slots.

### Content protection

Upload processing takes a shared PostgreSQL advisory lock through publication; GC and expired upload
cleanup take its exclusive counterpart for each physical delete batch. A separate WKC pool prevents
lock waiters from exhausting application query connections. Reference checks combine deployed scenes,
world thumbnails and live pending manifests in one SQL snapshot under that lock. Per-entity advisory
locks serialize batches and competing standard/partial completion attempts for the same entity.

#### Indexes

- **Primary Key**: `entity_id`
- **Index**: `pending_scenes_created_at_idx` on `created_at` (TTL expiry and GC protection set)
- **Index**: `pending_scenes_deployer_created_at_idx` on `(deployer, created_at)` (per-deployer upload count cap)

Deliberately unindexed: `world_name` and `parcels`. Uploads are addressed by entity id and never
looked up or replaced by overlap, so an index on either would only add write cost per staging insert.

---

Expand All @@ -36,7 +83,19 @@ erDiagram
VARCHAR deployer "Ethereum address"
TEXT_ARRAY parcels "Parcel coordinates"
BIGINT size "Scene size in bytes"
VARCHAR status "DEPLOYED or UNDEPLOYED (soft delete)"
TIMESTAMP created_at "Creation timestamp"
TIMESTAMP updated_at "Last status change"
}

pending_scenes {
VARCHAR entity_id PK "IPFS hash (CID)"
VARCHAR world_name "World being uploaded to (no FK)"
TEXT_ARRAY parcels "Parcel coordinates"
JSONB entity "Full entity JSON"
VARCHAR deployer "Ethereum address (lowercase)"
TIMESTAMPTZ created_at "Upload start (TTL anchor)"
TIMESTAMPTZ updated_at "Last batch received"
}

world_permissions {
Expand Down Expand Up @@ -91,18 +150,35 @@ Stores world metadata and access settings. With multi-scene support, this table

### Columns

| Column | Type | Nullable | Description |
| ------------------- | --------- | ------------ | ---------------------------------------------------------------------------------------- |
| `name` | VARCHAR | NOT NULL | **Primary Key**. World name (DCL name, e.g., `"myworld.dcl.eth"`). Stored in lowercase. |
| `access` | JSONB | **NOT NULL** | Access control settings. See [Access Settings](#access-settings) below. |
| `owner` | VARCHAR | NULL | Ethereum address of the DCL name owner (verified via blockchain). |
| `spawn_coordinates` | VARCHAR | NULL | World spawn parcel coordinate (e.g., `"0,0"`). Must belong be a coordinate inside of the world's shape. |
| `created_at` | TIMESTAMP | NOT NULL | Timestamp when the world record was first created. |
| `updated_at` | TIMESTAMP | NOT NULL | Timestamp when the world record was last updated. |
| Column | Type | Nullable | Description |
| ---------------------- | --------- | ------------ | ---------------------------------------------------------------------------------------- |
| `name` | VARCHAR | NOT NULL | **Primary Key**. World name (DCL name, e.g., `"myworld.dcl.eth"`). Stored in lowercase. |
| `access` | JSONB | **NOT NULL** | Access control settings. See [Access Settings](#access-settings) below. |
| `owner` | VARCHAR | NULL | Ethereum address of the DCL name owner (verified via blockchain). |
| `spawn_coordinates` | VARCHAR | NULL | World spawn parcel coordinate (e.g., `"0,0"`). Must belong be a coordinate inside of the world's shape. |
| `title` | VARCHAR | NULL | World title (settings; seeded from the first deployed scene's metadata). |
| `description` | TEXT | NULL | World description (settings; seeded from the first deployed scene's metadata). |
| `content_rating` | VARCHAR | NULL | Content rating (settings). |
| `skybox_time` | INTEGER | NULL | Fixed skybox time override (settings). |
| `categories` | TEXT[] | NULL | World categories/tags (settings). |
| `single_player` | BOOLEAN | NULL | Whether the world runs in single-player mode (settings). |
| `show_in_places` | BOOLEAN | NULL | Whether the world is listed in Places (settings). |
| `thumbnail_hash` | VARCHAR | NULL | Content hash of the world thumbnail (settings). |
| `last_deployed_at` | TIMESTAMP | NULL | Denormalized: timestamp of the latest deployed scene (maintained on deploy/undeploy). |
| `deployed_scene_count` | INTEGER | NOT NULL | Denormalized: number of DEPLOYED scenes (default 0). |
| `scene_min_x` | INTEGER | NULL | Denormalized: bounding rectangle of deployed scene parcels (min X). |
| `scene_max_x` | INTEGER | NULL | Denormalized: bounding rectangle of deployed scene parcels (max X). |
| `scene_min_y` | INTEGER | NULL | Denormalized: bounding rectangle of deployed scene parcels (min Y). |
| `scene_max_y` | INTEGER | NULL | Denormalized: bounding rectangle of deployed scene parcels (max Y). |
| `created_at` | TIMESTAMP | NOT NULL | Timestamp when the world record was first created. |
| `updated_at` | TIMESTAMP | NOT NULL | Timestamp when the world record was last updated. |

### Indexes

- **Primary Key**: `name`
- **GIN Index**: `worlds_search_idx` on `search_vector` (full-text search)
- **GIN Indexes**: `worlds_name_trgm_idx`, `worlds_title_trgm_idx`, `worlds_description_trgm_idx` (trigram search)
- **Index**: `worlds_last_deployed_at_idx` on `last_deployed_at` (world listings ordered by recency)

### Access Settings

Expand Down Expand Up @@ -178,14 +254,19 @@ Stores individual scene deployments within worlds. Each world can have multiple
| `deployer` | VARCHAR | NOT NULL | Ethereum address of the wallet that deployed this scene. |
| `parcels` | TEXT[] | NOT NULL | Array of parcel coordinates this scene occupies (e.g., `['0,0', '0,1', '1,0']`). |
| `size` | BIGINT | NOT NULL | Total size of this scene's content files in bytes. |
| `status` | VARCHAR | NOT NULL | Deployment status: `DEPLOYED` or `UNDEPLOYED` (soft delete; default `DEPLOYED`). |
| `created_at` | TIMESTAMP | NOT NULL | Timestamp when the scene was first deployed. |
| `updated_at` | TIMESTAMP | NOT NULL | Timestamp of the last status change or redeploy. |

### Indexes

- **Primary Key**: `(world_name, entity_id)` (composite primary key)
- **Index**: `world_scenes_world_name_idx` on `world_name` column
- **GIN Index**: `world_scenes_parcels_idx` on `parcels` column (for array operations)
- **Index**: `world_scenes_deployer_idx` on `deployer` column
- **Partial Index**: `world_scenes_status_idx` on `status` WHERE `status = 'DEPLOYED'` (hot-path reads)
- **Partial Index**: `world_scenes_undeployed_updated_at_idx` on `updated_at` WHERE `status = 'UNDEPLOYED'` (eviction job)
- **Index**: `world_scenes_updated_at_idx` on `updated_at` (garbage collection's deployed-since re-check)

### Constraints

Expand Down
Loading
Loading