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-component)
|
||||||
(require 'etaf-context)
|
(require 'etaf-context)
|
||||||
(require 'etaf-behavior)
|
(require 'etaf-behavior)
|
||||||
|
(require 'etaf-render-port)
|
||||||
|
|
||||||
(declare-function etaf--runtime-render-component "etaf-runtime" (runtime call path))
|
(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)
|
(etaf-runtime-mount buffer-or-name view options)
|
||||||
(let ((ebox-viewport-width (plist-get options :viewport-width))
|
(let ((ebox-viewport-width (plist-get options :viewport-width))
|
||||||
(ebox-viewport-height (plist-get options :viewport-height)))
|
(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)
|
(provide 'etaf-renderer)
|
||||||
|
|
||||||
|
|||||||
@ -427,13 +427,16 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
|||||||
first-condition))
|
first-condition))
|
||||||
|
|
||||||
(defun etaf-scheduler--record-fault
|
(defun etaf-scheduler--record-fault
|
||||||
(context projection-id condition &optional phase)
|
(context projection-id condition &optional phase turn-id)
|
||||||
"Record CONTEXT failure CONDITION for PROJECTION-ID and optional PHASE."
|
"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))
|
(setf (etaf-scheduler-context-fault-state context) (copy-tree condition))
|
||||||
(cl-incf (etaf-scheduler-context-projection-fault-count context))
|
(cl-incf (etaf-scheduler-context-projection-fault-count context))
|
||||||
(push (append (list :projection-epoch projection-id
|
(push (append (list :projection-epoch projection-id
|
||||||
:turn-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))
|
(and phase (list :phase phase))
|
||||||
(list :condition (copy-tree condition)))
|
(list :condition (copy-tree condition)))
|
||||||
(etaf-scheduler-context-diagnostics context))
|
(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."
|
"Drain PROJECTION source-first and re-signal its first context fault."
|
||||||
(let ((projection-id (etaf-scheduler--projection-id projection))
|
(let ((projection-id (etaf-scheduler--projection-id projection))
|
||||||
(failed-contexts (make-hash-table :test #'eq))
|
(failed-contexts (make-hash-table :test #'eq))
|
||||||
|
(contained-context-conditions (make-hash-table :test #'eq))
|
||||||
(context-steps (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)
|
(while (etaf-scheduler--projection-work-p projection)
|
||||||
(let ((marked-contexts (make-hash-table :test #'eq)))
|
(let ((marked-contexts (make-hash-table :test #'eq)))
|
||||||
;; All source propagation across every context settles before any
|
;; 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
|
(when-let* ((source-condition
|
||||||
(etaf-scheduler--drain-context-sources
|
(etaf-scheduler--drain-context-sources
|
||||||
context projection-id)))
|
context projection-id)))
|
||||||
(signal (car source-condition)
|
(unless (gethash context contained-context-conditions)
|
||||||
(cdr source-condition))))
|
(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)
|
((error quit)
|
||||||
(puthash context t failed-contexts)
|
(puthash context t failed-contexts)
|
||||||
(unless first-condition (setq first-condition condition))
|
(unless fatal-condition (setq fatal-condition condition))
|
||||||
(etaf-scheduler--record-fault
|
(etaf-scheduler--record-fault
|
||||||
context projection-id condition)
|
context projection-id condition)
|
||||||
(etaf-scheduler--reset-pending context)))))))
|
(etaf-scheduler--reset-pending context)))))))
|
||||||
@ -502,14 +514,35 @@ PROJECTION-ID identifies the bounded projection owning the detached turn."
|
|||||||
(when-let* ((runtime-condition
|
(when-let* ((runtime-condition
|
||||||
(etaf-scheduler--run-runtime-turn
|
(etaf-scheduler--run-runtime-turn
|
||||||
context turn)))
|
context turn)))
|
||||||
(signal (car runtime-condition)
|
(unless (gethash context contained-context-conditions)
|
||||||
(cdr runtime-condition))))
|
(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)
|
((error quit)
|
||||||
(puthash context t failed-contexts)
|
(puthash context t failed-contexts)
|
||||||
(unless first-condition (setq first-condition condition))
|
(unless fatal-condition (setq fatal-condition condition))
|
||||||
(etaf-scheduler--record-fault
|
(etaf-scheduler--record-fault
|
||||||
context projection-id condition)
|
context projection-id condition)
|
||||||
(etaf-scheduler--reset-pending context))))))
|
(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))
|
(dolist (context (etaf-scheduler--projection-contexts projection))
|
||||||
(setf (etaf-scheduler-context-phase context) nil)
|
(setf (etaf-scheduler-context-phase context) nil)
|
||||||
(unless (gethash context failed-contexts)
|
(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)
|
(setf (etaf-scheduler-context-completed-projection-epoch context)
|
||||||
projection-id
|
projection-id
|
||||||
(etaf-scheduler-context-fault-state context) nil)))
|
(etaf-scheduler-context-fault-state context) nil)))
|
||||||
(when first-condition
|
(when-let* ((condition (or fatal-condition first-condition)))
|
||||||
(signal (car first-condition) (cdr first-condition)))))
|
(signal (car condition) (cdr condition)))))
|
||||||
|
|
||||||
(defun etaf-scheduler--finish-projection (projection)
|
(defun etaf-scheduler--finish-projection (projection)
|
||||||
"Restore PROJECTION invariants and return contained finalizer errors."
|
"Restore PROJECTION invariants and return contained finalizer errors."
|
||||||
|
|||||||
@ -459,6 +459,56 @@
|
|||||||
(should (string-match-p "(etaf-render-port-update" source))
|
(should (string-match-p "(etaf-render-port-update" source))
|
||||||
(should-not (string-match-p "(ebox-commit" 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 ()
|
(ert-deftest etaf-render-port-selected-port-is-process-stable ()
|
||||||
"Every downstream read returns the one bootstrap-selected port identity."
|
"Every downstream read returns the one bootstrap-selected port identity."
|
||||||
(should (eq (etaf-render-port-selected)
|
(should (eq (etaf-render-port-selected)
|
||||||
|
|||||||
@ -269,7 +269,7 @@
|
|||||||
(ert-deftest etaf-scheduler-source-fault-does-not-suppress-context-tail ()
|
(ert-deftest etaf-scheduler-source-fault-does-not-suppress-context-tail ()
|
||||||
"A failing source delivery cannot discard later sources in the same turn."
|
"A failing source delivery cannot discard later sources in the same turn."
|
||||||
(let ((context (etaf-scheduler-context-create :name 'source-fault-tail))
|
(let ((context (etaf-scheduler-context-create :name 'source-fault-tail))
|
||||||
second-ran captured)
|
second-ran runtime-ran captured)
|
||||||
(condition-case condition
|
(condition-case condition
|
||||||
(etaf-scheduler-call-with-projection
|
(etaf-scheduler-call-with-projection
|
||||||
(lambda ()
|
(lambda ()
|
||||||
@ -278,14 +278,84 @@
|
|||||||
(lambda (_context _source _projection-id)
|
(lambda (_context _source _projection-id)
|
||||||
(signal 'etaf-scheduler-test-source-condition '("first"))))
|
(signal 'etaf-scheduler-test-source-condition '("first"))))
|
||||||
(etaf-scheduler-enqueue-source
|
(etaf-scheduler-enqueue-source
|
||||||
context 'second
|
context 'second
|
||||||
(lambda (_context _source _projection-id)
|
(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)))
|
(etaf-scheduler-test-source-condition (setq captured condition)))
|
||||||
(should (equal captured
|
(should (equal captured
|
||||||
'(etaf-scheduler-test-source-condition "first")))
|
'(etaf-scheduler-test-source-condition "first")))
|
||||||
(should second-ran)
|
(should second-ran)
|
||||||
|
(should runtime-ran)
|
||||||
(should (= 2 (etaf-scheduler-test--metric context :source-deliveries)))
|
(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))))
|
(should (etaf-scheduler-context-idle-p context))))
|
||||||
|
|
||||||
(ert-deftest etaf-scheduler-subscriber-fault-does-not-suppress-group-tail ()
|
(ert-deftest etaf-scheduler-subscriber-fault-does-not-suppress-group-tail ()
|
||||||
@ -293,6 +363,7 @@
|
|||||||
(let* ((context
|
(let* ((context
|
||||||
(etaf-scheduler-context-create :name 'subscriber-fault-tail))
|
(etaf-scheduler-context-create :name 'subscriber-fault-tail))
|
||||||
second-ran
|
second-ran
|
||||||
|
runtime-ran
|
||||||
(first
|
(first
|
||||||
(etaf-runtime-route-create
|
(etaf-runtime-route-create
|
||||||
:runtime-id 1 :mount-epoch 1 :authority-token 'first
|
:runtime-id 1 :mount-epoch 1 :authority-token 'first
|
||||||
@ -305,7 +376,12 @@
|
|||||||
(etaf-runtime-route-create
|
(etaf-runtime-route-create
|
||||||
:runtime-id 2 :mount-epoch 1 :authority-token 'second
|
:runtime-id 2 :mount-epoch 1 :authority-token 'second
|
||||||
:scheduler-context context
|
: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)
|
captured)
|
||||||
(condition-case condition
|
(condition-case condition
|
||||||
(etaf-scheduler-call-with-projection
|
(etaf-scheduler-call-with-projection
|
||||||
@ -319,7 +395,9 @@
|
|||||||
(should (equal captured
|
(should (equal captured
|
||||||
'(etaf-scheduler-test-source-condition "subscriber")))
|
'(etaf-scheduler-test-source-condition "subscriber")))
|
||||||
(should second-ran)
|
(should second-ran)
|
||||||
|
(should runtime-ran)
|
||||||
(should (= 2 (etaf-scheduler-test--metric context :subscriber-visits)))
|
(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))))
|
(should (etaf-scheduler-context-idle-p context))))
|
||||||
|
|
||||||
(ert-deftest etaf-scheduler-authority-predicate-fault-is-diagnostic ()
|
(ert-deftest etaf-scheduler-authority-predicate-fault-is-diagnostic ()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user