Fase 6: close the subs-loader in-flight dedup gap, raise batch limits, add /v1/invoke-batch
Fase 0's telemetry work had already flagged that subs-loader deduped
load! calls that landed before a flush, but not ones arriving while a
flush's own worker request was still in flight -- flush! cleared *batch
via take-batch! before the request it just sent even resolved, so a
load! for the same slot in that window fired a duplicate request. Fixed
by tracking in-flight requests in a separate *inflight map, untracked
only once the request settles (db/subs_loader.cljs), with a real e2e
test proving the dedup and that a later load! after settlement does
fire a fresh request (db/subs_loader_test.cljs).
Also raises the :children/:resources get-render-snapshots batch limits
from 25 to 200 -- 25 was calibrated for postMessage-to-a-worker being
free; over the network that trade inverts. Two existing subs-test.cljs
tests hardcoded the old 25-item batching behavior with 100 items across
4 calls; updated to the new single-call-of-100 behavior under the 200
limit.
Deliberately did NOT widen db/subs.cljs's load-batch-coalesce-window
from the same-tick 0ms setTimeout to a few ms, despite that being in
the original plan: doing so broke ~212 assertions across subs_test.cljs
that synchronize on same-tick flushing via (p/delay 0), and would be
redundant with the invoke-batch coalescer below, which already captures
the same "batch several concurrent RPCs into fewer HTTP round trips"
win at the transport layer instead.
New POST /v1/invoke-batch on the daemon (db_worker_node.cljs) runs an
array of {method, argsTransit} through the same repo-check/lock/invoke
path as /v1/invoke, but never rejects the whole batch on one item's
failure -- each item independently becomes {ok, resultTransit} or {ok:
false, error}. Verified against a live daemon: mixed success/repo-
mismatch/success in one request, 401 without auth, 405 on non-POST.
On the client, frontend.persist-db.remote/create-invoke-batcher
coalesces wrapped-worker calls (the RPC every state/<invoke-db-worker
call funnels through for a remote client) issued within a 10ms window
into one POST /v1/invoke-batch, generalizing subs-loader's own
same-method batching to every thread-api call a remote client makes.
Two invokes only land in the same batch when neither is awaited before
the other fires, so this can't reorder anything a caller depends on
sequentially.
New db_worker_node_test.cljs coverage: invoke-batch mixing success and
repo-mismatch failure in one request, and invoke-batch auth/method
enforcement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,10 @@
|
||||
[base-url]
|
||||
(str (normalize-base-url base-url) "/v1/invoke"))
|
||||
|
||||
(defn- invoke-batch-url
|
||||
[base-url]
|
||||
(str (normalize-base-url base-url) "/v1/invoke-batch"))
|
||||
|
||||
(defn- import-db-binary-url
|
||||
[base-url repo]
|
||||
(str (normalize-base-url base-url)
|
||||
@@ -214,6 +218,62 @@
|
||||
(on-invoke-failure method args error))
|
||||
(throw error))))))
|
||||
|
||||
(defn create-invoke-batcher
|
||||
"Coalesces thread-api invokes issued within a short window into a single
|
||||
POST /v1/invoke-batch instead of one HTTP round trip per call -- the
|
||||
generalization, to every wrapped-worker call a remote client makes, of the
|
||||
request-group batching frontend.db.subs-loader already does for
|
||||
get-render-snapshots specifically (Fase 6). Two invokes only land in the
|
||||
same batch when neither is awaited before the other is issued (independent
|
||||
fire-and-forget/parallel calls); a caller that does
|
||||
`(p/let [a (invoke...) b (invoke...)] ...)` awaits `a` before `b` is even
|
||||
requested, so sequentially-dependent invokes are never split across a
|
||||
batch boundary in a way that would change their observed order."
|
||||
[{:keys [base-url auth-token fetch-fn schedule-fn on-invoke-success on-invoke-failure
|
||||
invoke-coalesce-ms]
|
||||
:or {invoke-coalesce-ms 10}}]
|
||||
(let [*pending (atom [])
|
||||
do-flush!
|
||||
(fn []
|
||||
(let [batch @*pending]
|
||||
(reset! *pending [])
|
||||
(when (seq batch)
|
||||
(let [payload (js/JSON.stringify
|
||||
(clj->js {:invokes (mapv (fn [{:keys [method args]}]
|
||||
{:method method
|
||||
:argsTransit (ldb/write-transit-str args)})
|
||||
batch)}))]
|
||||
(-> (p/let [{:keys [status body]}
|
||||
(fetch-fn {:method "POST"
|
||||
:url (invoke-batch-url base-url)
|
||||
:headers (base-headers auth-token)
|
||||
:body payload})]
|
||||
(if (<= 200 status 299)
|
||||
(let [results (:results (parse-response-body body))]
|
||||
(doseq [[{:keys [method args deferred]} result] (map vector batch results)]
|
||||
(if (:ok result)
|
||||
(let [value (ldb/read-transit-str (:resultTransit result))]
|
||||
(when on-invoke-success (on-invoke-success method args value))
|
||||
(p/resolve! deferred value))
|
||||
(let [error (:error result)
|
||||
ex (ex-info (or (:message error) "db-worker invoke failed")
|
||||
(cond-> {:code (normalize-code (:code error))}
|
||||
error (assoc :error error)))]
|
||||
(when on-invoke-failure (on-invoke-failure method args ex))
|
||||
(p/reject! deferred ex)))))
|
||||
(throw (ex-info "db-worker batch invoke failed" {:status status}))))
|
||||
(p/catch (fn [error]
|
||||
(doseq [{:keys [method args deferred]} batch]
|
||||
(when on-invoke-failure (on-invoke-failure method args error))
|
||||
(p/reject! deferred error)))))))))]
|
||||
(fn invoke-batched! [method args]
|
||||
(let [deferred (p/deferred)
|
||||
schedule? (empty? @*pending)]
|
||||
(swap! *pending conj {:method method :args args :deferred deferred})
|
||||
(when schedule?
|
||||
(schedule-fn do-flush! invoke-coalesce-ms))
|
||||
deferred))))
|
||||
|
||||
(defn- import-db-binary!
|
||||
[{:keys [base-url auth-token fetch-fn on-invoke-success on-invoke-failure]} repo data]
|
||||
(let [method "thread-api/import-db-binary"
|
||||
@@ -385,8 +445,9 @@
|
||||
(defn start!
|
||||
[{:keys [base-url auth-token event-handler] :as opts}]
|
||||
(let [client (create-client (assoc opts :base-url base-url :auth-token auth-token))
|
||||
invoke-batched! (create-invoke-batcher client)
|
||||
wrapped-worker (fn [qkw & args]
|
||||
(invoke! client (method->str qkw) args))
|
||||
(invoke-batched! (method->str qkw) args))
|
||||
{:keys [disconnect!]} (connect-events-ws! (assoc client
|
||||
:event-handler event-handler
|
||||
:auth-token auth-token)
|
||||
|
||||
Reference in New Issue
Block a user