Skip to content

fix(sync): defer audio deletion to VIP cron and shorten DELETE timeout#570

Merged
galbus merged 1 commit into
mainfrom
claude/jovial-shamir-d239bd
Jul 19, 2026
Merged

fix(sync): defer audio deletion to VIP cron and shorten DELETE timeout#570
galbus merged 1 commit into
mainfrom
claude/jovial-shamir-d239bd

Conversation

@galbus

@galbus galbus commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Problem

Sync::on_trash_post() (wp_trash_post) and Sync::on_delete_post() (before_delete_post) each made a blocking Client::delete_audio() call — wp_remote_request() with 'timeout' => 30 — with no async/deferred path. Unlike audio generation (deliberately deferred to cron on VIP via is_async_generation_enabled()), deletion always ran inline on the admin request.

Bulk-trashing N posts with BeyondWords content fired wp_trash_post per post → N sequential 30s-worst-case remote DELETEs in a single admin request. Against a slow/unreachable API this easily exceeds VIP request limits and can hang or fatal the request mid-loop, leaving posts partially processed. It's also a VIP platform violation (blocking remote request with a long timeout on a synchronous path).

Fix

Mirror the generation design, plus give the DELETE a short timeout.

src/post/class-sync.php — deferred deletion on VIP

  • New DELETE_AUDIO_CRON_HOOK (beyondwords_delete_audio), registered unconditionally like the generation hook so a queued event still runs after a config change.
  • on_trash_post() / on_delete_post() now call a private delete_audio_for_post_or_defer(). When is_async_generation_enabled() (VIP), it captures the project + content IDs and schedules a single background cron event — the IDs travel in the event args because the meta is wiped on trash / the row is gone on permanent delete before the job runs. Off-VIP (unreliable WP-Cron), it falls back to a synchronous DELETE.
  • schedule_audio_deletion() mirrors schedule_audio_generation() (prefers the VIP wrapper, de-dupes via wp_next_scheduled). Sync::delete_audio_by_ids() is the cron callback.
  • Bulk trash on VIP now just schedules N cheap events (no network in the request); the deletes run in the background — resolving the N×30s blocking problem.

src/api/class-client.php — short, parametrized timeout

  • Added REQUEST_TIMEOUT = 30 (generation/reads, unchanged) and DELETE_TIMEOUT = 3.
  • build_args() / call_api() take an optional $timeout; DELETEs pass the short one. delete_audio() now delegates to a new delete_audio_by_ids($project_id, $content_id, $post_id), shared by the sync path and the cron callback. Off-VIP bulk trash is now bounded by 3s per post instead of 30s.

Reviewer notes

  • No phpcs:ignore added — the existing one was removed. Because build_args() now uses 'timeout' => $timeout (a variable, not a literal 30), the VIP RemoteRequestTimeout sniff no longer fires, so its phpcs:ignore is gone.
  • Async gating reuses is_async_generation_enabled() (and its beyondwords_async_generate_audio filter) intentionally — the underlying question, "is background cron reliable here?", is identical for deletion.
  • No nonce/capability/sanitization changes needed: these are internal post-lifecycle hooks (WP core already gates trash/delete with caps + nonce), and the project/content IDs come from our own post meta, not request input.
  • Behaviour is preserved on edge cases — e.g. a post with a content ID but no resolvable project ID still wipes local meta without an API call, exactly as before.
  • The beyondwords_delete_content editor path and batch_delete_audio (posts-list bulk action) are out of scope; the former still runs synchronously but now benefits from the shorter DELETE timeout.

Verification

  • phpcs: full .phpcs.xml (WordPress-VIP-Go) ruleset — clean, exit 0.
  • PHPUnit: SyncTest + ClientTest121 tests, 303 assertions, all pass, including 6 new tests (defer-on-VIP for trash & delete, sync-path-off-VIP asserting DELETE_TIMEOUT, Sync::delete_audio_by_ids delegation, Client-level short-timeout + missing-ID coverage).
  • Did not run the full Cypress suite.

🤖 Generated with Claude Code

on_trash_post() and on_delete_post() each made a blocking 30s remote
DELETE with no async path. Bulk-trashing N posts fired N sequential
30s-worst-case DELETEs in one admin request, a WordPress VIP
violation that could hang or fatal the request mid-loop.

Mirror the generation design: when is_async_generation_enabled()
(VIP), capture the project and content IDs and schedule one
background DELETE_AUDIO_CRON_HOOK event. The IDs travel in the event
args because the meta is wiped on trash (and the row is gone on
delete) before the job runs. Off-VIP, fall back to a synchronous
DELETE bounded by a new short DELETE_TIMEOUT (3s) instead of 30s.

Parametrize Client::build_args() and call_api() with a timeout and
add Client::delete_audio_by_ids(); delete_audio() delegates to it.
The timeout is now a variable, so the VIP RemoteRequestTimeout sniff
no longer fires and the existing phpcs:ignore is removed (none added).

Verified: full phpcs ruleset clean; SyncTest and ClientTest pass
(121 tests, 303 assertions, 6 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

@galbus
galbus marked this pull request as ready for review July 19, 2026 06:56
@galbus
galbus requested a review from gouravkhunger July 19, 2026 06:56
@galbus
galbus merged commit d3fd9ae into main Jul 19, 2026
12 checks passed
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