Skip to content

Database Performance Optimizations - #768

Open
malinosqui wants to merge 15 commits into
db-cleanup-baselinefrom
db-cleanup-optimized
Open

malinosqui wants to merge 15 commits into
db-cleanup-baselinefrom
db-cleanup-optimized

Conversation

@malinosqui

@malinosqui malinosqui commented Jun 1, 2026

Copy link
Copy Markdown

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

  • Deadlock Prevention in Annotation Cleanup:
    • Redesigned the CleanAnnotations and CleanOrphanedAnnotationTags logic. Instead of using a single complex DELETE statement 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.
  • SQLite Parameter Limit Fix:
    • Added specific handling for SQLite databases during batch deletions. If the cleanup batch size exceeds SQLite's maximum parameter limit (999), the query now safely formats the IDs directly into the SQL statement to prevent execution failures.
  • Increased Cleanup Frequency:
    • Reduced the interval of the background cleanup job from running every 10 minutes to every 1 minute. This ensures stale data is removed more frequently in smaller, more manageable batches.
  • Test Suite Optimizations:
    • Significantly sped up the annotation cleanup tests by utilizing batch inserts (InsertMulti) instead of single-row inserts when generating test data.
    • Renamed database-heavy tests to indicate they are integration tests and added logic to skip them when running tests in short mode.
    • Added a specific test case to verify the SQLite variable limit fix for large batch sizes.

alexweav and others added 15 commits January 12, 2024 14:05
… 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>
@malinosqui

malinosqui commented Jun 1, 2026

Copy link
Copy Markdown
Author

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

Comment on lines +609 to +613
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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Performance high

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.

Comment on lines +615 to +618
placeholders := "?" + strings.Repeat(",?", len(ids)-1)
sql = fmt.Sprintf(`DELETE FROM %s WHERE id IN (%s)`, table, placeholders)
args = asAny(ids)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

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.

Comment on lines +531 to +539
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
})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug medium

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, y
Prompt 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants