Repository navigation
fix: drop stale __rill_temp_* ClickHouse tables left behind by interrupted model runs - #10025
Conversation
…rrupted model runs The ClickHouse driver creates `__rill_temp_<table>_<hash>` tables while writing a model (one per partition for partition_overwrite inserts, a view for column inference) and only drops them in a defer with a 15 s budget. The drop never runs if the runtime restarts mid-run and fails when a cancelled insert still holds the table lock, so the tables accumulated in the project database. Add dropStaleTempTables, which drops the temp tables of a model's table (including its staging name and `_local` counterparts in cluster mode) at the start of every execution, on Rename and on Delete. Temp tables that executions in this process are writing to are tracked in Connection.tempTables and skipped. In cluster mode, dropTable now also drops a `_local` table whose Distributed table was never created.
There was a problem hiding this comment.
The solution feels somewhat complex for a garbage collection. Sweeping on every execute can be wasteful for models with large number of partitions.
As I mentioned offline the temp table names are stable so this is usually never required.
However if we still want to handle it, checking once in Open should be enough. It is guaranteed that max one connection is opened at a single time so there is no risk of concurrent execution.
If we want faster garbage collection and faster Open than I suggest a background garbage collector instead. Open would start a goroutine, only in readwrite mode, that runs once immediately and then hourly, and drops every _rill_temp* table older than 24 hours:
SELECT name FROM system.tables
WHERE database = currentDatabase()
AND startsWith(name, '__rill_temp_')
AND metadata_modification_time < now() - INTERVAL 24 HOUR
The default model execution timeout is 3h so 24h gives us a good margin as well.
Sweeping on every execution added a metadata query per partition run and needed an in-flight registry to protect concurrent partition inserts. Instead, Open now starts a goroutine for readwrite connections that runs once immediately and then hourly, and drops every __rill_temp_* table whose metadata_modification_time is older than 24 hours. The default model timeout is 3 hours, so running executions are not affected. The hooks in Execute, Rename and Delete and the registry are removed; the `_local` fallback in dropTable stays since it also fixes the in-flight cleanup of cancelled cluster inserts.
k-anshul
left a comment
There was a problem hiding this comment.
There is a rare but non-zero chance that list returns a table name and then a reconciler recreates it and then drop drops it while a concurrent reconcile is running.
| c.logger.Warn("clickhouse: failed to drop stale temp table", zap.String("name", name), zap.Error(err), observability.ZapCtx(ctx)) | ||
| continue | ||
| } | ||
| c.logger.Info("clickhouse: dropped stale temp table", zap.String("name", name), observability.ZapCtx(ctx)) |
There was a problem hiding this comment.
better to skip the log on ErrNotFound from correctness pov.
| } | ||
| for _, name := range names { | ||
| err := c.dropTable(ctx, name) | ||
| if err != nil && !errors.Is(err, drivers.ErrNotFound) { |
There was a problem hiding this comment.
Nit: Can return early on ctx canceled.
|
|
||
| for { | ||
| // Skip if the database hasn't been used recently and may be scaled to zero, so that the sweep does not wake it up. | ||
| if !c.config.CanScaleToZero || time.Since(c.lastUsedOn()) < 2*time.Minute { |
There was a problem hiding this comment.
Looks like this was copied from periodicallyEmitStats but that runs at a faster schedule whereas this runs at an hourly schedule, there is a good chance most schedules may miss it.
But waking up cluster to ensure garbage collection may be wasteful so may need to rethink. It also advances the used tick.
There was a problem hiding this comment.
I agree waking up the cluster seems wasteful and its ok if it doesn't end up running for few hrs or even days.. what we want to prevent is these tables silently piling up.
There was a problem hiding this comment.
Changed to a minute ticker that sweeps once an hour has passed and the DB was used in the last 2 min, so a due sweep runs as soon as the DB is awake, without waking it.
…r loop Temp table names are deterministic, so an execution could recreate a stale table after the sweep listed it and before it dropped it. Creation now holds tempTableMu for reading and the sweep holds it for writing across the listing and the drops. The sweep skips the log on ErrNotFound, returns early when the context is cancelled, and the collector ticks every minute but sweeps at most hourly, so a due sweep runs as soon as a database that can scale to zero is in use again instead of waiting for an hourly tick to coincide.
|
Switched to the collector as suggested; kept the Race fixed with |
|
|
||
| // tempTableMu serializes the creation of temporary tables (read lock) with the sweep of stale ones (write lock). | ||
| // Temporary tables have deterministic names, so without it an execution could recreate a stale table after the sweep listed it and before it dropped it. | ||
| tempTableMu sync.RWMutex |
There was a problem hiding this comment.
Use runtime/pkg/ctxsync/rwmutex.go
While writing a model, the ClickHouse driver creates
__rill_temp_<table>_<hash>tables: one per partition forpartition_overwriteinserts and a view for column inference. They are only dropped in adeferwith a 15 s budget, so they stay behind when the runtime restarts mid-run or when the drop fails because a cancelled insert still holds the table lock. Nothing swept them afterwards.Opennow starts a background collector for readwrite connections. It sweeps once immediately and then at most hourly, and drops every__rill_temp_*table in the project database whosemetadata_modification_timeis older than 24 hours. The default model timeout is 3 hours, so running executions are not affected. A database that can scale to zero is only swept while it is in use, so the collector never wakes it up.tempTableMu, since temp table names are deterministic: a table created before the sweep listed the stale ones is not stale, and one created after it waits for the drops to finish.dropTablein cluster mode now also drops a_localtable whoseDistributedtable was never created, which previously stranded the_localhalf of a temp table when the execution was cancelled between the twoCREATEs.Steps to reproduce:
incremental_strategy: partition_overwriteand several partitions.CREATE TABLE __rill_temp_<model>_<32 hex chars> AS <model>.SELECT name FROM system.tables WHERE name LIKE '__rill_temp_%'. Before this change the leftover table stays until the same partition happens to run again; now the collector drops it within an hour of it turning 24 hours old and logsdropped stale temp table.Closes APP-947
Checklist: