Skip to content

feat(free): add conflict-aware Undo and bounded history - #117

Merged
MrDarkRoot merged 4 commits into
mainfrom
free/110-conflict-aware-undo-history
Oct 1, 2026
Merged

MrDarkRoot merged 4 commits into
mainfrom
free/110-conflict-aware-undo-history

Conversation

@MrDarkRoot

@MrDarkRoot MrDarkRoot commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Fixes #110
Refs #106
Refs #107
Refs #108
Refs #109
Refs #111

Conflict-aware Undo for every journal-proven WriteLeash APPLIED item, plus bounded Free history, retention and lifecycle. Undo restores exactly one stored regular-price value through Woo CRUD only when fresh state still proves the applied value and post-apply fingerprint; otherwise typed UNDO_CONFLICT with zero overwrite. Undo is NOT rollback: no reversal of emails, webhooks, remote calls, orders or plugin side effects is claimed.

Undo identity/state model:

  • New durable tables writeleash_undo_operations (one row per apply job) and writeleash_undo_items (one row per eligible product, UNIQUE job/product). APPLY fact is never rewritten; Undo is a new durable action.
  • States: UNDO_PENDING/RUNNING/PAUSED/COMPLETED/COMPLETED_WITH_ISSUES/NEEDS_REVIEW/CANCELLED and per-item UNDO_PENDING/UNDO_APPLYING/UNDONE/UNDO_CONFLICT/UNDO_FAILED/UNDO_NEEDS_REVIEW. Illegal transitions rejected.

Eligibility/fingerprint:

  • Only journal-APPLIED + item-APPLIED rows; terminal COMPLETED/COMPLETED_WITH_ISSUES jobs only; fresh precondition (existence, core simple, publish, current==applied, sale/currency/decimals/versions, executor caps).
  • sha256 over 16 blocking fields from durable apply-time facts. ABA 80->70->80 is the documented residual limitation (value+context equality cannot prove it); post_modified_gmt researched against actual Woo 11.1.2 source (every save() bumps it, but 1s granularity, raw writers bypass, no apply-time record) and kept advisory-only.

Transaction/fence design:

  • Short per-item tx on the same Price_Apply_Connection; lock order undo-op row -> job row -> undo item -> journal -> Woo rows (apply order is a subsequence, no cycle). Undo_Transaction_Fence reuses the reviewed Price_Apply_Transaction_Guard point. Same-connection evidence recorded (fence==writer==Woo hook connection).
  • Apply/Undo mutual exclusion via terminal-only eligibility plus the shared job-row lock.

Crash matrix (real SIGKILL, both engines, default+Redis): kill before save / after save before journal / after journal before COMMIT all roll back to applied with Undo not UNDONE; kill after COMMIT recovers UNDONE with zero replay; lost acknowledgement -> UNDO_NEEDS_REVIEW, later adopted as ALREADY_UNDONE without save.

Partial Undo results: exact eligible/undone/conflict/failed/needs_review counts; UNDO_COMPLETED_WITH_ISSUES, never generic success. Duplicate workers: one restore save, one UNDONE.

History schema: backend recent-jobs/job/item models with apply+Undo counts, undo_eligible, undo_expires_at, typed reasons; bounded pagination; creator/approver/admin scoping.

Retention policy: 30 days (filterable), clock = terminal apply completed_at extended by Undo completion. Bounded keyset-paginated purge of terminal expired history only; active/paused/review/incomplete never purged. Exact COUNT(*) storage accounting.

Purge safety, deactivation/reactivation: in-flight items finish at the fence boundary; no new claims after deactivation; reactivation reconciles stale Undo leases without mutating products.

