From 4e75aaf0d361b7f65f1c41cfea388174d93b5a0c Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Tue, 1 Sep 2026 03:16:22 +0800 Subject: [PATCH] fix(runtime): restore overlay retry queue --- etaf-runtime.el | 26 ++++++++++--- tests/etaf-scheduler-tests.el | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/etaf-runtime.el b/etaf-runtime.el index 2a5f20d..fc0d203 100644 --- a/etaf-runtime.el +++ b/etaf-runtime.el @@ -5734,6 +5734,16 @@ RENDERED-IDENTITIES names the Component render participants." (setq owner (etaf--semantic-component-identity semantic)))))) owner)) +(defun etaf--runtime-restore-component-overlay-retry + (runtime retry-effect-ids) + "Restore failed RUNTIME overlay work for RETRY-EFFECT-IDS." + (etaf--runtime-clear-dirty-effects runtime) + (dolist (effect-id retry-effect-ids) + (etaf--runtime-enqueue-effect runtime effect-id)) + (etaf--runtime-dispose-created-candidate runtime) + (etaf--runtime-rollback-behaviors runtime) + (etaf--runtime-clear-candidate runtime)) + (defun etaf--runtime-component-overlay (runtime) "Publish RUNTIME dirty Component effects without traversing Root." (cl-block etaf--runtime-component-overlay @@ -5749,7 +5759,10 @@ RENDERED-IDENTITIES names the Component render participants." backend-component-identities journal resource-journal candidate-generation semantic-candidate participant) - (etaf--runtime-begin-component-overlay runtime old) + (let ((evaluation-completed-p nil)) + (unwind-protect + (progn + (etaf--runtime-begin-component-overlay runtime old) ;; A source notification can race a generation promotion and leave an old ;; effect id in the FIFO. Keep the dirty-id set and FIFO coherent before ;; sorting; stale ids are retired by the candidate generation cleanup. @@ -5844,6 +5857,10 @@ RENDERED-IDENTITIES names the Component render participants." (cl-pushnew effect-id (etaf-runtime-candidate-removed-effect-ids runtime) :test #'eql)))) + (setq evaluation-completed-p t)) + (unless evaluation-completed-p + (etaf--runtime-restore-component-overlay-retry + runtime retry-effect-ids)))) (setq range-changes (append (nreverse (etaf-runtime-candidate-eager-range-changes runtime)) @@ -6044,11 +6061,8 @@ RENDERED-IDENTITIES names the Component render participants." semantic-candidate)) (etaf--runtime-rollback-prearm runtime journal) (etaf--runtime-rollback-resource-journal runtime resource-journal) - (etaf--runtime-clear-dirty-effects runtime) - (dolist (effect-id retry-effect-ids) - (etaf--runtime-enqueue-effect runtime effect-id)) - (etaf--runtime-dispose-created-candidate runtime) - (etaf--runtime-clear-candidate runtime) + (etaf--runtime-restore-component-overlay-retry + runtime retry-effect-ids) (signal (car err) (cdr err)))) (etaf--runtime-participant-commit participant) (etaf--runtime-complete-generation runtime candidate-generation) diff --git a/tests/etaf-scheduler-tests.el b/tests/etaf-scheduler-tests.el index f8a0370..f67d2d5 100644 --- a/tests/etaf-scheduler-tests.el +++ b/tests/etaf-scheduler-tests.el @@ -14,6 +14,8 @@ "ETAF scheduler projection test condition") (define-error 'etaf-scheduler-test-body-condition "ETAF scheduler body test condition") +(define-error 'etaf-scheduler-test-render-recovery-condition + "ETAF scheduler render recovery condition") (etaf-define-component etaf-scheduler-test-pair (&key left right) "Render reactive LEFT and RIGHT values." @@ -43,6 +45,16 @@ (signal 'etaf-scheduler-test-projection-condition '("projection"))) (etaf-node 'text nil (list (symbol-name status))))) +(etaf-define-component etaf-scheduler-test-render-recovery + (&key source fail) + "Render SOURCE unless FAIL requests a deterministic render error." + :render + (progn + (when (etaf-value fail) + (signal 'etaf-scheduler-test-render-recovery-condition + '("expected render failure" :payload recovery))) + (etaf-node 'text nil (list (format "Value %s" (etaf-value source)))))) + (defun etaf-scheduler-test--text (buffer-name) "Return BUFFER-NAME text without properties." (with-current-buffer buffer-name @@ -494,6 +506,66 @@ (etaf-value (etaf-data-error controller))))) (etaf-data-stop controller)))) +(ert-deftest etaf-scheduler-render-failure-keeps-retry-fifo-coherent () + "A failed Component evaluation retries and visibly recovers on new state." + (let* ((buffer-name " *etaf-scheduler-render-recovery*") + (source (etaf-ref "A")) + (fail (etaf-ref nil)) + captured) + (unwind-protect + (progn + (etaf-mount + buffer-name + (etaf--view-call + 'etaf-scheduler-test-render-recovery + (list :source source :fail fail) nil)) + (setf (etaf-value source) "B") + (let* ((runtime (etaf-runtime-for-buffer buffer-name)) + (generation (etaf-runtime-generation runtime)) + (revision + (plist-get (ebox-buffer-update-report buffer-name) + :surface-revision)) + (contents + (with-current-buffer buffer-name (buffer-string)))) + (condition-case condition + (setf (etaf-value fail) t) + (etaf-scheduler-test-render-recovery-condition + (setq captured condition))) + (should + (equal captured + '(etaf-scheduler-test-render-recovery-condition + "expected render failure" :payload recovery))) + (should (= generation (etaf-runtime-generation runtime))) + (should (= revision + (plist-get (ebox-buffer-update-report buffer-name) + :surface-revision))) + (should (equal-including-properties + contents (with-current-buffer buffer-name + (buffer-string)))) + (should + (equal (sort (hash-table-keys + (etaf-runtime-dirty-effect-ids runtime)) #'<) + (sort (copy-sequence + (etaf-runtime-dirty-effect-queue runtime)) #'<))) + (etaf-reactive-call-with-batch + (lambda () + (setf (etaf-value fail) nil) + (setf (etaf-value source) "C"))) + (should (equal "Value C" + (etaf-scheduler-test--text buffer-name))) + (should (= (1+ generation) + (etaf-runtime-generation runtime))) + (should (= (1+ revision) + (plist-get (ebox-buffer-update-report buffer-name) + :surface-revision))) + (should-not (hash-table-keys + (etaf-runtime-dirty-effect-ids runtime))) + (should-not (etaf-runtime-dirty-effect-queue runtime)))) + (when-let* ((runtime (etaf-runtime-for-buffer buffer-name))) + (etaf-unmount runtime)) + (when-let* ((buffer (get-buffer buffer-name))) + (kill-buffer buffer))))) + (ert-deftest etaf-scheduler-detached-route-does-not-enter-fan-out () "An invalidated Runtime route cannot enqueue work in its old context." (let* ((buffer-name " *etaf-scheduler-stale-route*")