host: durable lazy sessions — logins survive a restart
Sessions were in-memory, so a restart logged everyone out (same class as the relation wipe). Move them to the durable store, but LAZILY so anonymous/crawler traffic doesn't spam it: session/create mints a sid with no row; the row appears on the first session/set (a login). A per-boot epoch (one durable write at startup, host/session-init!) keeps sids unique across restarts without a write per request. - lib/host/session.sx: lazy backend (create = no row, set = create row, exists = row written) + epoch/in-memory-counter sid generation. - serve.sh: point the session store at the durable backend + host/session-init!. - blog.sx: host/current-principal is now a durable read, so host/auth-footer (home + post footers) had to move OUT of the quasiquote into let bindings — a perform during page-tree build raises VmSuspended (the whole site 500'd for a beat). Principal computed once per page. - 2 session tests: create writes no row, set creates the row. 249/249. Verified live: site renders (anon + authed), login + footer survive a container force-recreate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -326,12 +326,14 @@
|
|||||||
(let ((slug (dream-param req "slug")))
|
(let ((slug (dream-param req "slug")))
|
||||||
(let ((r (host/blog-get slug)))
|
(let ((r (host/blog-get slug)))
|
||||||
(if r
|
(if r
|
||||||
;; Compute the rendered body + related block in let bindings BEFORE the
|
;; Compute everything that does durable reads — body, related block, AND
|
||||||
;; quasiquote — host/blog--related-block does durable reads, and IO must
|
;; the auth footer (a durable session read now) — in let bindings BEFORE
|
||||||
;; happen in the handler body, not while the page tree is being built.
|
;; the quasiquote. IO must run in the handler body, never while the page
|
||||||
|
;; tree is built (a perform there raises VmSuspended under http-listen).
|
||||||
|
(let ((principal (host/current-principal req)))
|
||||||
(let ((body-html (host/blog-render r))
|
(let ((body-html (host/blog-render r))
|
||||||
(related-block (host/blog--related-block slug
|
(related-block (host/blog--related-block slug (not (nil? principal))))
|
||||||
(not (nil? (host/current-principal req))))))
|
(auth-foot (host/auth-footer req)))
|
||||||
(dream-html
|
(dream-html
|
||||||
(host/blog--page (get r :title)
|
(host/blog--page (get r :title)
|
||||||
(quasiquote
|
(quasiquote
|
||||||
@@ -345,7 +347,7 @@
|
|||||||
" · "
|
" · "
|
||||||
(a :href "/" "all posts")
|
(a :href "/" "all posts")
|
||||||
" · "
|
" · "
|
||||||
(unquote (host/auth-footer req))))))))
|
(unquote auth-foot))))))))
|
||||||
(dream-html-status 404
|
(dream-html-status 404
|
||||||
(host/blog--page "Not found"
|
(host/blog--page "Not found"
|
||||||
(quasiquote
|
(quasiquote
|
||||||
@@ -364,7 +366,10 @@
|
|||||||
posts)))
|
posts)))
|
||||||
(let ((listing (if (> (len posts) 0)
|
(let ((listing (if (> (len posts) 0)
|
||||||
(list (quote ul) items)
|
(list (quote ul) items)
|
||||||
(quote (p "No posts yet.")))))
|
(quote (p "No posts yet."))))
|
||||||
|
;; auth-footer does a durable session read — bind it BEFORE the
|
||||||
|
;; quasiquote (a perform during tree-build raises VmSuspended).
|
||||||
|
(auth-foot (host/auth-footer req)))
|
||||||
(dream-html
|
(dream-html
|
||||||
(host/blog--page "Blog"
|
(host/blog--page "Blog"
|
||||||
(quasiquote
|
(quasiquote
|
||||||
@@ -372,7 +377,7 @@
|
|||||||
(unquote listing)
|
(unquote listing)
|
||||||
(p (a :href "/new" "+ New post"))
|
(p (a :href "/new" "+ New post"))
|
||||||
(p :style "margin-top:2em;font-size:0.9em;opacity:0.8"
|
(p :style "margin-top:2em;font-size:0.9em;opacity:0.8"
|
||||||
(unquote (host/auth-footer req))))))))))))
|
(unquote auth-foot)))))))))))
|
||||||
|
|
||||||
(define host/blog-index (fn (req) (host/ok (host/blog-list))))
|
(define host/blog-index (fn (req) (host/ok (host/blog-list))))
|
||||||
|
|
||||||
|
|||||||
@@ -111,11 +111,18 @@ EPOCH=1
|
|||||||
echo "(epoch $EPOCH)"
|
echo "(epoch $EPOCH)"
|
||||||
echo "(eval \"(host/blog-load-edges!)\")"
|
echo "(eval \"(host/blog-load-edges!)\")"
|
||||||
EPOCH=$((EPOCH+1))
|
EPOCH=$((EPOCH+1))
|
||||||
# Session signing secret + admin login credentials, then grant the admin
|
# Sessions on the DURABLE store, LAZILY: only a logged-in session (one that
|
||||||
# principal "edit" on "blog" so a logged-in session passes the ACL gate on the
|
# writes a field) persists, so a login survives a restart while anonymous /
|
||||||
# write routes. Sessions stay IN-MEMORY (default store) — logins reset on
|
# crawler traffic leaves no rows. host/session-init! bumps the per-boot epoch
|
||||||
# restart but the durable KV isn't spammed by anonymous/ crawler sessions
|
# that keeps sids unique across restarts. Then the signing secret + admin
|
||||||
# (lazy session creation is a future lib/dream/session.sx improvement).
|
# credentials, and grant admin "edit" on "blog" so a logged-in session passes
|
||||||
|
# the ACL gate on the write routes.
|
||||||
|
echo "(epoch $EPOCH)"
|
||||||
|
echo "(eval \"(host/session-use-store! (persist/durable-backend))\")"
|
||||||
|
EPOCH=$((EPOCH+1))
|
||||||
|
echo "(epoch $EPOCH)"
|
||||||
|
echo "(eval \"(host/session-init!)\")"
|
||||||
|
EPOCH=$((EPOCH+1))
|
||||||
echo "(epoch $EPOCH)"
|
echo "(epoch $EPOCH)"
|
||||||
echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")"
|
echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")"
|
||||||
EPOCH=$((EPOCH+1))
|
EPOCH=$((EPOCH+1))
|
||||||
|
|||||||
@@ -20,26 +20,37 @@
|
|||||||
|
|
||||||
;; ── keys ────────────────────────────────────────────────────────────
|
;; ── keys ────────────────────────────────────────────────────────────
|
||||||
(define host/-sess-key (fn (sid) (str "session:" sid)))
|
(define host/-sess-key (fn (sid) (str "session:" sid)))
|
||||||
(define host/-sess-counter-key "session:-counter")
|
(define host/-sess-epoch-key "session:-epoch")
|
||||||
|
|
||||||
;; mint the next sid from a persisted counter (signature guards guessability)
|
;; sid generation: a per-BOOT epoch (one durable write at startup) + an in-memory
|
||||||
|
;; counter. The epoch keeps sids unique across restarts WITHOUT a write per
|
||||||
|
;; request, so anonymous traffic costs no disk. host/session-init! bumps the epoch
|
||||||
|
;; on boot (serve.sh); without it (e.g. tests) epoch 0 is fine within one process.
|
||||||
|
(define host/session-epoch 0)
|
||||||
|
(define host/session-ctr 0)
|
||||||
|
(define host/session-init!
|
||||||
|
(fn ()
|
||||||
|
(let ((e (+ 1 (or (persist/backend-kv-get host/session-store host/-sess-epoch-key) 0))))
|
||||||
|
(begin
|
||||||
|
(persist/backend-kv-put host/session-store host/-sess-epoch-key e)
|
||||||
|
(set! host/session-epoch e)
|
||||||
|
(set! host/session-ctr 0)))))
|
||||||
(define host/-sess-next-sid
|
(define host/-sess-next-sid
|
||||||
(fn ()
|
(fn ()
|
||||||
(let ((n (+ 1 (or (persist/backend-kv-get host/session-store host/-sess-counter-key) 0))))
|
|
||||||
(begin
|
(begin
|
||||||
(persist/backend-kv-put host/session-store host/-sess-counter-key n)
|
(set! host/session-ctr (+ host/session-ctr 1))
|
||||||
(str "s" n)))))
|
(str "s" host/session-epoch "-" host/session-ctr))))
|
||||||
|
|
||||||
;; ── backend io fn: dispatch session/* ops onto the persist KV ───────
|
;; ── backend io fn: dispatch session/* ops onto the persist KV ───────
|
||||||
|
;; LAZY: session/create mints a sid but writes NO row, so an anonymous request
|
||||||
|
;; (which never sets a field) leaves no durable trace — the store isn't spammed by
|
||||||
|
;; crawlers. The row appears on the first session/set (i.e. login), so a logged-in
|
||||||
|
;; session persists and survives a restart; session/exists is "has a written row".
|
||||||
(define host/session-backend
|
(define host/session-backend
|
||||||
(fn (op)
|
(fn (op)
|
||||||
(let ((kind (get op :op)))
|
(let ((kind (get op :op)))
|
||||||
(cond
|
(cond
|
||||||
((= kind "session/create")
|
((= kind "session/create") (host/-sess-next-sid))
|
||||||
(let ((sid (host/-sess-next-sid)))
|
|
||||||
(begin
|
|
||||||
(persist/backend-kv-put host/session-store (host/-sess-key sid) {})
|
|
||||||
sid)))
|
|
||||||
((= kind "session/exists")
|
((= kind "session/exists")
|
||||||
(persist/backend-kv-has? host/session-store (host/-sess-key (get op :sid))))
|
(persist/backend-kv-has? host/session-store (host/-sess-key (get op :sid))))
|
||||||
((= kind "session/get")
|
((= kind "session/get")
|
||||||
|
|||||||
@@ -126,6 +126,18 @@
|
|||||||
(dream-status (host-se-secure host-se-live-cookie)))
|
(dream-status (host-se-secure host-se-live-cookie)))
|
||||||
401)
|
401)
|
||||||
|
|
||||||
|
;; ── lazy persistence: only a written (logged-in) session leaves a durable row ──
|
||||||
|
(host-se-test "session/create writes no row (anonymous leaves no durable trace)"
|
||||||
|
(host/session-backend {:op "session/exists" :sid (host/session-backend {:op "session/create"})})
|
||||||
|
false)
|
||||||
|
(host-se-test "session/set creates the row (a login persists)"
|
||||||
|
(let ((sid (host/session-backend {:op "session/create"})))
|
||||||
|
(begin
|
||||||
|
(host/session-backend {:op "session/set" :sid sid :key :principal :val "bob"})
|
||||||
|
(list (host/session-backend {:op "session/exists" :sid sid})
|
||||||
|
(host/session-backend {:op "session/get" :sid sid :key :principal}))))
|
||||||
|
(list true "bob"))
|
||||||
|
|
||||||
(define host-se-tests-run!
|
(define host-se-tests-run!
|
||||||
(fn ()
|
(fn ()
|
||||||
{:total (+ host-se-pass host-se-fail)
|
{:total (+ host-se-pass host-se-fail)
|
||||||
|
|||||||
Reference in New Issue
Block a user