Skip to content

fix: delete expired presence rows by key in bounded passes - #258

Open
aldorizona10-glitch wants to merge 2 commits into
WordPress:mainfrom
aldorizona10-glitch:fix/206-bounded-portable-cleanup
Open

fix: delete expired presence rows by key in bounded passes#258
aldorizona10-glitch wants to merge 2 commits into
WordPress:mainfrom
aldorizona10-glitch:fix/206-bounded-portable-cleanup

Conversation

@aldorizona10-glitch

Copy link
Copy Markdown
Contributor

What & why

Resolves #206. wp_delete_expired_presence_data() (includes/functions.php) batched deletions with a MySQL-only DELETE ... LIMIT inside a loop that had no iteration ceiling:

do {
	$deleted = $wpdb->query(
		$wpdb->prepare( "DELETE FROM {$wpdb->presence} WHERE date_gmt < %s LIMIT 1000", $cutoff )
	);
} while ( $deleted > 0 );

Two divergences from core, both from #206:

  1. PortabilityDELETE ... LIMIT is MySQL-specific and appears nowhere in wp-includes, so it breaks on non-MySQL backends, including the SQLite integration Playground uses to run the demo blueprints.
  2. Unbounded work — a site returning from a cron outage with a large backlog keeps looping until max_execution_time, committing an unpredictable fraction of the work.

Change

Follows core's batching convention — select a bounded set of primary keys, delete by key, a fixed number of passes per invocation, remainder to the next run:

  • Each pass SELECTs a page of ids older than the cutoff (LIMIT %d) and deletes them with a portable WHERE id IN ( ... ) built from %d placeholders passed through prepare().
  • A fixed max passes per invocation bounds the work; the cron event fires every minute, so any remaining backlog drains over subsequent runs instead of in one long request.
  • Batch size (default 1000) and passes per run (default 10) are filterable via wp_presence_cleanup_batch_size and wp_presence_cleanup_max_passes.

No schema change; behavior on a normally-drained table is unchanged.

Tests

Added to tests/test-functions.php (@covers ::wp_delete_expired_presence_data):

  • test_cleanup_is_bounded_per_invocation — with batch_size and max_passes filtered to 2, seeds 5 expired rows and asserts one invocation deletes at most 2 × 2 = 4, leaving the remainder; a second invocation clears it. This locks in the "remainder to the next run" guarantee.
  • test_cleanup_leaves_fresh_entries_untouched — fresh rows survive cleanup even with a batch size of one.

The existing test_cleanup_removes_expired_entries continues to pass.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Investigating core's batching convention, implementing the fix, and writing tests

wp_delete_expired_presence_data() looped over a MySQL-only
`DELETE ... LIMIT` with no iteration ceiling. Two problems (WordPress#206):

1. `DELETE ... LIMIT` is MySQL-specific and appears nowhere in
   wp-includes, so it is a portability hazard for non-MySQL backends,
   including the SQLite integration Playground uses for the demo
   blueprints.
2. The loop had no upper bound. A site returning from a cron outage with
   a large backlog would keep looping until max_execution_time, having
   committed an unpredictable fraction of the work.

Follow core's batching convention: select a bounded page of primary keys
older than the cutoff and delete them by key (portable `IN (...)`), for a
fixed number of passes per invocation. The remainder is left for the next
cron run (the event fires every minute), so a single request cannot run
unbounded. Batch size (default 1000) and passes per run (default 10) are
filterable via wp_presence_cleanup_batch_size / wp_presence_cleanup_max_passes.

Adds tests covering the per-invocation bound (remainder carried to the
next run) and that fresh entries are never removed.

Fixes WordPress#206

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Investigating core's batching convention, implementing the fix, and writing tests
@github-actions

Copy link
Copy Markdown
Contributor

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props aldorza, joefusco.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

▶ Preview in WordPress Playground

Open in WordPress Playground

Boots a fresh WordPress with this PR's presence-api build, seeds 5 demo users, and drops you on the dashboard.

Stress-test variant: 40 demo users · Built from 30a8cfa3048d107ee09a09084398869a36930a56. Auto-updates when you push.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 72.63%. Comparing base (bfb701f) to head (30a8cfa).

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #258      +/-   ##
============================================
+ Coverage     72.51%   72.63%   +0.12%     
  Complexity      175      175              
============================================
  Files            14       14              
  Lines          1819     1831      +12     
============================================
+ Hits           1319     1330      +11     
- Misses          500      501       +1     
Flag Coverage Δ
multisite 72.63% <93.75%> (+0.12%) ⬆️
phpunit 71.32% <93.75%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The interpolated %d placeholder list trips WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, not ReplacementsWrongNumber; suppress the correct code.
@josephfusco josephfusco added [Type] Bug An existing feature is broken [Area] Database Issues for the wp_presence table and cron cleanup Performance Work relates to query load, cache behavior, or scaling php Pull requests that update php code labels Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Database Issues for the wp_presence table and cron cleanup Performance Work relates to query load, cache behavior, or scaling php Pull requests that update php code [Type] Bug An existing feature is broken

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expired-data cleanup uses MySQL-only DELETE ... LIMIT in an uncapped loop

2 participants