Database Performance Optimizations - #768
malinosqui wants to merge 15 commits into
Conversation
… deadlocks on MySQL (#80329) * Split subquery when cleaning annotations * update comment * Raise batch size, now that we pay attention to it * Iterate in batches * Separate cancellable batch implementation to allow for multi-statement callbacks, add overload for single-statement use * Use split-out utility in outer batching loop so it respects context cancellation * guard against empty queries * Use SQL parameters * Use same approach for tags * drop unused function * Work around parameter limit on sqlite for large batches * Bulk insert test data in DB * Refactor test to customise test data creation * Add test for catching SQLITE_MAX_VARIABLE_NUMBER limit * Turn annotation cleanup test to integration tests * lint --------- Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
Code Review Completed! 🔥The code review was successfully completed based on your current configurations. Kody Guide: Usage and ConfigurationInteracting with Kody
Current Kody ConfigurationReview OptionsThe following review options are enabled or disabled:
|
| values := fmt.Sprint(ids[0]) | ||
| for _, v := range ids[1:] { | ||
| values = fmt.Sprintf("%s, %d", values, v) | ||
| } | ||
| sql = fmt.Sprintf(`DELETE FROM %s WHERE id IN (%s)`, table, values) |
There was a problem hiding this comment.
O(N²) memory allocation bottleneck exists in the SQLite string concatenation loop because fmt.Sprintf generates a new string on every iteration. Use strings.Join or strings.Builder to construct the comma-separated values in linear time and avoid Out-Of-Memory crashes.
var strIDs []string
for _, id := range ids {
strIDs = append(strIDs, strconv.FormatInt(id, 10))
}
sql = fmt.Sprintf(`DELETE FROM %s WHERE id IN (%s)`, table, strings.Join(strIDs, ","))Prompt for LLM
File pkg/services/annotations/annotationsimpl/xorm_store.go:
Line 609 to 613:
WHAT: The SQLite string concatenation loop creates an O(N²) memory allocation bottleneck. WHY: Using `fmt.Sprintf` inside a loop generates a new string on every iteration. For large batch sizes (e.g., >10,000), this allocates hundreds of megabytes or even gigabytes of string garbage, risking Out-Of-Memory crashes and massive GC spikes. HOW: Use a string slice and `strings.Join`, or `strings.Builder` to construct the comma-separated values in linear time.
Suggested Code:
var strIDs []string
for _, id := range ids {
strIDs = append(strIDs, strconv.FormatInt(id, 10))
}
sql = fmt.Sprintf(`DELETE FROM %s WHERE id IN (%s)`, table, strings.Join(strIDs, ","))
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| placeholders := "?" + strings.Repeat(",?", len(ids)-1) | ||
| sql = fmt.Sprintf(`DELETE FROM %s WHERE id IN (%s)`, table, placeholders) | ||
| args = asAny(ids) | ||
| } |
There was a problem hiding this comment.
Parameter limit violation in PostgreSQL and MySQL occurs because asAny(ids) creates a parameter for each ID, exceeding the 65,535 limit. Chunk the ids slice or cap AnnotationCleanupJobBatchSize to prevent driver errors from breaking the cleanup routine.
Prompt for LLM
File pkg/services/annotations/annotationsimpl/xorm_store.go:
Line 615 to 618:
WHAT: The new slice-based placeholder generation imposes a hard limit of 65,535 on the batch size for PostgreSQL and MySQL. WHY: Previously, the batch size was enforced via a `LIMIT` subquery which did not use placeholders and had no upper bound. Now, `asAny(ids)` creates a parameter for each ID. If an administrator configures `AnnotationCleanupJobBatchSize` > 65535, the cleanup job will perpetually fail with a driver error ('too many parameters' or 'prepared statement contains too many placeholders'), breaking the cleanup routine. HOW: Chunk the slice of IDs into smaller batches (e.g., 65000 elements) before executing the query, or cap the configured batch size at runtime.
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
| if err != nil { | ||
| return 0, err | ||
| } | ||
| r.log.Error("Annotations to clean by time", "count", len(ids), "ids", ids, "cond", cond, "err", err) | ||
|
|
||
| affected, err := r.executeUntilDoneOrCancelled(ctx, sql) | ||
| x, y := r.deleteByIDs(ctx, "annotation", ids) | ||
| r.log.Error("cleaned annotations by time", "count", len(ids), "affected", x, "err", y) | ||
| return x, y | ||
| }) |
There was a problem hiding this comment.
Unintentional r.log.Error calls in the cleanup routines, including MaxCount and OrphanedTags blocks, spam production logs with large ids arrays and cause false alarms. Remove these statements or change the level to r.log.Debug to prevent log bloat.
ids, err := r.fetchIDs(ctx, "annotation", cond)
if err != nil {
return 0, err
}
x, y := r.deleteByIDs(ctx, "annotation", ids)
return x, yPrompt for LLM
File pkg/services/annotations/annotationsimpl/xorm_store.go:
Line 531 to 539:
WHAT: Debug statements using `r.log.Error` were accidentally left in the cleanup routines. WHY: Because the cleanup job runs every 1 minute, it will spam production logs with unconditional ERROR-level messages and format large arrays of IDs even when no cleanup is needed, causing false alarms and log bloat. HOW: Remove these log statements or change them to `r.log.Debug` (Note: this also applies to the similar log lines in the MaxCount and OrphanedTags blocks).
Suggested Code:
ids, err := r.fetchIDs(ctx, "annotation", cond)
if err != nil {
return 0, err
}
x, y := r.deleteByIDs(ctx, "annotation", ids)
return x, y
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.
Here is a precise description of the pull request based on the provided code changes:
Summary
This PR introduces database performance optimizations and stability improvements for the background cleanup processes, specifically targeting how annotations and orphaned tags are deleted. It resolves potential database deadlocks, fixes a crash related to SQLite limits, and increases the frequency of the cleanup job.
Key Changes
CleanAnnotationsandCleanOrphanedAnnotationTagslogic. Instead of using a single complexDELETEstatement with batched sub-queries (which caused deadlocks with concurrent inserts, particularly in MySQL), the process now fetches the target IDs into memory first and then deletes them in batches. This allows database locks to flush between operations.InsertMulti) instead of single-row inserts when generating test data.shortmode.