Conversation
The self-hosted usage port flushed a fresh accumulator per queue message, producing one synchronous ClickHouse insert (and one part) per API request. On the benchmark host the resulting merge churn cost ~15% RPS (#13286). Standardize on cloud's write path: a worker-scoped accumulator flushed at 10,000 entries or every 20 seconds (per message in development, so local and E2E reads stay immediate), and async inserts on the adapter so ClickHouse coalesces parts server-side. The accumulator is detached before flushing so concurrent worker coroutines cannot double-write a snapshot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
| if ($accumulator->count() >= $this->flushThreshold() || $accumulator->elapsedSeconds() >= self::FLUSH_INTERVAL) { | ||
| // Detach before flushing: flush() yields on the insert, and the | ||
| // worker runs several coroutines — another message reaching this | ||
| // point mid-flush must not snapshot (and double-write) the same | ||
| // entries. Entries a failed flush retains are dropped with the | ||
| // detached buffer, consistent with the no-retry policy below. | ||
| $this->accumulator = null; | ||
| if (!$accumulator->flush()) { | ||
| Console::error('Usage event flush returned false'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Interval Requires Another Message
The 20-second interval is checked only while processing a queue message. If production traffic stops with fewer than 10,000 unique entries buffered, the deadline does not trigger a flush: usage remains unavailable until another message arrives and is lost if the worker exits first. The aged accumulator needs to be flushed independently of later traffic.
Knowledge Base Used: Background processing and scheduling
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Appwrite/Platform/Workers/StatsUsage.php
Line: 149-159
Comment:
**Interval Requires Another Message**
The 20-second interval is checked only while processing a queue message. If production traffic stops with fewer than 10,000 unique entries buffered, the deadline does not trigger a flush: usage remains unavailable until another message arrives and is lost if the worker exits first. The aged accumulator needs to be flushed independently of later traffic.
**Knowledge Base Used:** [Background processing and scheduling](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/appwrite/appwrite/-/docs/background-processing.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
✨ Benchmark resultsComparing
Per-scenario breakdown & investigation detailsMetrics below reflect the current branch (after). Δ P95 compares against the base.
Top API waits (after)
|
Replace the fixed flush constants with _APP_USAGE_FLUSH_THRESHOLD and _APP_USAGE_FLUSH_INTERVAL. Thresholds default to 10000 entries / 20s; development defaults to flushing every message so reads stay immediate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cloud's StatsUsage duplicates this worker (~300 lines) to add premium geo tags and resource-path parsing; a port of that copy is how the per-message flush regression shipped. Give the worker the narrow protected seams the cloud subclass needs — normalizeMetric() and resolveGeoTags() no-op hooks, an extracted flush(), and a protected timestamp() — so cloud can extend instead of copy. No behavior change on self-hosted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| [ | ||
| 'name' => '_APP_USAGE_FLUSH_THRESHOLD', | ||
| 'description' => 'Number of buffered usage entries that triggers a ClickHouse write. Defaults to 10000, or 1 in development so reads see usage at once.', | ||
| 'introduction' => '', | ||
| 'default' => '10000', | ||
| 'required' => false, | ||
| 'question' => '', | ||
| 'filter' => '' | ||
| ], | ||
| [ | ||
| 'name' => '_APP_USAGE_FLUSH_INTERVAL', | ||
| 'description' => 'Maximum age in seconds of buffered usage entries before they are written to ClickHouse.', | ||
| 'introduction' => '', | ||
| 'default' => '20', | ||
| 'required' => false, | ||
| 'question' => '', | ||
| 'filter' => '' | ||
| ], |
There was a problem hiding this comment.
Flush Settings Never Reach Workers
The new settings are read from the worker process environment, but neither the combined nor separate Docker Compose worker environment forwards them. On a normal self-hosted deployment, setting _APP_USAGE_FLUSH_THRESHOLD or _APP_USAGE_FLUSH_INTERVAL therefore has no effect, so usage flushing silently keeps its in-container defaults.
Knowledge Base Used: Platform configuration and policies
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/config/variables.php
Line: 1858-1875
Comment:
**Flush Settings Never Reach Workers**
The new settings are read from the worker process environment, but neither the combined nor separate Docker Compose worker environment forwards them. On a normal self-hosted deployment, setting `_APP_USAGE_FLUSH_THRESHOLD` or `_APP_USAGE_FLUSH_INTERVAL` therefore has no effect, so usage flushing silently keeps its in-container defaults.
**Knowledge Base Used:** [Platform configuration and policies](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/appwrite/appwrite/-/docs/platform-configuration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What does this PR do?
Standardizes self-hosted's usage write path on the pattern appwrite-labs/cloud already runs, and opens the worker up so cloud can extend it instead of copying it. The self-hosted port in #13286 flushed a fresh
Accumulatorper queue message, producing one synchronous ClickHouse insert — and one ClickHouse part — per completed API request. The resulting part/merge churn cost ~15% RPS on the benchmark (analysis on the project board item Investigate benchmark regressions in PRs merged Aug 18 – Sep 18).Three commits:
asyncInserts: true, asyncInsertWait: falseon the ClickHouse adapter (matching cloud'sapp/worker.php) so parts coalesce server-side. One deliberate improvement over cloud's copy: the accumulator is detached before flushing, sinceflush()yields on the insert and the worker runsstats-usageat 8 coroutines — flushing the shared buffer directly can double-write a snapshot (cloud's copy has this latent race)._APP_USAGE_FLUSH_THRESHOLD(default 10000; 1 in development so local and E2E reads see usage at once) and_APP_USAGE_FLUSH_INTERVAL(default 20s), documented inapp/config/variables.php.StatsUsageduplicates this worker (~300 lines) to add premium geo tags and resource-path parsing — porting that copy is how the per-message flush shipped in the first place. The worker now exposes narrow protected hooks (normalizeMetric(),resolveGeoTags(), extractedflush(), protectedtimestamp()) so the cloud class becomes a thin subclass. Companion cloud PR (draft, depends on this one): appwrite-labs/cloud#5954.Semantics
Usage delivery stays best-effort (unchanged): a worker crash now loses at most one flush window, and entries retained by a failed flush are dropped with the detached buffer, consistent with the existing no-retry policy.
Test plan
composer lintandcomposer analyzepass on the changed files.tests/e2e/Services/Usagereads viaassertEventually, and development mode keeps per-message flushing, so E2E behavior is unchanged.Related PRs and issues
🤖 Generated with Claude Code