Parallelize S3-backed multipart uploads while preserving publication consistency by loks0n · Pull Request #14709 · appwrite/appwrite · GitHub
Skip to content

Parallelize S3-backed multipart uploads while preserving publication consistency - #14709

Open
loks0n wants to merge 4 commits into
codex/storage-upload-list-performancefrom
codex/storage-parallel-multipart
Open

loks0n wants to merge 4 commits into
codex/storage-upload-list-performancefrom
codex/storage-parallel-multipart

Conversation

@loks0n

@loks0n loks0n commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Native and S3-compatible multipart uploads currently hold a shared upload lock across every backend transfer. Eight concurrent SDK calls therefore still upload one part at a time. Split preparation, transfer and receipt recording so distinct S3-backed parts overlap, while same-part retries and final publication remain coordinated. Local storage retains its existing serialization.

Use unique native multipart generation paths and validate their path/upload ID before acknowledging progress. Coordinate native deletion with publication, and return retryable S3 SlowDown while a native file is incomplete. Native document creation now takes the same folder/name publication lock as S3 completion, only around document creation. Contention retains finalized staging bytes for safe retries when an older native writer bypasses that guard. Keep S3's isolated overwrite paths, selected-part ETags and pinned download streams intact. Existing upload() still auto-finalizes; uploadChunk() exposes the existing explicit non-finalizing phase.

Distributed locks can borrow Redis per command rather than retaining a pooled connection for the lease. This is necessary for nested per-part and upload-state guards without exhausting the connection pool; direct Redis construction is unchanged. Token-guarded release retries a transient Redis failure once; persistent cleanup failure preserves the original operation exception, and still propagates if the operation succeeded. No SDK or benchmark source changes are required.

Test Plan

Local isolated Appwrite + PostgreSQL + Redis + the CI-pinned Silo S3 backend, PHP 8.5.11, four HTTP workers:

  • Storage package: 179 tests / 661 assertions.
  • Real-Redis lock E2E: 20 tests / 82 assertions; provider and release-failure regressions observed red/green. Lock unit tier passes; two Docker pcntl skips covered by all four host File tests.
  • Native HTTP E2E: 8 tests / 337 assertions; guest/ownership: 2 / 70; range validation: 2 / 22.
  • S3 HTTP E2E: 5 tests / 123 assertions, including parallel/duplicate parts, selected parts, late retries, native reads during overwrite, and S3/native publication. New cross-API regression observed red/green.
  • Deterministic S3 publication drill: original completion returned 503 and retry failed 500 after losing staged data. With the shared creation guard, completion succeeds and native creation waits. With an older native handler deliberately bypassing the guard, the first completion returns 503 and retry succeeds with the full expected SHA-256.
  • Forced real-HTTP drills: stale transfer after delete/recreate, conflicting native/S3 retries, lease loss and recovery, and abort during transfer all preserve exact expected bytes/state.
  • 64 concurrent native requests / 32 distinct parts / 160 MiB: all successful and full-file SHA-256 verified.
  • Targeted PHPStan, storage/lock package Rector/Pint/PHPStan, root E2E Rector, formatting and monorepo validation pass.

Three verified 100 MiB samples/API/version with the same client, eight concurrent part calls and a controlled 250 ms backend part delay:

API Before median After median Backend overlap
Native 9.283 s 1.855 s 1 → 8
S3 8.800 s 1.614 s 1 → 8

Native file-lock hold median: 374 → 3.72 ms; S3 upload-state lock hold median: 362 → 0.14 ms. These are controlled latency results, not production estimates or a Railway comparison. Initial exploratory single-worker memory exhaustion is not claimed fixed; both comparison sides use four workers.

Coverage limit: forced lease-loss and delete/recreate-in-flight checks are controlled manual drills, not automated CI E2E. The review-fix drill also remains manual. Current review-fix evidence is /Users/loks0n/.codex/artifacts/storage-review-fixes-2026-10-21. The original performance source/trace/report bundle is /Users/loks0n/.codex/artifacts/storage-multipart-profile-2026-10-20; operational details are on the linked tracking issue.

Deployments remain paused. No merge, release, staging/production rollout or Cloud dependency update is included.

Related PRs and Issues

Checklist

  • Read repository contribution and agent instructions.
  • No HTTP SDK metadata or API schema changes; no generated spec updates required.

@loks0n loks0n self-assigned this Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Security rules

No new WARNING or ERROR findings from security rules.

19 existing findings tracked in .semgrep/baseline.json
  • php.appwrite.guest-write-without-abuse-limit (12)
  • php.appwrite.secret-compare-timing (5)
  • php.appwrite.weak-secret-env-default (2)

Posted by Checks / Rules. Re-runs update this comment in place. Rule details and baseline: .semgrep/README.md.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

✨ Benchmark results

Comparing codex/storage-upload-list-performance (before) → codex/storage-parallel-multipart (after).

Metric Before After Change
🚀 Requests/sec 206.99 205.29 ⚪ -0.8%
⏱️ Latency P50 83.51 ms 84.69 ms ⚪ +1.4%
⏱️ Latency P95 189.24 ms 196.3 ms ⚪ +3.7%
Per-scenario breakdown & investigation details

Metrics below reflect the current branch (after). Δ P95 compares against the base.

