diff --git a/exporter/src/app/consumer/exports.cljs b/exporter/src/app/consumer/exports.cljs index a133888965..268a84e258 100644 --- a/exporter/src/app/consumer/exports.cljs +++ b/exporter/src/app/consumer/exports.cljs @@ -360,15 +360,19 @@ ;; registered a terminate callback for this job (jobs/mark-cancelled job-id))] (p/catch - (p/let [session' (api/create-job-session job-id) + (p/let [beats (start-beats! job-id stop-cmd!) + beats! (:beat! beats) + _ (reset! stop-watchdog! (with-watchdog! beats! (:last beats))) + ;; the first breath right after the claim, before the + ;; session roundtrip: the widget moves from queued to + ;; exporting on it, and a cancel that landed meanwhile + ;; raises here instead of after wasted work + _ (check-beat! (beats! :preparing {} :force? true)) + session' (api/create-job-session job-id) _ (reset! session session') token (:session-token session') _ (l/info :hint "render session minted" :job-id (str job-id)) - beats (start-beats! job-id stop-cmd!) - beats! (:beat! beats) - _ (reset! stop-watchdog! (with-watchdog! beats! (:last beats))) - _ (check-beat! (beats! :preparing {} :force? true)) plan (make-plan job-id token params) resource (run-prepared! beats! cancelled plan) _ (@stop-watchdog!) diff --git a/exporter/test/exporter_tests/worker_test.cljs b/exporter/test/exporter_tests/worker_test.cljs index 2f9bf3f2ee..f4e14edd46 100644 --- a/exporter/test/exporter_tests/worker_test.cljs +++ b/exporter/test/exporter_tests/worker_test.cljs @@ -256,9 +256,9 @@ respond! (fake-fetch! (answer-with params sid stages)) restore! (fake-render! "rendered!")] (p/let [_ (worker/process! (run-claim job-id))] - (t/testing "the run went by claim, session, beats, settle" - (t/is (= ["claim-job" "create-job-session" - "report-job-progress" "report-job-progress" "report-job-progress" + (t/testing "the run went by claim, first breath, session, beats, settle" + (t/is (= ["claim-job" "report-job-progress" "create-job-session" + "report-job-progress" "report-job-progress" "complete-job"] (steps-of)))) (t/testing "the settle carried the real artifact, named and typed" diff --git a/frontend/src/app/main/data/exports/assets.cljs b/frontend/src/app/main/data/exports/assets.cljs index 81c89dc36c..404179e6d7 100644 --- a/frontend/src/app/main/data/exports/assets.cljs +++ b/frontend/src/app/main/data/exports/assets.cljs @@ -154,7 +154,9 @@ (defn- initialize-export-status "The state a just created job starts at: the widget shows it queued, with no figures yet — the counter comes with the first milestone the - worker publishes, and counts itself down from the items meanwhile." + worker publishes, and counts itself down from the items meanwhile. + The creation answers the row status (`pending`): the widget names it + queued, because nobody works on the job yet." [exports cmd {:keys [id status]}] (ptk/reify ::initialize-export-status ptk/UpdateEvent @@ -169,7 +171,7 @@ :last-update (ct/now) :cmd cmd :job-id id - :status status})))) + :status (if (= "pending" status) "queued" status)})))) (defn- counter-of "The figures of one milestone: `objects` counts the shapes and @@ -181,8 +183,11 @@ (defn- add-milestone "One beat of the job into the widget: what has landed and what is - coming, named by the stage the run is in. A milestone that arrives - once the widget settled from a cancel or an error is ignored." + coming, named by the stage the run is in. The status moves to running + on the first beat, figures or not. A milestone without figures (the + first breath of the worker) names the stage but keeps the figures the + widget already holds. A milestone that arrives once the widget + settled from a cancel or an error is ignored." [payload] (let [figures (counter-of (:counters payload))] (ptk/reify ::add-milestone @@ -190,13 +195,15 @@ (update [_ state] (if-not (get-in state [:export :in-progress]) state - (update state :export assoc - :status "running" - :stage (:stage payload) - :last-update (ct/now) - :progress (:current figures) - :total (or (:total figures) - (get-in state [:export :total])))))))) + (-> (update state :export assoc + :status "running" + :stage (:stage payload) + :last-update (ct/now)) + (cond-> (some? figures) + (update :export assoc + :progress (:current figures) + :total (or (:total figures) + (get-in state [:export :total])))))))))) (defn- add-outcome "The last row of the job as the end of the widget. A completed job diff --git a/frontend/src/app/main/ui/exports/assets.cljs b/frontend/src/app/main/ui/exports/assets.cljs index 239b4baf7f..5047a97e99 100644 --- a/frontend/src/app/main/ui/exports/assets.cljs +++ b/frontend/src/app/main/ui/exports/assets.cljs @@ -246,11 +246,18 @@ (not cancelling?)) total (or (:total state) (count items)) complete? (= progress total) + ;; The figure the detail names: a percentage when the worker + ;; said how much there is, nothing when it has not said yet + ;; (queued, or a milestone without counters). + percent (when (and (number? progress) + (number? total) + (pos? total)) + (js/Math.round (* 100 (/ progress total)))) circ (* 2 Math/PI 12) pct (if (zero? total) circ (- circ (* circ (/ progress total)))) pwidth - (if error? + (if (or error? (zero? total)) 280 (/ (* progress 280) total)) @@ -338,16 +345,22 @@ [:button {:class (stl/css :retry-btn) :on-click cancel-export} (tr "workspace.options.cancel-export")] - [:span {:class (stl/css :progress)} - (dm/str progress " / " total)]] + (when (some? percent) + [:span {:class (stl/css :progress)} + (dm/str percent "%")])] - ;; A counter for work that is being abandoned says nothing useful. - (or cancelling? cancelled?) + ;; A counter for work that is waiting, going away, or gone + ;; says nothing useful. + (or queued? cancelling? cancelled?) nil - :else + ;; The running figure, once the worker said how much there is. + (some? percent) [:span {:class (stl/css :progress)} - (dm/str progress " / " total)])] + (dm/str percent "%")] + + :else + nil)] [:button {:class (stl/css :progress-close-button) :on-click toggle-detail-visibility} diff --git a/frontend/test/frontend_tests/data/exports_assets_test.cljs b/frontend/test/frontend_tests/data/exports_assets_test.cljs index 788f96ebf8..fd308cf9b0 100644 --- a/frontend/test/frontend_tests/data/exports_assets_test.cljs +++ b/frontend/test/frontend_tests/data/exports_assets_test.cljs @@ -71,6 +71,28 @@ (t/is (= (export-with-name (str (:object-id export))) (de/normalize-export (assoc export :name ""))))) +(t/deftest initialize-export-status-shows-a-fresh-job-as-queued + ;; the creation answers the row status ("pending"): nobody works on + ;; the job yet, so the widget names it queued until the first + ;; milestone moves it to running + (let [state (ptk/update (#'de/initialize-export-status + [export] nil {:id (uuid/next) :status "pending"}) + {})] + (t/is (= "queued" (get-in state [:export :status]))) + (t/is (true? (get-in state [:export :in-progress]))) + (t/is (= 0 (get-in state [:export :progress]))))) + +(t/deftest add-milestone-without-figures-keeps-the-figures + ;; the first breath of the worker (:preparing) carries no counters: + ;; it moves the widget to running without blanking the figures + (let [before {:export {:in-progress true :status "queued" + :progress 2 :total 5}} + after (ptk/update (#'de/add-milestone {:stage :preparing :counters {}}) + before)] + (t/is (= "running" (get-in after [:export :status]))) + (t/is (= 2 (get-in after [:export :progress]))) + (t/is (= 5 (get-in after [:export :total]))))) + (t/deftest request-simple-export-runs-one-assets-job (t/async done (let [export (export-with-name "")