fix(runtime): preserve scheduler work and selected publication
This commit is contained in:
parent
de622c263f
commit
b82f09aa79
@ -18,6 +18,7 @@
|
||||
(require 'etaf-component)
|
||||
(require 'etaf-context)
|
||||
(require 'etaf-behavior)
|
||||
(require 'etaf-render-port)
|
||||
|
||||
(declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path))
|
||||
|
||||
@ -843,7 +844,10 @@ OPTIONS is forwarded to `etaf-runtime-mount' when the Runtime is loaded."
|
||||
(etaf-runtime-mount buffer-or-name view options)
|
||||
(let ((ebox-viewport-width (plist-get options :viewport-width))
|
||||
(ebox-viewport-height (plist-get options :viewport-height)))
|
||||
(ebox-render-to-buffer buffer-or-name (etaf-render view)))))
|
||||
(let ((input (etaf-render view))
|
||||
(buffer (get-buffer-create buffer-or-name)))
|
||||
(etaf-render-port-initial buffer input #'ignore #'ignore)
|
||||
buffer))))
|
||||
|
||||
(provide 'etaf-renderer)
|
||||
|
||||
|
||||
@ -427,13 +427,16 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
||||
first-condition))
|
||||
|
||||
(defun etaf-scheduler--record-fault
|
||||
(context projection-id condition &optional phase)
|
||||
"Record CONTEXT failure CONDITION for PROJECTION-ID and optional PHASE."
|
||||
(context projection-id condition &optional phase turn-id)
|
||||
"Record CONTEXT failure CONDITION for PROJECTION-ID.
|
||||
PHASE is optional diagnostic metadata. TURN-ID, when non-nil, preserves the
|
||||
turn where a contained condition originated before delayed quarantine."
|
||||
(setf (etaf-scheduler-context-fault-state context) (copy-tree condition))
|
||||
(cl-incf (etaf-scheduler-context-projection-fault-count context))
|
||||
(push (append (list :projection-epoch projection-id
|
||||
:turn-id
|
||||
(etaf-scheduler-context-active-turn-id context))
|
||||
(or turn-id
|
||||
(etaf-scheduler-context-active-turn-id context)))
|
||||
(and phase (list :phase phase))
|
||||
(list :condition (copy-tree condition)))
|
||||
(etaf-scheduler-context-diagnostics context))
|
||||
@ -459,8 +462,9 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
||||
"Drain PROJECTION source-first and re-signal its first context fault."
|
||||
(let ((projection-id (etaf-scheduler--projection-id projection))
|
||||
(failed-contexts (make-hash-table :test #'eq))
|
||||
(contained-context-conditions (make-hash-table :test #'eq))
|
||||
(context-steps (make-hash-table :test #'eq))
|
||||
first-condition)
|
||||
first-condition fatal-condition)
|
||||
(while (etaf-scheduler--projection-work-p projection)
|
||||
(let ((marked-contexts (make-hash-table :test #'eq)))
|
||||
;; All source propagation across every context settles before any
|
||||
@ -482,11 +486,19 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
||||
(when-let* ((source-condition
|
||||
(etaf-scheduler--drain-context-sources
|
||||
context projection-id)))
|
||||
(signal (car source-condition)
|
||||
(cdr source-condition))))
|
||||
(unless (gethash context contained-context-conditions)
|
||||
(puthash context
|
||||
(list
|
||||
:condition source-condition
|
||||
:turn-id
|
||||
(etaf-scheduler-context-active-turn-id
|
||||
context))
|
||||
contained-context-conditions))
|
||||
(unless first-condition
|
||||
(setq first-condition source-condition))))
|
||||
((error quit)
|
||||
(puthash context t failed-contexts)
|
||||
(unless first-condition (setq first-condition condition))
|
||||
(unless fatal-condition (setq fatal-condition condition))
|
||||
(etaf-scheduler--record-fault
|
||||
context projection-id condition)
|
||||
(etaf-scheduler--reset-pending context)))))))
|
||||
@ -502,14 +514,35 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
||||
(when-let* ((runtime-condition
|
||||
(etaf-scheduler--run-runtime-turn
|
||||
context turn)))
|
||||
(signal (car runtime-condition)
|
||||
(cdr runtime-condition))))
|
||||
(unless (gethash context contained-context-conditions)
|
||||
(puthash context
|
||||
(list
|
||||
:condition runtime-condition
|
||||
:turn-id
|
||||
(etaf-scheduler-context-active-turn-id
|
||||
context))
|
||||
contained-context-conditions))
|
||||
(unless first-condition
|
||||
(setq first-condition runtime-condition))))
|
||||
((error quit)
|
||||
(puthash context t failed-contexts)
|
||||
(unless first-condition (setq first-condition condition))
|
||||
(unless fatal-condition (setq fatal-condition condition))
|
||||
(etaf-scheduler--record-fault
|
||||
context projection-id condition)
|
||||
(etaf-scheduler--reset-pending context))))))
|
||||
;; Item-level source/subscriber/Runtime faults are reported only after
|
||||
;; the detached Runtime turn has consumed work enqueued by successful
|
||||
;; tail callbacks. Scheduler-control faults above still reset at once.
|
||||
(dolist (context (etaf-scheduler--projection-contexts projection))
|
||||
(when-let* ((contained
|
||||
(gethash context contained-context-conditions))
|
||||
(condition (plist-get contained :condition)))
|
||||
(unless (gethash context failed-contexts)
|
||||
(puthash context t failed-contexts)
|
||||
(etaf-scheduler--record-fault
|
||||
context projection-id condition nil
|
||||
(plist-get contained :turn-id))
|
||||
(etaf-scheduler--reset-pending context))))
|
||||
(dolist (context (etaf-scheduler--projection-contexts projection))
|
||||
(setf (etaf-scheduler-context-phase context) nil)
|
||||
(unless (gethash context failed-contexts)
|
||||
@ -519,8 +552,8 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
||||
(setf (etaf-scheduler-context-completed-projection-epoch context)
|
||||
projection-id
|
||||
(etaf-scheduler-context-fault-state context) nil)))
|
||||
(when first-condition
|
||||
(signal (car first-condition) (cdr first-condition)))))
|
||||
(when-let* ((condition (or fatal-condition first-condition)))
|
||||
(signal (car condition) (cdr condition)))))
|
||||
|
||||
(defun etaf-scheduler--finish-projection (projection)
|
||||
"Restore PROJECTION invariants and return contained finalizer errors."
|
||||
|
||||
@ -459,6 +459,56 @@
|
||||
(should (string-match-p "(etaf-render-port-update" source))
|
||||
(should-not (string-match-p "(ebox-commit" source)))))
|
||||
|
||||
(ert-deftest etaf-render-port-owns-all-live-publication-routing ()
|
||||
"No ETAF module except the selected-port owner publishes directly to Ebox."
|
||||
(dolist (file (directory-files etaf-render-port-test--root t "\\.el\\'"))
|
||||
(unless (string= (file-name-nondirectory file) "etaf-render-port.el")
|
||||
(with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(let ((source (buffer-string)))
|
||||
(dolist (call '("(ebox-render-to-buffer"
|
||||
"(ebox-commit"
|
||||
"(ebox-surface-"))
|
||||
(should-not (string-match-p (regexp-quote call) source))))))))
|
||||
|
||||
(ert-deftest etaf-render-port-routes-standalone-renderer-mount ()
|
||||
"The no-Runtime compatibility mount uses the immutable selected port."
|
||||
(require 'etaf-renderer)
|
||||
(let ((buffer (generate-new-buffer " *etaf-standalone-port-mount*"))
|
||||
seen-input seen-viewport)
|
||||
(unwind-protect
|
||||
(cl-letf (((symbol-function 'etaf-runtime-mount) nil)
|
||||
((symbol-function 'etaf-render-port-initial)
|
||||
(lambda (target input framework-stage framework-rollback
|
||||
&optional _observer)
|
||||
(setq seen-input input
|
||||
seen-viewport
|
||||
(list ebox-viewport-width ebox-viewport-height))
|
||||
(funcall framework-stage nil)
|
||||
(ignore framework-rollback)
|
||||
(list :status 'success :buffer target))))
|
||||
(should
|
||||
(eq (etaf-mount
|
||||
buffer (etaf-view (text "standalone"))
|
||||
'(:viewport-width 91 :viewport-height 17))
|
||||
buffer))
|
||||
(should (ebox-canonical-input-p seen-input))
|
||||
(should (equal seen-viewport '(91 17))))
|
||||
(when (and (buffer-live-p buffer)
|
||||
(ebox-surface-buffer-mounted-p buffer))
|
||||
(ebox-unmount-buffer buffer))
|
||||
(when (buffer-live-p buffer) (kill-buffer buffer)))))
|
||||
|
||||
(ert-deftest etaf-render-port-standalone-render-failure-creates-no-buffer ()
|
||||
"Pure lowering must fail before standalone mount allocates its target buffer."
|
||||
(require 'etaf-renderer)
|
||||
(let ((name " *etaf-standalone-render-failure*"))
|
||||
(when-let* ((buffer (get-buffer name))) (kill-buffer buffer))
|
||||
(cl-letf (((symbol-function 'etaf-runtime-mount) nil))
|
||||
(should-error (etaf-mount name 'invalid-etaf-view)
|
||||
:type 'etaf-renderer-error))
|
||||
(should-not (get-buffer name))))
|
||||
|
||||
(ert-deftest etaf-render-port-selected-port-is-process-stable ()
|
||||
"Every downstream read returns the one bootstrap-selected port identity."
|
||||
(should (eq (etaf-render-port-selected)
|
||||
|
||||
@ -269,7 +269,7 @@
|
||||
(ert-deftest etaf-scheduler-source-fault-does-not-suppress-context-tail ()
|
||||
"A failing source delivery cannot discard later sources in the same turn."
|
||||
(let ((context (etaf-scheduler-context-create :name 'source-fault-tail))
|
||||
second-ran captured)
|
||||
second-ran runtime-ran captured)
|
||||
(condition-case condition
|
||||
(etaf-scheduler-call-with-projection
|
||||
(lambda ()
|
||||
@ -280,12 +280,82 @@
|
||||
(etaf-scheduler-enqueue-source
|
||||
context 'second
|
||||
(lambda (_context _source _projection-id)
|
||||
(setq second-ran t)))))
|
||||
(setq second-ran t)
|
||||
(etaf-scheduler-enqueue-runtime
|
||||
context 'second-runtime
|
||||
(lambda () (setq runtime-ran t)))))))
|
||||
(etaf-scheduler-test-source-condition (setq captured condition)))
|
||||
(should (equal captured
|
||||
'(etaf-scheduler-test-source-condition "first")))
|
||||
(should second-ran)
|
||||
(should runtime-ran)
|
||||
(should (= 2 (etaf-scheduler-test--metric context :source-deliveries)))
|
||||
(should (= 1 (etaf-scheduler-test--metric context :runtime-executions)))
|
||||
(should (etaf-scheduler-context-idle-p context))))
|
||||
|
||||
(ert-deftest etaf-scheduler-control-fault-precedes-contained-item-fault ()
|
||||
"A later fatal budget fault outranks an earlier contained source condition."
|
||||
(let ((context
|
||||
(etaf-scheduler-context-create
|
||||
:name 'control-fault-precedence :fixed-point-step-budget 1))
|
||||
runtime-ran captured summary)
|
||||
(let ((etaf-scheduler-projection-observer
|
||||
(lambda (value) (setq summary value))))
|
||||
(condition-case condition
|
||||
(etaf-scheduler-call-with-projection
|
||||
(lambda ()
|
||||
(etaf-scheduler-enqueue-source
|
||||
context 'item-fault
|
||||
(lambda (_context _source _projection-id)
|
||||
(signal 'etaf-scheduler-test-source-condition '("item"))))
|
||||
(cl-labels
|
||||
((deliver (_context source _projection-id)
|
||||
(etaf-scheduler-enqueue-source
|
||||
context (1+ source) #'deliver)))
|
||||
(etaf-scheduler-enqueue-source context 0 #'deliver))
|
||||
(etaf-scheduler-enqueue-runtime
|
||||
context 'runtime (lambda () (setq runtime-ran t)))))
|
||||
((error quit) (setq captured condition))))
|
||||
(should (eq (car captured) 'etaf-scheduler-error))
|
||||
(should (eq (plist-get (cdr captured) :kind)
|
||||
'fixed-point-step-budget))
|
||||
(should (= (plist-get (cdr captured) :steps) 2))
|
||||
(should (= (plist-get (cdr captured) :budget) 1))
|
||||
(should-not runtime-ran)
|
||||
(should (equal (etaf-scheduler-context-fault-state context) captured))
|
||||
(should (equal (plist-get summary :condition) captured))
|
||||
(should (etaf-scheduler-context-idle-p context))))
|
||||
|
||||
(ert-deftest etaf-scheduler-contained-fault-keeps-originating-turn-id ()
|
||||
"Delayed item-fault quarantine records the source turn, not Runtime turn."
|
||||
(let ((context (etaf-scheduler-context-create :name 'fault-origin-turn))
|
||||
runtime-ran captured)
|
||||
(condition-case condition
|
||||
(etaf-scheduler-call-with-projection
|
||||
(lambda ()
|
||||
(etaf-scheduler-enqueue-source
|
||||
context 'item-fault
|
||||
(lambda (_context _source _projection-id)
|
||||
(signal 'etaf-scheduler-test-source-condition '("origin"))))
|
||||
(cl-labels
|
||||
((deliver (_context source _projection-id)
|
||||
(when (zerop source)
|
||||
(etaf-scheduler-enqueue-source
|
||||
context 1 #'deliver))))
|
||||
(etaf-scheduler-enqueue-source context 0 #'deliver))
|
||||
(etaf-scheduler-enqueue-runtime
|
||||
context 'runtime (lambda () (setq runtime-ran t)))))
|
||||
(etaf-scheduler-test-source-condition (setq captured condition)))
|
||||
(should (equal captured
|
||||
'(etaf-scheduler-test-source-condition "origin")))
|
||||
(should runtime-ran)
|
||||
(should (= (etaf-scheduler-context-active-turn-id context) 2))
|
||||
(let ((diagnostic
|
||||
(cl-find captured (etaf-scheduler-context-diagnostics context)
|
||||
:key (lambda (entry) (plist-get entry :condition))
|
||||
:test #'equal)))
|
||||
(should diagnostic)
|
||||
(should (= (plist-get diagnostic :turn-id) 1)))
|
||||
(should (etaf-scheduler-context-idle-p context))))
|
||||
|
||||
(ert-deftest etaf-scheduler-subscriber-fault-does-not-suppress-group-tail ()
|
||||
@ -293,6 +363,7 @@
|
||||
(let* ((context
|
||||
(etaf-scheduler-context-create :name 'subscriber-fault-tail))
|
||||
second-ran
|
||||
runtime-ran
|
||||
(first
|
||||
(etaf-runtime-route-create
|
||||
:runtime-id 1 :mount-epoch 1 :authority-token 'first
|
||||
@ -305,7 +376,12 @@
|
||||
(etaf-runtime-route-create
|
||||
:runtime-id 2 :mount-epoch 1 :authority-token 'second
|
||||
:scheduler-context context
|
||||
:scheduler (lambda (_route _source) (setq second-ran t))))
|
||||
:scheduler
|
||||
(lambda (_route _source)
|
||||
(setq second-ran t)
|
||||
(etaf-scheduler-enqueue-runtime
|
||||
context 'second-runtime
|
||||
(lambda () (setq runtime-ran t))))))
|
||||
captured)
|
||||
(condition-case condition
|
||||
(etaf-scheduler-call-with-projection
|
||||
@ -319,7 +395,9 @@
|
||||
(should (equal captured
|
||||
'(etaf-scheduler-test-source-condition "subscriber")))
|
||||
(should second-ran)
|
||||
(should runtime-ran)
|
||||
(should (= 2 (etaf-scheduler-test--metric context :subscriber-visits)))
|
||||
(should (= 1 (etaf-scheduler-test--metric context :runtime-executions)))
|
||||
(should (etaf-scheduler-context-idle-p context))))
|
||||
|
||||
(ert-deftest etaf-scheduler-authority-predicate-fault-is-diagnostic ()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user