mirror of
https://github.com/penpot/penpot.git
synced 2026-08-08 13:58:35 +00:00
✨ Put the graph subsystem behind a flag, off by default (#11075)
`app.graph.ladybug` imports `com.ladybugdb.*` at namespace load. Two namespaces reach the subsystem and both required it at the top level: `app.http.debug`, which registers the `/dbg` routes, and `app.srepl.main`, which loads with the REPL server. Every backend built from this branch therefore linked the Ladybug native library into the JVM at boot, whether or not a graph was ever used. Add a `:graph` flag to `varia`, deliberately absent from `default` so that a released Penpot ships with the subsystem off. Both require sites now resolve `app.graph.*` at call time, so with the flag off no `com.ladybugdb` class is loaded. The nine `/dbg` graph routes are registered only when the flag is on, and 404 otherwise. The `/dbg` admin gate is untouched: the flag decides which routes exist, not who may reach them. When the flag is on, route init requires the subsystem eagerly, so a missing or unusable native library fails the boot rather than the first console request. No tracked file turns the flag on. `backend/scripts/_env` leaves it out, so a devenv boots with the subsystem off exactly as a released build does, and `docker/images/docker-compose.yaml`, the self-hosting distribution, is untouched. Whoever works on the graph turns it on for one checkout through the gitignored `backend/scripts/_env.local`, which every backend and exporter dev script sources right after `_env`. Verified with `-verbose:class` over a boot's namespace load plus `ig/init-key ::routes`: 9 `com.ladybugdb` classes before this change with no flag set, 0 after it with the flag off, 9 with `enable-graph`.
This commit is contained in:
parent
df80aca9cd
commit
c18c44f575
@ -222,6 +222,7 @@ Debug Main Page
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
{% if graph-enabled %}
|
||||
<fieldset>
|
||||
<legend>Export graph (Ladybug):</legend>
|
||||
<desc>Given a FILE-ID, builds the graph projection and downloads
|
||||
@ -237,6 +238,7 @@ Debug Main Page
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
<fieldset>
|
||||
<legend>Import binfile:</legend>
|
||||
<desc>Import penpot file in binary format.</desc>
|
||||
|
||||
@ -22,8 +22,6 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.features.file-migrations :as feat.fmig]
|
||||
[app.graph.debug :as graph.debug]
|
||||
[app.graph.ingest :as graph.ingest]
|
||||
[app.http.session :as session]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files-create :refer [create-file]]
|
||||
@ -61,6 +59,7 @@
|
||||
::yres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {:version (:full cf/version)
|
||||
:profile profile
|
||||
:graph-enabled (contains? cf/flags :graph)
|
||||
:current-clock ct/*clock*
|
||||
:current-offset (if offset
|
||||
(ct/format-duration offset)
|
||||
@ -334,6 +333,16 @@
|
||||
"content-disposition" (str "attachmen; filename=" (first file-ids) ".penpot")}}))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; GRAPH (flag: :graph)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; `app.graph.*` resolves at call time, never at the top of this namespace.
|
||||
;; `app.graph.ladybug` imports `com.ladybugdb.*`, so requiring it links the
|
||||
;; Ladybug native library into the JVM, and this namespace loads on every
|
||||
;; backend boot. The routes below are registered only under the `:graph` flag,
|
||||
;; so with the flag off nothing resolves and no native code loads.
|
||||
|
||||
(defn graph-export-handler
|
||||
"Build (or rebuild) the Ladybug graph for a file and stream the `.lbug`
|
||||
database. MVP: synchronous ingest on each request."
|
||||
@ -344,7 +353,8 @@
|
||||
:code :missing-arguments
|
||||
:hint "missing file-id"))
|
||||
|
||||
(let [{:keys [db-path]} (graph.ingest/ingest-file! cfg file-id :skip-stats? true)]
|
||||
(let [ingest-file! (requiring-resolve 'app.graph.ingest/ingest-file!)
|
||||
{:keys [db-path]} (ingest-file! cfg file-id :skip-stats? true)]
|
||||
(when-not (fs/exists? db-path)
|
||||
(ex/raise :type :internal
|
||||
:code :graph-file-not-found
|
||||
@ -366,40 +376,44 @@
|
||||
|
||||
(defn graph-console-handler
|
||||
[_cfg {:keys [::session/profile-id]}]
|
||||
(graph-console-response (graph.debug/console-context profile-id)))
|
||||
(let [console-context (requiring-resolve 'app.graph.debug/console-context)]
|
||||
(graph-console-response (console-context profile-id))))
|
||||
|
||||
(defn graph-load-handler
|
||||
[cfg {:keys [params ::session/profile-id]}]
|
||||
(let [file-id (some-> (:file-id params) parse-uuid)]
|
||||
(let [file-id (some-> (:file-id params) parse-uuid)
|
||||
load-session! (requiring-resolve 'app.graph.debug/load-session!)]
|
||||
(when-not file-id
|
||||
(ex/raise :type :validation
|
||||
:code :missing-arguments
|
||||
:hint "missing file-id"))
|
||||
(graph.debug/load-session! cfg profile-id file-id)
|
||||
(load-session! cfg profile-id file-id)
|
||||
{::yres/status 302
|
||||
::yres/headers {"location" "/dbg/graph"}}))
|
||||
|
||||
(defn graph-unload-handler
|
||||
[_cfg {:keys [::session/profile-id]}]
|
||||
(graph.debug/unload-session! profile-id)
|
||||
((requiring-resolve 'app.graph.debug/unload-session!) profile-id)
|
||||
{::yres/status 302
|
||||
::yres/headers {"location" "/dbg/graph"}})
|
||||
|
||||
(defn graph-reload-handler
|
||||
"Re-ingest the currently loaded file into the in-memory graph session."
|
||||
[cfg {:keys [::session/profile-id]}]
|
||||
(if-let [file-id (some-> (graph.debug/session-info profile-id) :file-id)]
|
||||
(do
|
||||
(graph.debug/load-session! cfg profile-id file-id)
|
||||
{::yres/status 302
|
||||
::yres/headers {"location" "/dbg/graph"}})
|
||||
(ex/raise :type :not-found
|
||||
:code :graph-session-not-loaded
|
||||
:hint "load a file graph before reloading")))
|
||||
(let [session-info (requiring-resolve 'app.graph.debug/session-info)
|
||||
load-session! (requiring-resolve 'app.graph.debug/load-session!)]
|
||||
(if-let [file-id (some-> (session-info profile-id) :file-id)]
|
||||
(do
|
||||
(load-session! cfg profile-id file-id)
|
||||
{::yres/status 302
|
||||
::yres/headers {"location" "/dbg/graph"}})
|
||||
(ex/raise :type :not-found
|
||||
:code :graph-session-not-loaded
|
||||
:hint "load a file graph before reloading"))))
|
||||
|
||||
(defn graph-sync-status-handler
|
||||
[_cfg {:keys [::session/profile-id]}]
|
||||
(if-let [status (graph.debug/sync-status profile-id)]
|
||||
(if-let [status ((requiring-resolve 'app.graph.debug/sync-status) profile-id)]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (t/encode-str status {:type :json-verbose})}
|
||||
@ -411,7 +425,7 @@
|
||||
"Export the in-memory session graph as plain JSON (not transit) for the
|
||||
G6 graph view embedded in the console page."
|
||||
[_cfg {:keys [::session/profile-id]}]
|
||||
(if-let [data (graph.debug/export-graph-data! profile-id)]
|
||||
(if-let [data ((requiring-resolve 'app.graph.debug/export-graph-data!) profile-id)]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (json/encode data)}
|
||||
@ -470,18 +484,20 @@
|
||||
|
||||
(defn graph-query-handler
|
||||
[_cfg {:keys [params ::session/profile-id] :as request}]
|
||||
(let [query (:query params)]
|
||||
(let [query (:query params)
|
||||
query-session! (requiring-resolve 'app.graph.debug/query-session!)
|
||||
console-context (requiring-resolve 'app.graph.debug/console-context)]
|
||||
(try
|
||||
(let [result (graph.debug/query-session! profile-id query)]
|
||||
(let [result (query-session! profile-id query)]
|
||||
(if (json-request? request)
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (t/encode-str {:query query
|
||||
:query-result result}
|
||||
{:type :json-verbose})}
|
||||
(graph-console-response (graph.debug/console-context profile-id
|
||||
:query query
|
||||
:query-result result))))
|
||||
(graph-console-response (console-context profile-id
|
||||
:query query
|
||||
:query-result result))))
|
||||
(catch Throwable e
|
||||
(let [error (or (:hint (ex-data e)) (ex-message e))]
|
||||
(if (json-request? request)
|
||||
@ -489,9 +505,9 @@
|
||||
::yres/headers {"content-type" "application/json; charset=utf-8"}
|
||||
::yres/body (t/encode-str {:query query :error error}
|
||||
{:type :json-verbose})}
|
||||
(graph-console-response (graph.debug/console-context profile-id
|
||||
:query query
|
||||
:error error))))))))
|
||||
(graph-console-response (console-context profile-id
|
||||
:query query
|
||||
:error error))))))))
|
||||
|
||||
(defn import-handler
|
||||
[{:keys [::db/pool] :as cfg} {:keys [params ::session/profile-id] :as request}]
|
||||
@ -727,33 +743,49 @@
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool")
|
||||
(assert (session/manager? (::session/manager params)) "expected a valid session manager"))
|
||||
|
||||
(defn- graph-action-routes
|
||||
[cfg]
|
||||
[["/graph-export" {:handler (partial graph-export-handler cfg)}]
|
||||
["/graph-load" {:handler (partial graph-load-handler cfg)}]
|
||||
["/graph-query" {:handler (partial graph-query-handler cfg)}]
|
||||
["/graph-unload" {:handler (partial graph-unload-handler cfg)}]
|
||||
["/graph-reload" {:handler (partial graph-reload-handler cfg)}]
|
||||
["/graph-sync-status" {:handler (partial graph-sync-status-handler cfg)}]
|
||||
["/graph-data" {:handler (partial graph-data-handler cfg)}]
|
||||
["/graph-files" {:handler (partial graph-files-handler cfg)}]])
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ {:keys [::db/pool] :as cfg}]
|
||||
[["/readyz" {:handler (partial health-handler cfg)}]
|
||||
["/dbg" {:middleware [[session/authz cfg]
|
||||
[with-authorization pool]]}
|
||||
["" {:handler (partial index-handler cfg)}]
|
||||
["/health" {:handler (partial health-handler cfg)}]
|
||||
["/changelog" {:handler (partial changelog-handler cfg)}]
|
||||
["/graph" {:handler (partial graph-console-handler cfg)}]
|
||||
["/error/:id" {:handler (partial error-handler cfg)}]
|
||||
["/error" {:handler (partial error-list-handler cfg)}]
|
||||
["/actions" {:middleware [[errors]]}
|
||||
["/set-virtual-clock"
|
||||
{:handler (partial set-virtual-clock cfg)}]
|
||||
["/resend-email-verification"
|
||||
{:handler (partial resend-email-notification cfg)}]
|
||||
["/handle-team-features"
|
||||
{:handler (partial handle-team-features cfg)}]
|
||||
["/file-export" {:handler (partial export-handler cfg)}]
|
||||
["/graph-export" {:handler (partial graph-export-handler cfg)}]
|
||||
["/graph-load" {:handler (partial graph-load-handler cfg)}]
|
||||
["/graph-query" {:handler (partial graph-query-handler cfg)}]
|
||||
["/graph-unload" {:handler (partial graph-unload-handler cfg)}]
|
||||
["/graph-reload" {:handler (partial graph-reload-handler cfg)}]
|
||||
["/graph-sync-status" {:handler (partial graph-sync-status-handler cfg)}]
|
||||
["/graph-data" {:handler (partial graph-data-handler cfg)}]
|
||||
["/graph-files" {:handler (partial graph-files-handler cfg)}]
|
||||
["/file-import" {:handler (partial import-handler cfg)}]
|
||||
["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]])
|
||||
;; The graph routes are registered only under the `:graph` flag. Left
|
||||
;; unregistered they 404, and nothing ever resolves `app.graph.*`. The `/dbg`
|
||||
;; admin gate is unchanged: it covers the graph routes exactly as before.
|
||||
(let [graph? (contains? cf/flags :graph)
|
||||
actions (cond-> ["/actions" {:middleware [[errors]]}
|
||||
["/set-virtual-clock"
|
||||
{:handler (partial set-virtual-clock cfg)}]
|
||||
["/resend-email-verification"
|
||||
{:handler (partial resend-email-notification cfg)}]
|
||||
["/handle-team-features"
|
||||
{:handler (partial handle-team-features cfg)}]
|
||||
["/file-export" {:handler (partial export-handler cfg)}]
|
||||
["/file-import" {:handler (partial import-handler cfg)}]
|
||||
["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]
|
||||
graph? (into (graph-action-routes cfg)))
|
||||
dbg (cond-> ["/dbg" {:middleware [[session/authz cfg]
|
||||
[with-authorization pool]]}
|
||||
["" {:handler (partial index-handler cfg)}]
|
||||
["/health" {:handler (partial health-handler cfg)}]
|
||||
["/changelog" {:handler (partial changelog-handler cfg)}]
|
||||
["/error/:id" {:handler (partial error-handler cfg)}]
|
||||
["/error" {:handler (partial error-list-handler cfg)}]
|
||||
actions]
|
||||
graph? (conj ["/graph" {:handler (partial graph-console-handler cfg)}]))]
|
||||
(when graph?
|
||||
;; With the flag on, the Ladybug native library belongs to this process,
|
||||
;; so load it here. A missing or unusable library then fails the boot
|
||||
;; instead of the first console request.
|
||||
(require 'app.graph.debug 'app.graph.ingest))
|
||||
|
||||
[["/readyz" {:handler (partial health-handler cfg)}]
|
||||
dbg]))
|
||||
|
||||
|
||||
@ -25,9 +25,6 @@
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.graph.ingest :as graph.ingest]
|
||||
[app.graph.ladybug :as graph.ladybug]
|
||||
[app.graph.report :as graph.report]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.msgbus :as mbus]
|
||||
@ -405,21 +402,30 @@
|
||||
;; GRAPH / LADYBUG
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; The graph namespaces resolve at call time, never at the top of this
|
||||
;; namespace. `app.graph.ladybug` imports `com.ladybugdb.*`, and this namespace
|
||||
;; loads with the REPL server on every boot, so a top-level require would link
|
||||
;; the Ladybug native library into every backend, graph or not. Calling one of
|
||||
;; the functions below loads the library at that point: the operator has asked
|
||||
;; for it explicitly. The `:graph` flag gates the request path
|
||||
;; (`app.http.debug`), not the REPL.
|
||||
|
||||
(defn graph-smoke-test!
|
||||
"Execute a basic Ladybug smoke test (CREATE + count).
|
||||
|
||||
Uses the embedded Ladybug Java API. Use :db-path \":memory:\" (default)
|
||||
or a filesystem path such as /tmp/test.lbug."
|
||||
[& {:keys [db-path] :or {db-path ":memory:"}}]
|
||||
(graph.ladybug/smoke-test! :db-path db-path))
|
||||
((requiring-resolve 'app.graph.ladybug/smoke-test!) :db-path db-path))
|
||||
|
||||
(defn graph-query-test!
|
||||
"Query Document count for a file's graph db (REPL diagnostic)."
|
||||
[file-id & {:keys [db-path]}]
|
||||
(let [file-id (h/parse-uuid file-id)
|
||||
db-path (or db-path (graph.ladybug/db-path-for-file file-id))
|
||||
stmt "MATCH (n:Document) RETURN count(n) AS Document_c;"]
|
||||
(graph.ladybug/query-scalar! db-path stmt)))
|
||||
(let [file-id (h/parse-uuid file-id)
|
||||
db-path (or db-path ((requiring-resolve 'app.graph.ladybug/db-path-for-file) file-id))
|
||||
query-scalar! (requiring-resolve 'app.graph.ladybug/query-scalar!)
|
||||
stmt "MATCH (n:Document) RETURN count(n) AS Document_c;"]
|
||||
(query-scalar! db-path stmt)))
|
||||
|
||||
(defn ingest-file-to-graph!
|
||||
"Project a Penpot file into a per-file Ladybug database.
|
||||
@ -432,8 +438,10 @@
|
||||
- `:reset-db?` delete any existing db first (default true)
|
||||
- `:skip-stats?` skip post-ingest MATCH count queries (default false)"
|
||||
[file-id & opts]
|
||||
(let [result (apply graph.ingest/ingest-file! sys/system file-id opts)]
|
||||
(graph.report/print-ingest! result)
|
||||
(let [ingest-file! (requiring-resolve 'app.graph.ingest/ingest-file!)
|
||||
print-ingest! (requiring-resolve 'app.graph.report/print-ingest!)
|
||||
result (apply ingest-file! sys/system file-id opts)]
|
||||
(print-ingest! result)
|
||||
result))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -100,6 +100,10 @@
|
||||
:backend-svgo
|
||||
;; If enabled, it makes the Google Fonts available.
|
||||
:google-fonts-provider
|
||||
;; Enables the Ladybug graph subsystem: the `/dbg` graph console and its
|
||||
;; actions. Off by default. With the flag off, `app.graph.*` never loads,
|
||||
;; so the Ladybug native library never enters the JVM.
|
||||
:graph
|
||||
;; Only for development.
|
||||
:nrepl-server
|
||||
;; Interactive repl. Only for development.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user