Ahead of running this against a live site (not just staging bulk-imports),
addressed the two risks that only matter under concurrent traffic:
- One bad product no longer wastes a whole batch: each product save is
wrapped in its own SAVEPOINT within the batch transaction, so a failure
rolls back just that row (counted in the new failed[] result, surfaced
via WP_CLI::warning like import-gutenberg already does) instead of
discarding up to 200 already-good rows. Verified SAVEPOINT/ROLLBACK TO
SAVEPOINT actually works on this MariaDB instance via a direct $wpdb test
before relying on it.
- Cache invalidation suspension is now scoped per-batch instead of the
whole run, with clean_post_cache() called explicitly per product once
each batch commits, replacing the single end-of-run wp_cache_flush().
This site runs a shared Redis object cache across PHP-FPM workers, so a
whole-run suspension + full flush would let a concurrent visitor see
stale cached product data for the run's duration, and the flush itself
would evict sessions and everything else site-wide. Scoping bounds
staleness to one batch and targets only the products actually touched.
Also moved term resolution (term_exists()/wp_insert_term()) out of the
per-product savepoint entirely: warm_term_cache() now resolves every
distinct subject for a batch before its transaction opens, so a term
created for one product can never get silently rolled back by a *different*
product's savepoint failure while a stale term_id sits cached in memory
and gets attached to a later product (an orphaned term_relationships row,
checked for directly post-fix: 0 found).
Verified after: 2000-item sync still ~90s (savepoints add no measurable
overhead), 0 lookup-table mismatches, 0 term-count mismatches, 0 orphaned
term_relationships, idempotent re-run with 0 duplicates.