Scenario P50 (ms) P95 (ms) Requests RPS Δ P95 (ms)
API total 84.69 196.3 12,597 205.29 +7.06
Account 163.7 296.44 663 11 +30.9
TablesDB 80.62 147.55 6,851 113.07 +2.36
Storage 82.33 190.43 3,315 56.96 -8.31
Functions 121.14 224.67 1,768 31.06 +14.83

Top API waits (after)

API request Max wait (ms)
account.name.update 604.01
functions.variables.update 456.22
account.prefs.update 421.97
storage.buckets.create 420.29
functions.create 373.61

@loks0n
loks0n marked this pull request as ready for review October 21, 2026 09:57
@tenki-reviewer

tenki-reviewer Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 1 medium

📍 Findings outside the diff (1) — 🟡 1 medium — defects on lines GitHub can't attach comments to

🟡 Medium — Failed release() in finally masks the original exception · Distributed.php:129–133 · unchanged line

// packages/lock/src/Distributed.php
129	        try {
130	            return $callback();
131	        } finally {
132	            $this->release();
133	        }

withLock calls release() from a finally, but release() (packages/lock/src/Distributed.php:112-120) throws if the Redis round-trip fails and only clears $this->token on success. When the guarded callback throws for a Redis-related reason, release must re-borrow a connection at exactly that moment, so its exception replaces the callback's original error and the token stays set. Callers then observe the wrong failure (the LockContention handling in Files/Create.php:697-699 never runs) and the key stays held until TTL expiry, forcing waiters to contend against a ghost lease for up to 600s.


The PR introduces lease-based distributed locking around S3 multipart uploads and file deletion, adds device-layer support for chunked staging and part accounting, and wires the new lock resource into the app bootstrap. E2E tests cover the S3 server and storage flows.

Files Change
packages/lock/src/Distributed.php, packages/lock/tests/E2E/DistributedTest.php Distributed lock lease/execute/release semantics with e2e coverage
packages/storage/src/Device.php, packages/storage/src/Device/{Local,S3,Telemetry}.php + unit tests Device API changes for chunk staging, S3 part handling, telemetry counters
src/Appwrite/Platform/Modules/S3/Http/S3/{Base,Create}.php, src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/{Create,Delete}.php Multipart part receipt handling and lock-guarded upload/delete actions
app/init/registers.php, app/init/resources.php Register the new lock resource for all process types
tests/e2e/Services/Storage/* E2E suites for the S3 server and shared storage flows

Reviewed commit: 37aebb6

@hansi-codes

hansi-codes Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Tier S · Ready to merge

Multipart uploads now separate preparation, transfer, and receipt recording so S3-backed parts can overlap while local uploads retain serialization. Native uploads use generation-specific paths and coordinate publication and deletion with S3 operations; distributed locks borrow Redis connections per command, and storage APIs expose explicit chunk upload without finalizing.

Latest changes: The newest commits make lock release retry once while preserving callback errors, coordinate native document creation with S3 object publication, and retain finalized multipart staging on contention so receipts remain retryable.

Verdict New comments Fixed Still open
✅ Approved 0 1 0
📂 Walkthrough · 16
File Change
app/init/registers.php Adjusts the Redis lock pool capacity and documents its connection budget.
app/init/resources.php Configures distributed locks to borrow a Redis connection for each command.
packages/lock/src/Distributed.php Supports Redis providers and retries token-guarded release while preserving operation errors.
packages/lock/tests/E2E/DistributedTest.php Tests nested borrowed connections and transient or sustained release failures.
packages/storage/src/Device.php Adds an explicit non-finalizing chunk upload operation to the storage device contract.
packages/storage/src/Device/Local.php Exposes local chunk upload through the device API.
packages/storage/src/Device/S3.php Exposes S3 chunk upload while keeping transfer separate from finalization.
packages/storage/src/Device/Telemetry.php Exposes and measures chunk uploads through the telemetry wrapper.
packages/storage/tests/Device/LocalTest.php Covers local chunk upload remaining unpublished until finalization.
packages/storage/tests/Device/S3Test.php Covers explicit S3 chunk uploads and selected-part ETags during finalization.
src/Appwrite/Platform/Modules/S3/Http/S3/Base.php Separates S3 part transfer from state updates, coordinates publication, and retains staging on contention.
src/Appwrite/Platform/Modules/S3/Http/S3/Create.php Adds per-part coordination and merges receipts into current multipart state.
src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php Splits native S3-backed upload phases, assigns generation-specific paths, and coordinates document creation with S3 publication.
src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Delete.php Serializes native file deletion with upload coordination.
tests/e2e/Services/Storage/S3CustomServerTest.php Covers parallel S3 multipart uploads and native/S3 overwrite behavior.
tests/e2e/Services/Storage/StorageBase.php Updates the late-retry fixture to verify the original uploaded chunk is retained.
✅ Fixed since the last review · 1
  • The native-upload check is not atomic with S3 publication · src/Appwrite/Platform/Modules/S3/Http/S3/Base.php:795

Reviewed the commits since 37aebb6 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Tier B · 1 blocking finding to address. Summary

Comment thread src/Appwrite/Platform/Modules/S3/Http/S3/Base.php

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Tier S · Looks good to merge. Summary

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