Skip to content

feat/prune age#1970

Open
alanpeixinho wants to merge 8 commits into
kernelci:mainfrom
profusion:feat/prune-age
Open

feat/prune age#1970
alanpeixinho wants to merge 8 commits into
kernelci:mainfrom
profusion:feat/prune-age

Conversation

@alanpeixinho

@alanpeixinho alanpeixinho commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Add prune_db, a manual management command to delete old data and reclaim space.

  • Deletes checkouts, builds and tests older than a given age (e.g. --older-than "30 days").
  • Manual cascade (models use DO_NOTHING): pruning a checkout removes its builds, pruning a build removes its tests — even when children are newer.
  • --origins scopes the age filter; cascade stays origin-agnostic so no orphans are left.
  • Batched deletes (--batch-size) that each commit on their own to keep locks short.
  • --dry-run and a per-table confirmation prompt showing exactly how many rows will be affected (skip with --yes).
  • Extracts parse_interval from update_db into a shared helper.

Usage

python manage.py prune_db --older-than "30 days" --dry-run                                                                                                                                                         
python manage.py prune_db --older-than "30 days" --origins maestro                                                                                                                                          

How to Test

  • In a local database create newly seeded data poetry run python3 manage.py seed_data --clear --yes
  • Run command poetry run python3 manage.py prune_db --dry-run with different arguments for older-than parameter.
  • Verify the number of selected rows matches the database.
  • Run command poetry run python3 manage.py prune_db with different arguments for older-than parameter.
  • Verify the rows are properly deleted.

@alanpeixinho
alanpeixinho force-pushed the feat/prune-age branch 2 times, most recently from e5c35c3 to 552bcc4 Compare July 3, 2026 20:04

# Values are always passed as query parameters; only static SQL fragments are
# interpolated. Named parameters let us reuse the same dict across every query.
params = {"cutoff": cutoff, "batch_size": options["batch_size"]}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a minor but I think that would be nice to validate the batch_size. Currently we are able to pass a negative value to the database.

django.db.utils.DataError: LIMIT must not be negative

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines +1 to +8
"""
Management command to prune old builds, tests and checkouts.

Removes checkouts, builds and tests older than a given age. To keep referential
integrity, deletion cascades manually (models use DO_NOTHING): a removed
checkout drags its builds, and a removed build drags its tests, even when those
children are newer than the cutoff.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth noting in docs that aggregate tables are not touched by this command.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@tales-aparecida

Copy link
Copy Markdown

@nuclearcat how long/far-back do we keep the raw KCIDB json payloads?

@@ -0,0 +1,25 @@
from datetime import datetime, timedelta

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Django has a built-in parser django.utils.dateparse.parse_duration that could be used here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I took a look on the built-in parser. Unfortunately it has different standards from what we are following.

  • standard: "30 00:00:00" (%d %H:%M:%S)
  • ISO 8601: "P30D"
  • postgres: "3 days 04:05:06" (only works for days)
    I believe we might benefit more of keeping our custom parser, we could even extend it to include weeks or months, especially in a scenario where we are working with longer ranges.
    What you think?

@nuclearcat

Copy link
Copy Markdown
Member

Each batch re-evaluates the full nested predicate. Add on top batch might be too small (we need to check how many tests per day we have, how long take deletion of 10k).
Consider materializing the ids once into a CREATE TEMP TABLE tobedeleted_... AS SELECT id .., and then batch-deleting by joining against it. Temp tables survive across the per-batch commits within the same session, so this fits the existing design.

@alanpeixinho
alanpeixinho requested a review from mentonin July 7, 2026 12:45
@alanpeixinho
alanpeixinho force-pushed the feat/prune-age branch 2 times, most recently from 9a454fa to 6995fc0 Compare July 8, 2026 21:16
@alanpeixinho

Copy link
Copy Markdown
Contributor Author

Each batch re-evaluates the full nested predicate. Add on top batch might be too small (we need to check how many tests per day we have, how long take deletion of 10k). Consider materializing the ids once into a CREATE TEMP TABLE tobedeleted_... AS SELECT id .., and then batch-deleting by joining against it. Temp tables survive across the per-batch commits within the same session, so this fits the existing design.

Nice catch @nuclearcat . I have included a temp table to store ids before batch deleting.

  * Delete rows older than a given age with manual cascade (checkout -> builds -> tests)
  * --origins scoping, --dry-run and a --yes confirmation.
  Extract parse_interval into a shared helper and add integration tests.

  Closes kernelci#1969

Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
…tests. * Delete rows older than a given age with manual cascade (checkout -> builds -> tests) * --origins scoping, --dry-run and a --yes confirmation. Extract parse_interval into a shared helper and add integration tests.

    * Add tables parameter.
    * Protect issue related rows by default, and add parameter to
      bypass.

Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
…tests. * Delete rows older than a given age with manual cascade (checkout -> builds -> tests) * --origins scoping, --dry-run and a --yes confirmation. Extract parse_interval into a shared helper and add integration tests.

    * Add tables parameter.
    * Protect issue related rows by default, and add parameter to
      bypass.

Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
…tests. * Delete rows older than a given age with manual cascade (checkout -> builds -> tests) * --origins scoping, --dry-run and a --yes confirmation. Extract parse_interval into a shared helper and add integration tests.

    * Add tables parameter.
    * Protect issue related rows by default, and add parameter to
      bypass.

Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
return

if dry_run:
self.stdout.write(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the warning go to stderr instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

summary = ", ".join(f"{counts[t]} {t}" for t in selected_tables)
try:
answer = (
input(f"Delete {summary} ({total} rows total)? [y/N] ")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We already printed counts + total, can we make this shorter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. Changed to simple message to confirm.

self.stdout.write("Aborted.")
return

# Delete child-first (reverse of PRUNABLE_TABLES order): each batch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Document the strict parent-before-child order of PRUNABLE_TABLES at its definition

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment on lines +247 to +249
f"DELETE FROM {temp_table} WHERE id IN "
f"(SELECT id FROM {temp_table} LIMIT %(batch_size)s) RETURNING id"
f") DELETE FROM {table} WHERE id IN (SELECT id FROM batch)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

even we are filtering unknown tables from command args with PRUNABLE_TABLES I think we should go safer here and quote the identifiers. DELETE FROM "{table}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

  * Send dry-run warning to stderr
  * Shorten confirmation prompt now that counts are already printed
  * Document PRUNABLE_TABLES parent-before-child order
  * Quote table identifiers in prune SQL

Signed-off-by: Alan Peixinho <alan.peixinho@profusion.mobi>
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.

5 participants