From a889e2e60fa35017ad28ef962a4b1622d84a1629 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 14 May 2026 03:30:39 +0000 Subject: [PATCH] feat(clickhouse): add opt-in async cleanup via setAsyncCleanup() `cleanup()` defaults to ClickHouse's synchronous mutation behavior, so existing tests and callers see no change. Consumers that just need to schedule the DELETE (e.g. maintenance workers running per-project on a shared multi-tenant table) can call `setAsyncCleanup(true)` to append `SETTINGS lightweight_deletes_sync = 0` and have the HTTP call return once the mutation is queued. This avoids the 30s client timeout observed in production when the per-tenant DELETE serialized N mutations through Keeper: Failed to cleanup ClickHouse audit logs for project : ClickHouse query execution failed: Operation timed out after 30026 milliseconds with 0 bytes received Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Audit/Adapter/ClickHouse.php | 36 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index e1fff33..3bc1b81 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -68,6 +68,8 @@ class ClickHouse extends SQL protected bool $sharedTables = false; + protected bool $asyncCleanup = false; + /** * @param string $host ClickHouse host * @param string $username ClickHouse username (default: 'default') @@ -296,6 +298,31 @@ public function isSharedTables(): bool return $this->sharedTables; } + /** + * Set whether cleanup() should return after scheduling the DELETE mutation + * rather than waiting for it to complete. When enabled, the DELETE is sent + * with `SETTINGS lightweight_deletes_sync = 0` and the HTTP call returns + * as soon as the mutation is queued. + * + * @param bool $asyncCleanup + * @return self + */ + public function setAsyncCleanup(bool $asyncCleanup): self + { + $this->asyncCleanup = $asyncCleanup; + return $this; + } + + /** + * Get whether cleanup() runs asynchronously. + * + * @return bool + */ + public function isAsyncCleanup(): bool + { + return $this->asyncCleanup; + } + /** * Override getAttributes to provide extended attributes for ClickHouse. * Includes existing attributes from parent and adds new missing ones. @@ -1957,8 +1984,6 @@ public function countByResourceAndEvents( /** * Delete logs older than the specified datetime. * - * ClickHouse uses ALTER TABLE DELETE with synchronous mutations. - * * @throws Exception */ public function cleanup(\DateTime $datetime): bool @@ -1967,14 +1992,13 @@ public function cleanup(\DateTime $datetime): bool $tenantFilter = $this->getTenantFilter(); $escapedTable = $this->escapeIdentifier($this->database) . '.' . $this->escapeIdentifier($tableName); - // Convert DateTime to string format expected by ClickHouse $datetimeString = $datetime->format('Y-m-d H:i:s.v'); - // Use DELETE statement for synchronous deletion (ClickHouse 23.3+) - // Falls back to ALTER TABLE DELETE with mutations_sync for older versions + $settings = $this->asyncCleanup ? ' SETTINGS lightweight_deletes_sync = 0' : ''; + $sql = " DELETE FROM {$escapedTable} - WHERE time < {datetime:String}{$tenantFilter} + WHERE time < {datetime:String}{$tenantFilter}{$settings} "; $this->query($sql, ['datetime' => $datetimeString]);