mirror of
https://github.com/penpot/penpot.git
synced 2026-11-01 07:20:32 +00:00
An instance left the audit events pipeline dead: every push-audit-events answered with 429 and 69 worker threads parked for good inside a limiter. Two code paths of one profile acquired the same two limiters in opposite order: a command declaring its own limits (upload-file-media-object) took process-image/by-profile and then root/by-profile, while a command with no limits of its own took the roots first and then re-entered climit from inside its handler with the same ids (create-file-media-object-from-url). Each path held what the other one waited for, and no limiter had a timeout to break the wait. Now wrap puts the root limiters after the command's own, so they end up outermost and every path meets them first: the nested chains carry only the command's own limits and run inside them, which puts both paths in the same order. impl-invoke also keeps, per thread, the set of limiters that thread is inside of and runs the handler inline when a call asks for one it already holds, so a handler can no longer queue behind itself. The shipped climit.edn sets a timeout on every limiter (60s for operations that can be slow, 30s otherwise), so no wait can park a thread for ever. The production file lives outside the repository and has to be mirrored. Tests cover the acquisition order, the deadlock, the reentrancy and the timeouts of the shipped config. AI-assisted-by: deepseek-v4.1-flash
52 lines
2.0 KiB
Clojure
52 lines
2.0 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns backend-tests.rpc-climit-test
|
|
(:require
|
|
[app.rpc.climit :as-alias climit]
|
|
[app.util.services :as-alias sv]
|
|
[backend-tests.helpers :as th]
|
|
[clojure.test :as t]))
|
|
|
|
(t/use-fixtures :once th/state-init)
|
|
|
|
(defn- method-limit-declarations
|
|
"The `::climit/id` declarations of every RPC command, as the running system
|
|
wraps them."
|
|
[]
|
|
(->> (:app.rpc/methods th/*system*)
|
|
(vals)
|
|
(map first)
|
|
(keep (fn [mdata]
|
|
(when-let [limits (::climit/id mdata)]
|
|
[mdata limits])))))
|
|
|
|
(t/deftest commands-declare-the-by-profile-limit-before-the-global-one
|
|
;; `climit/get-limits` reverses a declared vector of pairs, so the first one
|
|
;; declared is acquired first, and the nested `climit/invoke!` chains read the
|
|
;; same declaration. A pair declared global first puts a command back in the
|
|
;; inverted order that parked two paths of one profile for good.
|
|
(let [declarations (method-limit-declarations)]
|
|
|
|
;; the system wraps every command, so a short list means the scan went stale
|
|
(t/is (<= 10 (count declarations)))
|
|
|
|
(doseq [[mdata limits] declarations
|
|
:when (and (vector? limits) (vector? (first limits)))]
|
|
(let [ids (mapv first limits)
|
|
index (into {} (map-indexed (fn [idx id] [id idx]) ids))
|
|
where (str (::sv/name mdata))]
|
|
|
|
(t/is (= (count ids) (count (distinct ids)))
|
|
(str where " declares the same limit twice"))
|
|
|
|
(doseq [[id idx] index
|
|
:when (= "by-profile" (name id))]
|
|
(when-let [gidx (get index (keyword (namespace id) "global"))]
|
|
(t/is (< idx gidx)
|
|
(str where " must declare " id " before "
|
|
(keyword (namespace id) "global")))))))))
|