Uninstall race proof: uninstall.php removes exactly the owned option names (incl. 2 new Undo options; 13-option classifier updated in lockstep) and performs zero DDL by reviewed constraint, so tables/evidence survive a live worker; worker drains at its fence boundary; Woo rows, AS tables and Guard/Strict objects untouched; no auto-restore. Deliberate documented deviation: durable Free tables remain operator lifecycle (as #109 job tables), keeping the evidence-deletion kill condition structurally false.

External side-effect boundary: hook fixture proves apply 1 / Undo 1 / ALREADY_UNDONE 0; original markers never erased.

Explicit #111 non-goals: no Admin screens, preview wizard, history page, progress dashboard. One narrow POST-only start endpoint for tests/#111.

Verification: new wordpress-woo-undo workflow (MySQL 8.0.44 + MariaDB 10.11.15 x default + Redis 7.4.2, Woo 11.1.2, WP 7.1.2, PHP 8.2); existing #107/#108/#109/foundation/adapter suites re-ran green locally.

Do NOT merge. #106 remains OPEN. #110 remains OPEN until maintainer merge. #111 not started.


Repair round (audited blockers 1-4)

Audited head 234126a → repair head pushed on this branch. No #107/#108/#109 contract change; Woo CRUD, journal, fresh-observer, lease-generation and transactional fencing model preserved.

  • Blocker 1 — history item pagination over the complete logical set. Undo_Repository::history_items() no longer reads one 100-row apply page and slices/filters it in memory. It runs one identifier-qualified LEFT JOIN with apply_state/undo_state predicates in SQL, deterministic sequence ASC, product_id ASC, bounded limit <= 100, and exposes total/next_offset for the filtered set. Regression: a 170-item job pages 0/50/100/150 and 160,limit=100; Undo/apply-state matches seeded after item 100 are returned and paginated over the filtered logical set.
  • Blocker 2 — atomic per-job purge. purge_job_atomic() replaces the separate DELETEs: one short transaction per candidate, deterministic job-row-first lock order, eligibility re-evaluated from locked rows, reviewed logical delete order, every DELETE must affect exactly the locked row count, and any error/invariant/lost-transaction rolls the whole candidate back and reports purge_failed. Regression: ten deterministic failure-injection points (before/after each of the five deletion stages) each report purge_failed with a fresh-observer oracle proving job/job-items/journal/Undo-operation/Undo-items survive; a later clean purge removes the complete history.
  • Blocker 3 — purge serializes against Undo initiation. Global deterministic lock order is now parent apply job row → Undo operation row → Undo item row → apply job item/journal row → Woo rows. Initiation locks the job row first, re-evaluates terminal status/plan binding/retention under that lock and creates the operation/items in the same transaction; purge locks the same job row first and re-evaluates from locked rows. Regression: two real processes with a deterministic job-row-lock barrier in both orderings — (A) initiation wins, purge removes nothing and the intact evidence still completes the Undo; (B) purge wins completely, initiation refuses UNDO_NOT_ELIGIBLE, zero orphan Undo rows — no lock timeout, no partial deletion, no product mutation.
  • Blocker 4 — explicit uninstall decision (no silent change). DDL-free uninstall is kept deliberately. The contract now distinguishes deactivation, uninstall option cleanup, durable table retention and later retention purge/operator lifecycle, and explicitly states that issue WriteLeash Free: implement conflict-aware Undo, history retention and lifecycle #110's candidate "remove plugin-owned tables on uninstall" wording is superseded and must not be claimed as satisfied (maintainer decides whether to amend the issue text).

Verification at the repaired head: conflict-aware Undo matrix on MySQL 8.0.44 + MariaDB 10.11.15 × default + Redis 7.4.2 (Woo 11.1.2 / WP 7.1.2 / PHP 8.2) green; #107/#108/#109, foundation (both engines), Redirection adapter, engine research, MySQL/MariaDB feasibility, Native PG16 suite and the V0.1 release matrix re-run green.


Lifecycle repair round (fail-closed authority, owned-only scheduler cleanup)

Repair head dad6853. No #107/#108/#109/#110 domain semantics changed; only the lifecycle gate is tightened.

  • Fail-closed runner authority. Job_Worker::runner_active() and Undo_Worker::runner_active() now authorize only an explicit durable active value (return 'active' === $value;) and document the intentional fail-closed behavior. Missing (including after uninstall deletes the option), empty, deactivated, malformed and unknown values all stop a worker before its next claim; an item transaction that already acquired its authoritative fence may still finish that one boundary.
  • Uninstall cancels only owned wake-ups. uninstall.php now fail-closes the runner gate first, then cancels exactly the three owned Action Scheduler groups (writeleash-jobs, writeleash-undo, writeleash-maintenance) through the public ActionScheduler::store()->cancel_actions_by_group() API. Unrelated groups/actions and all AS tables are untouched; if AS is unavailable the cleanup is skipped safely and the lifecycle gate still stops new claims. Uninstall stays DDL-free and never drops durable Free tables.
  • Real multi-item uninstall races. Both worker families are exercised with real separate processes and two items: item 1 holds its authoritative transaction boundary, uninstall runs while it is in-flight, and after release item 1 completes exactly once while the lifecycle gate blocks item 2. Fresh observers prove Undo item 1 UNDONE / item 2 UNDO_PENDING with zero second save, and Apply item 1 APPLIED / item 2 PENDING with the journal still PENDING.
  • Explicit missing/malformed-state coverage. Real Apply and Undo worker loops are run for absent, '', deactivated and an unknown value, each producing zero claims, zero Woo saves, zero price mutation and typed DEACTIVATED pauses.
  • Issue authority. Issue WriteLeash Free: implement conflict-aware Undo, history retention and lifecycle #110's lifecycle/test/acceptance wording was amended in place to match the reviewed DDL-free table-retention decision (retained tables, fail-closed runner authority, owned-only AS cancellation, real multi-item races).
  • Stale comment fixed in Woo_Undo_Mutator (lock order is now job row -> Undo operation row).
  • Storage claim corrected. The contract now claims only the measured 100-item storage evidence and explicitly defers 1000-item scale certification to WriteLeash Free: prove scale, compatibility, persistent-cache and real-host acceptance #112.

Final lifecycle linearization repair (d0fd124)

The existing indexed writeleash_runner_state option remains the only lifecycle authority. A claim now runs in a short same-handle transaction: LOCK IN SHARE MODE on the existing InnoDB option_name unique-index row, lock/verify live lease generation (apply job; Undo parent job then operation), then conditional item UPDATE with both lease EXISTS and option_value='active' EXISTS. The independent Woo mutation transaction takes that same shared lifecycle record lock before job/Undo/journal/Woo locks and holds it through COMMIT/ROLLBACK. Deactivation's direct indexed UPDATE and uninstall's first indexed DELETE require an exclusive lock; a missing row is never treated as a lock/authority. Uninstall verifies the active row is gone before touching further evidence or AS. Global order: lifecycle -> parent apply job -> Undo operation -> item/journal -> Woo. Initiation/purge remain job-first without holding lifecycle, preserving their audited order.

A claimed item whose mutation loses lifecycle authority returns typed DEACTIVATED, is CAS-released to PENDING with claim token cleared and attempt count refunded, and pauses its job/operation; journal remains PENDING and zero Woo save occurs. Lease-generation loss remains FENCE_LOST, and unknown outcomes still require review. Chunk-end checks retain review truth when shutdown and budget expiry overlap. The sole adjacent #108 correction detects a coherent different stored regular/active/lookup price as a typed optimistic CONFLICT when a concurrent Woo object read lags storage; malformed/lookup-divergent evidence remains NEEDS_REVIEW and no save or direct Woo SQL is introduced. Undo RUNNING -> PENDING is an explicit legal bounded-chunk transition (existing worker path).

Deterministic real-process A/B/C races for both Apply and Undo cover: active PHP read followed by uninstall before claim (zero claim); durable claim followed by uninstall/deactivation before mutation (refund, zero save); and already-fenced item transaction followed by uninstall (indexed shutdown DELETE visibly waits until item COMMIT, then next item blocked). Tests use an independent fresh DB observer, FOR UPDATE NOWAIT and live process-list SQL-wait evidence on MySQL 8.0.44 and MariaDB 10.11.15, default and Redis caches. The previously audited multi-item uninstall, purge/Undo initiation, pagination, atomic purge, #109 takeover and owned-only AS suites remain green. No table drops and no #111 work.

@MrDarkRoot
MrDarkRoot merged commit d82d3dd into main Oct 1, 2026
12 of 14 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.

WriteLeash Free: implement conflict-aware Undo, history retention and lifecycle

1 participant