mirror of
https://github.com/logseq/logseq.git
synced 2026-05-24 04:34:14 +00:00
add snapshot api
Disabled snapshot button though since cloudflare sandbox backup is slow for large repos.
This commit is contained in:
50
deps/workers/src/logseq/agents/do.cljs
vendored
50
deps/workers/src/logseq/agents/do.cljs
vendored
@@ -925,6 +925,53 @@
|
||||
create-pr?
|
||||
push-branch-fn)))))))))))
|
||||
|
||||
(defn- handle-snapshot [^js self request]
|
||||
(let [user-id (user-id-from-request request)]
|
||||
(if-not (string? user-id)
|
||||
(http/unauthorized)
|
||||
(p/let [current-session (<get-session self)]
|
||||
(cond
|
||||
(nil? current-session)
|
||||
(http/not-found)
|
||||
|
||||
(terminal-status? (:status current-session))
|
||||
(session-conflict "session is not writable")
|
||||
|
||||
(not (map? (:runtime current-session)))
|
||||
(session-conflict "session runtime unavailable")
|
||||
|
||||
:else
|
||||
(let [runtime (:runtime current-session)
|
||||
provider (runtime-provider/resolve-provider (.-env self) runtime)]
|
||||
(-> (p/let [_ (<append-event! self {:type "sandbox.snapshot.started"
|
||||
:data {:by user-id}
|
||||
:ts (common/now-ms)})
|
||||
result (runtime-provider/<snapshot-runtime! provider
|
||||
runtime
|
||||
{:task (:task current-session)})
|
||||
snapshot-id (or (:snapshot-id result)
|
||||
(:id result))
|
||||
_ (<append-event! self {:type "sandbox.snapshot.succeeded"
|
||||
:data (cond-> {:by user-id}
|
||||
(string? snapshot-id) (assoc :snapshot-id snapshot-id))
|
||||
:ts (common/now-ms)})]
|
||||
(http/json-response :sessions/snapshot
|
||||
(cond-> {:status "snapshot-created"
|
||||
:message "Sandbox snapshot created."}
|
||||
(string? snapshot-id) (assoc :snapshot-id snapshot-id))))
|
||||
(p/catch (fn [error]
|
||||
(let [reason (error-reason error)
|
||||
unsupported? (= reason "unsupported-snapshot")]
|
||||
(p/let [_ (<append-event! self {:type "sandbox.snapshot.failed"
|
||||
:data {:by user-id
|
||||
:reason reason
|
||||
:error (str error)}
|
||||
:ts (common/now-ms)})]
|
||||
(if unsupported?
|
||||
(session-conflict (or (some-> error ex-message str string/trim not-empty)
|
||||
"session runtime snapshot unavailable"))
|
||||
(http/error-response (str error) 500)))))))))))))
|
||||
|
||||
(defn- handle-cancel [^js self request]
|
||||
(let [user-id (user-id-from-request request)]
|
||||
(if-not (string? user-id)
|
||||
@@ -1165,6 +1212,9 @@
|
||||
(= path "/__session__/pr")
|
||||
(handle-pr self request)
|
||||
|
||||
(= path "/__session__/snapshot")
|
||||
(handle-snapshot self request)
|
||||
|
||||
(= path "/__session__/terminal")
|
||||
(handle-terminal self request)
|
||||
|
||||
|
||||
11
deps/workers/src/logseq/agents/handler.cljs
vendored
11
deps/workers/src/logseq/agents/handler.cljs
vendored
@@ -193,6 +193,16 @@
|
||||
(forward-request stub do-url "POST" headers body-json))
|
||||
(http/error-response "server error" 500)))))))))
|
||||
|
||||
(defn- handle-snapshot [{:keys [env request url claims route]}]
|
||||
(let [session-id (get-in route [:path-params :session-id])]
|
||||
(if-not (string? session-id)
|
||||
(http/bad-request "invalid session id")
|
||||
(if-let [^js stub (session-stub env session-id)]
|
||||
(let [headers (base-headers request claims)
|
||||
do-url (str (.-origin url) "/__session__/snapshot")]
|
||||
(forward-request stub do-url "POST" headers nil))
|
||||
(http/error-response "server error" 500)))))
|
||||
|
||||
(defn handle [{:keys [route] :as ctx}]
|
||||
(case (:handler route)
|
||||
:sessions/create (handle-create ctx)
|
||||
@@ -203,6 +213,7 @@
|
||||
:sessions/interrupt (handle-control ctx "/__session__/interrupt")
|
||||
:sessions/cancel (handle-cancel ctx)
|
||||
:sessions/pr (handle-pr ctx)
|
||||
:sessions/snapshot (handle-snapshot ctx)
|
||||
:sessions/terminal (handle-terminal ctx)
|
||||
:sessions/events (handle-events ctx)
|
||||
:sessions/branches (handle-branches ctx)
|
||||
|
||||
1
deps/workers/src/logseq/agents/routes.cljs
vendored
1
deps/workers/src/logseq/agents/routes.cljs
vendored
@@ -12,6 +12,7 @@
|
||||
["/interrupt" {:methods {"POST" :sessions/interrupt}}]
|
||||
["/cancel" {:methods {"POST" :sessions/cancel}}]
|
||||
["/pr" {:methods {"POST" :sessions/pr}}]
|
||||
["/snapshot" {:methods {"POST" :sessions/snapshot}}]
|
||||
["/events" {:methods {"GET" :sessions/events}}]
|
||||
["/branches" {:methods {"GET" :sessions/branches}}]
|
||||
["/terminal" {:methods {"GET" :sessions/terminal}}]
|
||||
|
||||
178
deps/workers/src/logseq/agents/runtime_provider.cljs
vendored
178
deps/workers/src/logseq/agents/runtime_provider.cljs
vendored
@@ -121,7 +121,7 @@
|
||||
|
||||
(def ^:private default-repo-base-dir "/workspace")
|
||||
(def ^:private cloudflare-local-host "http://localhost")
|
||||
(def ^:private cloudflare-backup-ttl-seconds (* 7 24 60 60))
|
||||
(def ^:private cloudflare-snapshot-ttl-seconds (* 7 24 60 60))
|
||||
(defonce ^:private cloudflare-backup-cache (atom {}))
|
||||
|
||||
(defn clear-cloudflare-backup-cache!
|
||||
@@ -496,9 +496,9 @@
|
||||
[backup-key backup-id]
|
||||
(when (and (string? backup-key) (string? backup-id))
|
||||
(let [now (js/Date.now)
|
||||
ttl-ms (* cloudflare-backup-ttl-seconds 1000)]
|
||||
ttl-ms (* cloudflare-snapshot-ttl-seconds 1000)]
|
||||
(swap! cloudflare-backup-cache assoc backup-key {:id backup-id
|
||||
:ttl-seconds cloudflare-backup-ttl-seconds
|
||||
:ttl-seconds cloudflare-snapshot-ttl-seconds
|
||||
:expires-at-ms (+ now ttl-ms)
|
||||
:updated-at-ms now}))))
|
||||
|
||||
@@ -507,6 +507,28 @@
|
||||
(when (string? backup-key)
|
||||
(swap! cloudflare-backup-cache dissoc backup-key)))
|
||||
|
||||
(defn- sanitize-backup-name
|
||||
[value]
|
||||
(let [raw (or (some-> value str string/lower-case) "snapshot")
|
||||
sanitized (-> raw
|
||||
;; Keep snapshot names flat for provider compatibility.
|
||||
(string/replace #"[^a-z0-9._-]+" "-")
|
||||
(string/replace #"-+" "-")
|
||||
(string/replace #"^-+" "")
|
||||
(string/replace #"-+$" ""))]
|
||||
(if (string/blank? sanitized)
|
||||
"snapshot"
|
||||
sanitized)))
|
||||
|
||||
(defn- snapshot-backup-name
|
||||
[runtime task]
|
||||
(let [base (or (repo-backup-key task)
|
||||
(some-> (:session-id runtime) str not-empty)
|
||||
"snapshot")]
|
||||
(str (sanitize-backup-name base)
|
||||
"-"
|
||||
(js/Date.now))))
|
||||
|
||||
(defn- fill-repo-template
|
||||
[template {:keys [repo-url session-id repo-dir]}]
|
||||
(-> (or template "")
|
||||
@@ -882,6 +904,43 @@
|
||||
(let [msg (when error (aget error "message"))]
|
||||
(if (string? msg) msg (str error))))
|
||||
|
||||
(defn- cloudflare-backup-config-error?
|
||||
[error]
|
||||
(let [message (-> (str (or (ex-message error) "")
|
||||
"\n"
|
||||
(error-message error))
|
||||
string/lower-case)]
|
||||
(or (string/includes? message "invalidbackupconfigerror")
|
||||
(string/includes? message "backup not configured")
|
||||
(string/includes? message "backup_bucket")
|
||||
(string/includes? message "presigned url credentials")
|
||||
(string/includes? message "missing env vars")
|
||||
(string/includes? message "cf-r2-error header")
|
||||
(string/includes? message "response.statuscode = 500"))))
|
||||
|
||||
(defn- cloudflare-backup-config-message
|
||||
[error]
|
||||
(let [raw (or (some-> error ex-message str string/trim not-empty)
|
||||
(some-> (error-message error) str string/trim not-empty)
|
||||
(str error))
|
||||
lower-raw (string/lower-case raw)
|
||||
sanitized (-> raw
|
||||
(string/replace #"(?i)^error:\s*" "")
|
||||
(string/replace #"(?i)^invalidbackupconfigerror:\s*" "")
|
||||
string/trim)]
|
||||
(cond
|
||||
(or (string/includes? lower-raw "cf-r2-error header")
|
||||
(string/includes? lower-raw "response.statuscode = 500"))
|
||||
(str "snapshot upload succeeded but backup verification through BACKUP_BUCKET failed in local dev. "
|
||||
"Ensure the R2 binding uses remote=true and run both workers with remote mode "
|
||||
"(e.g. `wrangler dev --remote` for sync and agents).")
|
||||
|
||||
(string/blank? sanitized)
|
||||
"snapshot backup is not configured. Configure BACKUP_BUCKET and backup credentials in wrangler.agents.toml."
|
||||
|
||||
:else
|
||||
sanitized)))
|
||||
|
||||
(defn- cloudflare-port-error?
|
||||
[error]
|
||||
(let [m (-> (error-message error) string/lower-case)]
|
||||
@@ -975,7 +1034,7 @@
|
||||
(let [backup-id (:id entry)
|
||||
backup #js {:id backup-id
|
||||
:dir target-dir}]
|
||||
(-> (->promise (.call restore-backup sandbox backup))
|
||||
(-> (->promise (.restoreBackup sandbox backup))
|
||||
(p/then (fn [_]
|
||||
(log/debug :agent/cloudflare-backup-restored
|
||||
{:backup-key backup-key
|
||||
@@ -992,37 +1051,52 @@
|
||||
false)))))))
|
||||
|
||||
(defn- <cloudflare-create-backup!
|
||||
[^js sandbox backup-key source-dir]
|
||||
[^js sandbox source-dir backup-name]
|
||||
(let [create-backup (js-method sandbox "createBackup")]
|
||||
(cond
|
||||
(or (not (string? backup-key))
|
||||
(not (string? source-dir)))
|
||||
(p/resolved nil)
|
||||
(not (string? source-dir))
|
||||
(p/rejected (ex-info "invalid snapshot source dir"
|
||||
{:reason :invalid-snapshot-source-dir
|
||||
:source-dir source-dir}))
|
||||
|
||||
(not (fn? create-backup))
|
||||
(p/resolved nil)
|
||||
(p/rejected (ex-info "cloudflare runtime does not support snapshots"
|
||||
{:reason :unsupported-snapshot
|
||||
:provider "cloudflare"}))
|
||||
|
||||
:else
|
||||
(-> (->promise (.call create-backup sandbox (clj->js {:dir source-dir
|
||||
:name backup-key
|
||||
:ttl cloudflare-backup-ttl-seconds})))
|
||||
(p/then (fn [backup]
|
||||
(let [backup-id (aget backup "id")]
|
||||
(when (string? backup-id)
|
||||
(remember-cloudflare-backup! backup-key backup-id)
|
||||
(log/debug :agent/cloudflare-backup-created
|
||||
{:backup-key backup-key
|
||||
:backup-id backup-id
|
||||
:dir source-dir
|
||||
:ttl-seconds cloudflare-backup-ttl-seconds})
|
||||
{:id backup-id
|
||||
:dir source-dir}))))
|
||||
(p/catch (fn [error]
|
||||
(log/error :agent/cloudflare-backup-create-failed
|
||||
{:backup-key backup-key
|
||||
:dir source-dir
|
||||
:error (str error)})
|
||||
nil))))))
|
||||
(p/catch
|
||||
;; Use direct method invocation instead of Function#call to avoid
|
||||
;; serializing the sandbox proxy receiver across RPC boundaries.
|
||||
(p/let [backup (->promise (.createBackup sandbox (clj->js {:dir source-dir
|
||||
:name backup-name
|
||||
:ttl cloudflare-snapshot-ttl-seconds})))
|
||||
backup-id (aget backup "id")]
|
||||
(if (string? backup-id)
|
||||
(do
|
||||
(log/debug :agent/cloudflare-snapshot-created
|
||||
{:snapshot-id backup-id
|
||||
:name backup-name
|
||||
:dir source-dir
|
||||
:ttl-seconds cloudflare-snapshot-ttl-seconds})
|
||||
{:snapshot-id backup-id
|
||||
:name backup-name
|
||||
:dir source-dir})
|
||||
(throw (ex-info "cloudflare snapshot create returned invalid id"
|
||||
{:reason :invalid-snapshot-id
|
||||
:backup backup}))))
|
||||
(fn [error]
|
||||
(log/error :agent/cloudflare-snapshot-create-failed
|
||||
{:name backup-name
|
||||
:dir source-dir
|
||||
:error (str error)})
|
||||
(if (cloudflare-backup-config-error? error)
|
||||
(p/rejected
|
||||
(ex-info (cloudflare-backup-config-message error)
|
||||
{:reason :unsupported-snapshot
|
||||
:provider "cloudflare"
|
||||
:raw-error (str error)}))
|
||||
(p/rejected error)))))))
|
||||
|
||||
(defn- <cloudflare-clone-repo! [^js env sandbox session-id task]
|
||||
(when-let [cmd (repo-clone-command env session-id task "cloudflare")]
|
||||
@@ -1121,6 +1195,7 @@
|
||||
(<open-events-stream! [this runtime])
|
||||
(<send-message! [this runtime message])
|
||||
(<open-terminal! [this runtime request opts])
|
||||
(<snapshot-runtime! [this runtime opts])
|
||||
(<push-branch! [this runtime opts])
|
||||
(<terminate-runtime! [this runtime]))
|
||||
|
||||
@@ -1202,6 +1277,12 @@
|
||||
{:reason :unsupported-terminal
|
||||
:provider "sprites"})))
|
||||
|
||||
(<snapshot-runtime! [_ _runtime _opts]
|
||||
(p/rejected
|
||||
(ex-info "sprites runtime provider does not support snapshots"
|
||||
{:reason :unsupported-snapshot
|
||||
:provider "sprites"})))
|
||||
|
||||
(<push-branch! [_ runtime opts]
|
||||
(let [name (:sprite-name runtime)
|
||||
session-id (:session-id opts)
|
||||
@@ -1289,6 +1370,12 @@
|
||||
{:reason :unsupported-terminal
|
||||
:provider "local-dev"})))
|
||||
|
||||
(<snapshot-runtime! [_ _runtime _opts]
|
||||
(p/rejected
|
||||
(ex-info "local-dev runtime provider does not support snapshots"
|
||||
{:reason :unsupported-snapshot
|
||||
:provider "local-dev"})))
|
||||
|
||||
(<push-branch! [_ _runtime _opts]
|
||||
(p/rejected
|
||||
(ex-info "local-dev runtime provider does not support managed git push"
|
||||
@@ -1382,6 +1469,25 @@
|
||||
(<open-terminal! [_ runtime request opts]
|
||||
(<cloudflare-open-terminal! env runtime request opts))
|
||||
|
||||
(<snapshot-runtime! [_ runtime opts]
|
||||
(let [sandbox-id (:sandbox-id runtime)
|
||||
session-id (:session-id runtime)
|
||||
backup-dir (or (:backup-dir runtime)
|
||||
(get-repo-dir session-id))
|
||||
task (:task opts)
|
||||
backup-key (repo-backup-key task)
|
||||
backup-name (snapshot-backup-name runtime task)]
|
||||
(when-not (string? sandbox-id)
|
||||
(throw (ex-info "missing sandbox-id on runtime"
|
||||
{:reason :missing-sandbox-id
|
||||
:runtime runtime})))
|
||||
(let [sandbox (cloudflare-sandbox env sandbox-id)]
|
||||
(p/let [result (<cloudflare-create-backup! sandbox backup-dir backup-name)
|
||||
snapshot-id (:snapshot-id result)]
|
||||
(when (and (string? backup-key) (string? snapshot-id))
|
||||
(remember-cloudflare-backup! backup-key snapshot-id))
|
||||
result))))
|
||||
|
||||
(<push-branch! [_ runtime opts]
|
||||
(let [sandbox-id (:sandbox-id runtime)
|
||||
session-id (:session-id opts)
|
||||
@@ -1440,15 +1546,10 @@
|
||||
|
||||
(<terminate-runtime! [_ runtime]
|
||||
(let [sandbox-id (:sandbox-id runtime)
|
||||
session-id (:session-id runtime)
|
||||
backup-key (:backup-key runtime)
|
||||
backup-dir (or (:backup-dir runtime)
|
||||
(get-repo-dir session-id))]
|
||||
session-id (:session-id runtime)]
|
||||
(log/debug :agent/cloudflare-terminate
|
||||
{:session-id session-id
|
||||
:sandbox-id sandbox-id
|
||||
:backup-key backup-key
|
||||
:backup-dir backup-dir})
|
||||
:sandbox-id sandbox-id})
|
||||
(if-not (string? sandbox-id)
|
||||
(p/resolved nil)
|
||||
(let [sandbox (cloudflare-sandbox env sandbox-id)
|
||||
@@ -1456,10 +1557,7 @@
|
||||
agent-token (or (:agent-token runtime)
|
||||
(env-str env "SANDBOX_AGENT_TOKEN"))]
|
||||
(p/catch
|
||||
(p/let [_ (p/catch
|
||||
(<cloudflare-create-backup! sandbox backup-key backup-dir)
|
||||
(fn [_] nil))
|
||||
_ (when (string? session-id)
|
||||
(p/let [_ (when (string? session-id)
|
||||
(p/catch
|
||||
(<cloudflare-terminate-session! sandbox port agent-token session-id)
|
||||
(fn [_] nil)))
|
||||
|
||||
13
deps/workers/src/logseq/sync/malli_schema.cljs
vendored
13
deps/workers/src/logseq/sync/malli_schema.cljs
vendored
@@ -260,6 +260,9 @@
|
||||
[:create-pr {:optional true} :boolean]
|
||||
[:force {:optional true} :boolean]])
|
||||
|
||||
(def sessions-snapshot-request-schema
|
||||
[:map])
|
||||
|
||||
(def sessions-create-response-schema
|
||||
[:map
|
||||
[:session-id :string]
|
||||
@@ -306,6 +309,12 @@
|
||||
[:github-raw-body {:optional true} :string]
|
||||
[:github-response-headers {:optional true} :any]])
|
||||
|
||||
(def sessions-snapshot-response-schema
|
||||
[:map
|
||||
[:status :string]
|
||||
[:snapshot-id {:optional true} [:maybe :string]]
|
||||
[:message {:optional true} [:maybe :string]]])
|
||||
|
||||
(def http-request-schemas
|
||||
{:graphs/create graph-create-request-schema
|
||||
:graph-members/create graph-member-create-request-schema
|
||||
@@ -316,7 +325,8 @@
|
||||
:e2ee/grant-access e2ee-grant-access-request-schema
|
||||
:sessions/create sessions-create-request-schema
|
||||
:sessions/message sessions-message-request-schema
|
||||
:sessions/pr sessions-pr-request-schema})
|
||||
:sessions/pr sessions-pr-request-schema
|
||||
:sessions/snapshot sessions-snapshot-request-schema})
|
||||
|
||||
(def http-response-schemas
|
||||
{:graphs/list graphs-list-response-schema
|
||||
@@ -349,6 +359,7 @@
|
||||
:sessions/interrupt http-ok-response-schema
|
||||
:sessions/cancel http-ok-response-schema
|
||||
:sessions/pr sessions-pr-response-schema
|
||||
:sessions/snapshot sessions-snapshot-response-schema
|
||||
:sessions/events sessions-events-response-schema
|
||||
:sessions/branches sessions-branches-response-schema
|
||||
:error http-error-response-schema})
|
||||
|
||||
41
deps/workers/test/logseq/agents/do_test.cljs
vendored
41
deps/workers/test/logseq/agents/do_test.cljs
vendored
@@ -719,6 +719,47 @@
|
||||
(is false (str "unexpected error: " error))
|
||||
(done))))))))
|
||||
|
||||
(deftest snapshot-endpoint-creates-sandbox-snapshot-test
|
||||
(testing "session snapshot endpoint creates a manual sandbox snapshot"
|
||||
(async done
|
||||
(let [snapshot-calls (atom [])
|
||||
env #js {"AGENT_RUNTIME_PROVIDER" "local-dev"}
|
||||
self (make-self env)
|
||||
headers {"content-type" "application/json"
|
||||
"x-user-id" "user-1"}]
|
||||
(-> (.put (.-storage self)
|
||||
"session"
|
||||
(clj->js {:id "sess-snapshot"
|
||||
:status "running"
|
||||
:task {:project {:repo-url "https://github.com/example/repo"}}
|
||||
:runtime {:provider "cloudflare"
|
||||
:sandbox-id "sbx-snapshot"
|
||||
:session-id "sess-snapshot"}
|
||||
:audit {}
|
||||
:created-at 0
|
||||
:updated-at 0}))
|
||||
(.then (fn [_]
|
||||
(with-redefs [runtime-provider/<snapshot-runtime!
|
||||
(fn [_provider runtime _opts]
|
||||
(swap! snapshot-calls conj runtime)
|
||||
(js/Promise.resolve {:snapshot-id "backup-1"}))]
|
||||
(agent-do/handle-fetch self
|
||||
(json-request "http://db-sync.local/__session__/snapshot"
|
||||
"POST"
|
||||
{}
|
||||
headers)))))
|
||||
(.then (fn [resp]
|
||||
(is (= 200 (.-status resp)))
|
||||
(.then (<json resp)
|
||||
(fn [body]
|
||||
(is (= "snapshot-created" (:status body)))
|
||||
(is (= "backup-1" (:snapshot-id body)))
|
||||
(is (= 1 (count @snapshot-calls)))
|
||||
(done)))))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected error: " error))
|
||||
(done))))))))
|
||||
|
||||
(deftest pr-endpoint-push-only-success-test
|
||||
(testing "session publish endpoint supports push-only flow"
|
||||
(async done
|
||||
|
||||
@@ -67,3 +67,9 @@
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:force "no"})))
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:commit-message 100})))
|
||||
(is (nil? (http/coerce-http-request :sessions/pr {:head-branch 42})))))
|
||||
|
||||
(deftest sessions-snapshot-coerce-test
|
||||
(testing "accepts empty sessions/snapshot request payload"
|
||||
(is (= {} (http/coerce-http-request :sessions/snapshot {}))))
|
||||
(testing "rejects invalid sessions/snapshot payload"
|
||||
(is (nil? (http/coerce-http-request :sessions/snapshot {:force true})))))
|
||||
|
||||
@@ -44,4 +44,7 @@
|
||||
(is (= "session-12" (get-in match [:path-params :session-id]))))
|
||||
(let [match (routes/match-route "POST" "/sessions/session-13/pr")]
|
||||
(is (= :sessions/pr (:handler match)))
|
||||
(is (= "session-13" (get-in match [:path-params :session-id]))))))
|
||||
(is (= "session-13" (get-in match [:path-params :session-id]))))
|
||||
(let [match (routes/match-route "POST" "/sessions/session-14/snapshot")]
|
||||
(is (= :sessions/snapshot (:handler match)))
|
||||
(is (= "session-14" (get-in match [:path-params :session-id]))))))
|
||||
|
||||
@@ -842,13 +842,12 @@
|
||||
(is (= "sess-3" (:session-id data))))
|
||||
(done)))))))
|
||||
|
||||
(deftest cloudflare-provider-backup-restore-test
|
||||
(deftest cloudflare-provider-does-not-auto-backup-or-restore-test
|
||||
(async done
|
||||
(runtime-provider/clear-cloudflare-backup-cache!)
|
||||
(let [calls (atom {:clone 0
|
||||
:restore []
|
||||
:backups []})
|
||||
backup-counter (atom 0)
|
||||
:restore 0
|
||||
:backup 0})
|
||||
sandboxes (atom {})
|
||||
make-sandbox
|
||||
(fn [sandbox-id]
|
||||
@@ -874,16 +873,13 @@
|
||||
(js/JSON.stringify #js {:ok true})
|
||||
#js {:status 200 :headers #js {"content-type" "application/json"}})))
|
||||
:createBackup
|
||||
(fn [opts]
|
||||
(let [backup-id (str "backup-" (swap! backup-counter inc))]
|
||||
(swap! calls update :backups conj {:sandbox-id sandbox-id
|
||||
:opts (js->clj opts :keywordize-keys true)
|
||||
:id backup-id})
|
||||
(js/Promise.resolve #js {:id backup-id :dir (aget opts "dir")})))
|
||||
(fn [_opts]
|
||||
(swap! calls update :backup inc)
|
||||
(js/Promise.resolve #js {:id (str sandbox-id "-backup")
|
||||
:dir "/workspace"}))
|
||||
:restoreBackup
|
||||
(fn [backup]
|
||||
(swap! calls update :restore conj {:sandbox-id sandbox-id
|
||||
:backup (js->clj backup :keywordize-keys true)})
|
||||
(fn [_backup]
|
||||
(swap! calls update :restore inc)
|
||||
(js/Promise.resolve #js {:success true}))
|
||||
:delete (fn [] (js/Promise.resolve nil))})
|
||||
sandbox-ns
|
||||
@@ -899,25 +895,15 @@
|
||||
task {:agent {:provider "codex"}
|
||||
:project {:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}}]
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-bk-1" task)
|
||||
(.then (fn [runtime-1]
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-no-auto-bk-1" task)
|
||||
(.then (fn [runtime]
|
||||
(is (= 1 (:clone @calls)))
|
||||
(-> (runtime-provider/<terminate-runtime! provider runtime-1)
|
||||
(is (= 0 (:restore @calls)))
|
||||
(-> (runtime-provider/<terminate-runtime! provider runtime)
|
||||
(.then (fn [_]
|
||||
(is (= 1 (count (:backups @calls))))
|
||||
(is (= 604800 (get-in @calls [:backups 0 :opts :ttl])))
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-bk-2" task)
|
||||
(.then (fn [_runtime-2]
|
||||
(is (= 1 (:clone @calls)))
|
||||
(is (= 1 (count (:restore @calls))))
|
||||
(is (= "backup-1"
|
||||
(get-in @calls [:restore 0 :backup :id])))
|
||||
(is (= "/workspace/sess-bk-2"
|
||||
(get-in @calls [:restore 0 :backup :dir])))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected reprovision error: " error))
|
||||
(done))))))
|
||||
(is (= 0 (:backup @calls)))
|
||||
(is (= 0 (:restore @calls)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected terminate error: " error))
|
||||
(done))))))
|
||||
@@ -925,22 +911,26 @@
|
||||
(is false (str "unexpected provision error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest cloudflare-provider-backup-single-pointer-per-repo-branch-test
|
||||
(deftest cloudflare-provider-restores-from-cached-snapshot-test
|
||||
(async done
|
||||
(runtime-provider/clear-cloudflare-backup-cache!)
|
||||
(let [calls (atom {:restore-ids []
|
||||
:backup-ids []})
|
||||
backup-counter (atom 0)
|
||||
(let [calls (atom {:clone 0
|
||||
:restore []
|
||||
:backup 0})
|
||||
sandboxes (atom {})
|
||||
make-sandbox
|
||||
(fn [_sandbox-id]
|
||||
(fn [sandbox-id]
|
||||
#js {:exec
|
||||
(fn [cmd]
|
||||
(cond
|
||||
(string/includes? cmd "/v1/health")
|
||||
(js/Promise.resolve #js {:success true})
|
||||
|
||||
(string/includes? cmd "git clone --depth 1 --single-branch --no-tags")
|
||||
(js/Promise.resolve #js {:success true})
|
||||
(do
|
||||
(swap! calls update :clone inc)
|
||||
(js/Promise.resolve #js {:success true}))
|
||||
|
||||
:else
|
||||
(js/Promise.resolve #js {:success true :stdout "" :stderr ""})))
|
||||
:setEnvVars (fn [_] (js/Promise.resolve nil))
|
||||
@@ -953,12 +943,12 @@
|
||||
#js {:status 200 :headers #js {"content-type" "application/json"}})))
|
||||
:createBackup
|
||||
(fn [_opts]
|
||||
(let [backup-id (str "backup-" (swap! backup-counter inc))]
|
||||
(swap! calls update :backup-ids conj backup-id)
|
||||
(js/Promise.resolve #js {:id backup-id :dir "/workspace"})))
|
||||
(swap! calls update :backup inc)
|
||||
(js/Promise.resolve #js {:id "backup-restore-1"
|
||||
:dir "/workspace"}))
|
||||
:restoreBackup
|
||||
(fn [backup]
|
||||
(swap! calls update :restore-ids conj (aget backup "id"))
|
||||
(swap! calls update :restore conj (js->clj backup :keywordize-keys true))
|
||||
(js/Promise.resolve #js {:success true}))
|
||||
:delete (fn [] (js/Promise.resolve nil))})
|
||||
sandbox-ns
|
||||
@@ -973,34 +963,56 @@
|
||||
provider (runtime-provider/create-provider env "cloudflare")
|
||||
task {:agent {:provider "codex"}
|
||||
:project {:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}}]
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-bk-a" task)
|
||||
(.then (fn [runtime-a]
|
||||
(-> (runtime-provider/<terminate-runtime! provider runtime-a)
|
||||
(.then (fn [_]
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-bk-b" task)
|
||||
(.then (fn [runtime-b]
|
||||
(-> (runtime-provider/<terminate-runtime! provider runtime-b)
|
||||
(.then (fn [_]
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-bk-c" task)
|
||||
(.then (fn [_runtime-c]
|
||||
(is (= ["backup-1" "backup-2"]
|
||||
(:restore-ids @calls)))
|
||||
(is (= ["backup-1" "backup-2"]
|
||||
(:backup-ids @calls)))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected third provision error: " error))
|
||||
(done))))))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected second terminate error: " error))
|
||||
(done))))))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected second provision error: " error))
|
||||
(done))))))
|
||||
:base-branch "main"}}
|
||||
runtime {:provider "cloudflare"
|
||||
:sandbox-id "sbx-1"
|
||||
:session-id "sess-cache"}]
|
||||
(-> (runtime-provider/<snapshot-runtime! provider runtime {:task task})
|
||||
(.then (fn [_]
|
||||
(is (= 1 (:backup @calls)))
|
||||
(-> (runtime-provider/<provision-runtime! provider "sess-cache-next" task)
|
||||
(.then (fn [_next-runtime]
|
||||
(is (= 0 (:clone @calls)))
|
||||
(is (= 1 (count (:restore @calls))))
|
||||
(is (= "backup-restore-1"
|
||||
(get-in @calls [:restore 0 :id])))
|
||||
(is (= "/workspace/sess-cache-next"
|
||||
(get-in @calls [:restore 0 :dir])))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected first terminate error: " error))
|
||||
(is false (str "unexpected reprovision error: " error))
|
||||
(done))))))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected first provision error: " error))
|
||||
(is false (str "unexpected snapshot error: " error))
|
||||
(done)))))))
|
||||
|
||||
(deftest cloudflare-provider-snapshot-runtime-test
|
||||
(async done
|
||||
(runtime-provider/clear-cloudflare-backup-cache!)
|
||||
(let [calls (atom {})
|
||||
sandbox-stub
|
||||
#js {:createBackup
|
||||
(fn [opts]
|
||||
(swap! calls assoc :snapshot-opts (js->clj opts :keywordize-keys true))
|
||||
(js/Promise.resolve #js {:id "backup-42"}))}
|
||||
sandbox-ns
|
||||
#js {:idFromName (fn [name] name)
|
||||
:get (fn [_id] sandbox-stub)}
|
||||
env #js {"Sandbox" sandbox-ns}
|
||||
provider (runtime-provider/create-provider env "cloudflare")
|
||||
runtime {:provider "cloudflare"
|
||||
:sandbox-id "sbx-1"
|
||||
:session-id "sess-snapshot"}]
|
||||
(-> (runtime-provider/<snapshot-runtime! provider
|
||||
runtime
|
||||
{:task {:project {:repo-url "https://github.com/example/repo"
|
||||
:base-branch "main"}}})
|
||||
(.then (fn [result]
|
||||
(is (= "backup-42" (:snapshot-id result)))
|
||||
(is (= "/workspace/sess-snapshot" (get-in @calls [:snapshot-opts :dir])))
|
||||
(is (= 604800 (get-in @calls [:snapshot-opts :ttl])))
|
||||
(is (string? (get-in @calls [:snapshot-opts :name])))
|
||||
(done)))
|
||||
(.catch (fn [error]
|
||||
(is false (str "unexpected snapshot error: " error))
|
||||
(done)))))))
|
||||
|
||||
24
deps/workers/worker/wrangler.agents.toml
vendored
24
deps/workers/worker/wrangler.agents.toml
vendored
@@ -26,6 +26,12 @@ class_name = "AgentSessionDO"
|
||||
name = "Sandbox"
|
||||
class_name = "Sandbox"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "BACKUP_BUCKET"
|
||||
bucket_name = "logseq-sync-assets-prod"
|
||||
preview_bucket_name = "logseq-sync-assets-prod"
|
||||
remote = true
|
||||
|
||||
[[migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = [ "AgentSessionDO" ]
|
||||
@@ -39,6 +45,8 @@ COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLn
|
||||
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
|
||||
COGNITO_CLIENT_ID = "69cs1lgme7p8kbgld8n5kseii6"
|
||||
AGENT_RUNTIME_PROVIDER = "cloudflare"
|
||||
CLOUDFLARE_ACCOUNT_ID = "2553ea8236c11ea0f88de28fce1cbfee"
|
||||
BACKUP_BUCKET_NAME = "logseq-sync-assets-prod"
|
||||
|
||||
[env.staging]
|
||||
name = "logseq-agents-staging"
|
||||
@@ -54,6 +62,8 @@ COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLn
|
||||
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
|
||||
COGNITO_CLIENT_ID = "69cs1lgme7p8kbgld8n5kseii6"
|
||||
AGENT_RUNTIME_PROVIDER = "cloudflare"
|
||||
CLOUDFLARE_ACCOUNT_ID = "2553ea8236c11ea0f88de28fce1cbfee"
|
||||
BACKUP_BUCKET_NAME = "logseq-sync-assets-dev"
|
||||
SENTRY_DSN = "https://dc09d27243b9492bbe15e0dd279ad7de@o416451.ingest.us.sentry.io/5311485"
|
||||
SENTRY_ENVIRONMENT = "staging"
|
||||
SENTRY_TRACES_SAMPLE_RATE = "0.1"
|
||||
@@ -66,6 +76,12 @@ class_name = "AgentSessionDO"
|
||||
name = "Sandbox"
|
||||
class_name = "Sandbox"
|
||||
|
||||
[[env.staging.r2_buckets]]
|
||||
binding = "BACKUP_BUCKET"
|
||||
bucket_name = "logseq-sync-assets-dev"
|
||||
preview_bucket_name = "logseq-sync-assets-dev"
|
||||
remote = true
|
||||
|
||||
[[env.staging.migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = [ "AgentSessionDO" ]
|
||||
@@ -85,6 +101,8 @@ COGNITO_JWKS_URL = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLn
|
||||
COGNITO_ISSUER = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_dtagLnju8"
|
||||
COGNITO_CLIENT_ID = "69cs1lgme7p8kbgld8n5kseii6"
|
||||
AGENT_RUNTIME_PROVIDER = "cloudflare"
|
||||
CLOUDFLARE_ACCOUNT_ID = "2553ea8236c11ea0f88de28fce1cbfee"
|
||||
BACKUP_BUCKET_NAME = "logseq-sync-assets-prod"
|
||||
SENTRY_DSN = "https://dc09d27243b9492bbe15e0dd279ad7de@o416451.ingest.us.sentry.io/5311485"
|
||||
SENTRY_ENVIRONMENT = "prod"
|
||||
SENTRY_TRACES_SAMPLE_RATE = "0.1"
|
||||
@@ -103,6 +121,12 @@ class_name = "AgentSessionDO"
|
||||
name = "Sandbox"
|
||||
class_name = "Sandbox"
|
||||
|
||||
[[env.prod.r2_buckets]]
|
||||
binding = "BACKUP_BUCKET"
|
||||
bucket_name = "logseq-sync-assets-prod"
|
||||
preview_bucket_name = "logseq-sync-assets-prod"
|
||||
remote = true
|
||||
|
||||
[[env.prod.migrations]]
|
||||
tag = "v1"
|
||||
new_sqlite_classes = [ "AgentSessionDO" ]
|
||||
|
||||
6
deps/workers/worker/wrangler.toml
vendored
6
deps/workers/worker/wrangler.toml
vendored
@@ -23,6 +23,8 @@ service = "logseq-agents"
|
||||
[[r2_buckets]]
|
||||
binding = "LOGSEQ_SYNC_ASSETS"
|
||||
bucket_name = "logseq-sync-assets-prod"
|
||||
preview_bucket_name = "logseq-sync-assets-prod"
|
||||
remote = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
@@ -64,6 +66,8 @@ new_sqlite_classes = [ "SyncDO" ]
|
||||
[[env.staging.r2_buckets]]
|
||||
binding = "LOGSEQ_SYNC_ASSETS"
|
||||
bucket_name = "logseq-sync-assets-dev"
|
||||
preview_bucket_name = "logseq-sync-assets-dev"
|
||||
remote = true
|
||||
|
||||
[[env.staging.d1_databases]]
|
||||
binding = "DB"
|
||||
@@ -99,6 +103,8 @@ new_sqlite_classes = [ "SyncDO" ]
|
||||
[[env.prod.r2_buckets]]
|
||||
binding = "LOGSEQ_SYNC_ASSETS"
|
||||
bucket_name = "logseq-sync-assets-prod"
|
||||
preview_bucket_name = "logseq-sync-assets-prod"
|
||||
remote = true
|
||||
|
||||
[[env.prod.d1_databases]]
|
||||
binding = "DB"
|
||||
|
||||
@@ -1043,8 +1043,10 @@
|
||||
(when-not terminal-tab-active?
|
||||
[:div.flex.items-center.justify-between.gap-2
|
||||
[:div.text-xs.opacity-60
|
||||
(if publish-busy?
|
||||
(cond
|
||||
publish-busy?
|
||||
"Publishing changes..."
|
||||
:else
|
||||
"Publish session changes")]
|
||||
[:div.flex.items-center.gap-2
|
||||
(shui/button
|
||||
|
||||
@@ -692,6 +692,18 @@
|
||||
(some-> error ex-message)
|
||||
"Publish failed."))
|
||||
|
||||
(defn- snapshot-status-message
|
||||
[resp]
|
||||
(or (:message resp)
|
||||
"Sandbox snapshot created."))
|
||||
|
||||
(defn- snapshot-error-message
|
||||
[error]
|
||||
(or (some-> (ex-data error) :body :message)
|
||||
(some-> (ex-data error) :body :error)
|
||||
(some-> error ex-message)
|
||||
"Snapshot failed."))
|
||||
|
||||
(defn <publish-session!
|
||||
[block opts]
|
||||
(let [base (db-sync/http-base)
|
||||
@@ -746,3 +758,48 @@
|
||||
(p/catch (fn [error]
|
||||
(notification/show! (publish-error-message error) :error false)
|
||||
(p/rejected error)))))))))
|
||||
|
||||
(defn <snapshot-session!
|
||||
[block]
|
||||
(let [base (db-sync/http-base)
|
||||
block-uuid (:block/uuid block)
|
||||
session (session-state block-uuid)
|
||||
session-id (or (:session-id session)
|
||||
(some-> block-uuid str))]
|
||||
(cond
|
||||
(not base)
|
||||
(do
|
||||
(notification/show! "DB sync is not configured." :error false)
|
||||
(p/resolved nil))
|
||||
|
||||
(not (task-ready? block))
|
||||
(do
|
||||
(notification/show! "Task needs Project (with Git Repo) and Agent." :warning)
|
||||
(p/resolved nil))
|
||||
|
||||
(not (:session-id session))
|
||||
(do
|
||||
(notification/show! "Start the agent session before snapshotting." :warning)
|
||||
(p/resolved nil))
|
||||
|
||||
:else
|
||||
(p/let [_ (js/Promise. user-handler/task--ensure-id&access-token)
|
||||
body (coerce-http-request :sessions/snapshot {})]
|
||||
(if (nil? body)
|
||||
(do
|
||||
(notification/show! "Invalid snapshot payload." :error false)
|
||||
nil)
|
||||
(-> (db-sync/fetch-json (str base "/sessions/" session-id "/snapshot")
|
||||
{:method "POST"
|
||||
:headers {"content-type" "application/json"}
|
||||
:body (js/JSON.stringify (clj->js body))}
|
||||
{:response-schema :sessions/snapshot})
|
||||
(p/then (fn [resp]
|
||||
(update-session-state! block-uuid {:last-snapshot resp
|
||||
:last-snapshot-at (util/time-ms)})
|
||||
(notification/show! (snapshot-status-message resp) :success false)
|
||||
(<fetch-events! block)
|
||||
resp))
|
||||
(p/catch (fn [error]
|
||||
(notification/show! (snapshot-error-message error) :error false)
|
||||
(p/rejected error)))))))))
|
||||
|
||||
Reference in New Issue
Block a user