-
Notifications
You must be signed in to change notification settings - Fork 1
refactor(usage): extract buffering into a standalone Accumulator #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
7eafc97
refactor(usage): extract buffering into a standalone Accumulator
loks0n 7fedf50
feat(usage): make tenant explicit, drop stateful setTenant
loks0n 7942cdc
fix(usage): tenant-correct purge + satisfy phpstan/pint
loks0n 0404988
refactor(clickhouse): bake tenant into parseQueries, drop scopeToTenant
loks0n 3aeaa4f
refactor(usage): Accumulator takes Usage, not Adapter
loks0n c0cf170
fix(usage): address review — collision-safe buffer key, retry timing,…
loks0n c2ff9fc
fix(usage): accept tenant/metric id "0" in collect()
loks0n 6c3b609
fix(usage): Tenant decorator rejects an empty bound tenant
loks0n 3715eaf
fix(clickhouse): reject empty tenant on reads in shared-tables mode
loks0n File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Usage; | ||
|
|
||
| /** | ||
| * In-memory metric accumulator. | ||
| * | ||
| * Buffers collect() calls and flushes them to an adapter in batches. | ||
| * Events are summed per metric+tags; gauges use last-write-wins. | ||
| * | ||
| * The accumulator exposes raw signals — count() and elapsedSeconds() — so | ||
| * callers can decide when to flush rather than baking a policy in here. | ||
| */ | ||
| class Accumulator | ||
| { | ||
| private Adapter $adapter; | ||
|
|
||
| /** | ||
| * In-memory buffer for metrics. | ||
| * Keyed by "{metric}:{type}:{tagsHash}" — events are summed, gauges use last-write-wins. | ||
| * | ||
| * @var array<string, array{metric: string, value: int, type: string, tags: array<string, mixed>}> | ||
| */ | ||
| private array $buffer = []; | ||
|
|
||
| /** @var float Timestamp of the last flush */ | ||
| private float $flushedAt; | ||
|
|
||
| /** | ||
| * @param Adapter $adapter The adapter to flush buffered metrics to | ||
| */ | ||
| public function __construct(Adapter $adapter) | ||
| { | ||
| $this->adapter = $adapter; | ||
| $this->flushedAt = microtime(true); | ||
| } | ||
|
|
||
| /** | ||
| * Collect a metric into the in-memory buffer for deferred flushing. | ||
| * | ||
| * For event type: multiple collect() calls for the same metric are summed. | ||
| * For gauge type: last-write-wins semantics. | ||
| * No period fan-out — raw timestamps are used. | ||
| * | ||
| * @param string $metric Metric name | ||
| * @param int $value Value | ||
| * @param string $type Metric type: 'event' or 'gauge' | ||
| * @param array<string,mixed> $tags Optional tags | ||
| * @return self | ||
| */ | ||
| public function collect(string $metric, int $value, string $type, array $tags = []): self | ||
| { | ||
| if (empty($metric)) { | ||
| throw new \InvalidArgumentException('Metric name cannot be empty'); | ||
| } | ||
| if ($value < 0) { | ||
| throw new \InvalidArgumentException('Value cannot be negative'); | ||
| } | ||
| if ($type !== Usage::TYPE_EVENT && $type !== Usage::TYPE_GAUGE) { | ||
| throw new \InvalidArgumentException("Invalid metric type '{$type}'. Allowed: " . Usage::TYPE_EVENT . ', ' . Usage::TYPE_GAUGE); | ||
| } | ||
|
|
||
| $tagsHash = !empty($tags) ? md5(json_encode($tags, JSON_THROW_ON_ERROR)) : ''; | ||
| $key = $metric . ':' . $type . ':' . $tagsHash; | ||
|
|
||
| if ($type === Usage::TYPE_EVENT && isset($this->buffer[$key])) { | ||
| // Additive: sum values for the same metric + tags combination | ||
| $this->buffer[$key]['value'] += $value; | ||
| } else { | ||
| // New event entry, or gauge (last-write-wins) | ||
| $this->buffer[$key] = [ | ||
| 'metric' => $metric, | ||
| 'value' => $value, | ||
| 'type' => $type, | ||
| 'tags' => $tags, | ||
| ]; | ||
| } | ||
|
|
||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Flush the in-memory buffer to storage. | ||
| * | ||
| * Separates buffered metrics into events and gauges, then writes each batch | ||
| * to the appropriate table via addBatch(). Only entries whose batch write | ||
| * succeeds are removed from the buffer, so a partial failure preserves | ||
| * the unwritten metrics for retry on the next flush. | ||
| * | ||
| * If addBatch() throws mid-flush, any earlier successful batches have | ||
| * already been cleared from the buffer — the exception is allowed to | ||
| * propagate so the caller can observe the failure. | ||
| * | ||
| * @return bool True if all batches succeeded (or buffer was empty) | ||
| * @throws \Exception | ||
| */ | ||
| public function flush(): bool | ||
| { | ||
| if (empty($this->buffer)) { | ||
| $this->flushedAt = microtime(true); | ||
| return true; | ||
| } | ||
|
|
||
| // Separate events and gauges; keep track of buffer keys so we can | ||
| // selectively unset only the entries whose write succeeded. | ||
| $eventKeys = []; | ||
| $gaugeKeys = []; | ||
| $events = []; | ||
| $gauges = []; | ||
|
|
||
| foreach ($this->buffer as $key => $entry) { | ||
| if ($entry['type'] === Usage::TYPE_EVENT) { | ||
| $events[] = $entry; | ||
| $eventKeys[] = $key; | ||
| } else { | ||
| $gauges[] = $entry; | ||
| $gaugeKeys[] = $key; | ||
| } | ||
| } | ||
|
|
||
| $overallResult = true; | ||
|
|
||
| // Flush events — clear buffer entries only on success. | ||
| if (!empty($events)) { | ||
| if ($this->adapter->addBatch($events, Usage::TYPE_EVENT)) { | ||
| foreach ($eventKeys as $key) { | ||
| unset($this->buffer[$key]); | ||
| } | ||
| } else { | ||
| $overallResult = false; | ||
| } | ||
| } | ||
|
|
||
| // Flush gauges — clear buffer entries only on success. | ||
| if (!empty($gauges)) { | ||
| if ($this->adapter->addBatch($gauges, Usage::TYPE_GAUGE)) { | ||
| foreach ($gaugeKeys as $key) { | ||
| unset($this->buffer[$key]); | ||
| } | ||
| } else { | ||
| $overallResult = false; | ||
| } | ||
| } | ||
|
|
||
| $this->flushedAt = microtime(true); | ||
|
greptile-apps[bot] marked this conversation as resolved.
Outdated
greptile-apps[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| return $overallResult; | ||
| } | ||
|
|
||
| /** | ||
| * Number of unique metric entries currently buffered. | ||
| * | ||
| * @return int | ||
| */ | ||
| public function count(): int | ||
| { | ||
| return count($this->buffer); | ||
| } | ||
|
|
||
| /** | ||
| * Seconds elapsed since the last flush. | ||
| * | ||
| * @return float | ||
| */ | ||
| public function elapsedSeconds(): float | ||
| { | ||
| return microtime(true) - $this->flushedAt; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.