sun, 22:25 at the minemoon, waxing crescent, 36% lit - 22:25 at the mine(spicemine)μΩΩλΩ

sign inEN日本

mélangej · ABCL on Jooby and Netty

Development tools

The REPL is the tool

ABCL is a Common Lisp, so the loop is the one you already know:

core/gradlew -p core repl        # ABCL REPL on the project classpath

From there you can load the pieces and work on them directly. load of a source file interprets it; load of a .abcl gives you the compiled version:

CL-USER> (load "core/build/install/spicemine/lisp/spice-html.abcl")
CL-USER> (spice/html:html (list :p "it <works>"))
"<p>it &lt;works&gt;</p>"

Most UI work is done this way. Anything that does not need the request object renders straight into a string you can read:

SPICE/UI> (engine-block "fossil")
"<div class=\"engine\" id=\"e-fossil\"><h3>Fossil</h3>..."
SPICE/UI> (step-line 2 "and then this")
"<div class=\"step\"><b>2</b><span>and then this</span></div>"

Full pages take a context, so those are exercised through the browser suite rather than at the REPL — which is the right split: the fragment is a function, the page is a request.

Redefinition works as it should. Change a function, C-c C-c it (or paste it), and the next call uses the new one. Within one image that is instant, and it is the reason a Lisp REPL is worth more than a file watcher.

Editor: SLIME and SLY work

ABCL supports SLIME and SLY through swank. Measured on the machine and the ABCL this book is written on:

  (load ".../slime-v2.32/swank-loader.lisp")         0.5 s
  (swank-loader:init)   first time, compiles swank  13.6 s   (writes 21 fasls)
  (swank-loader:init)   every later image           11.0 s
  (swank:create-server :port 4005)                   instant

Thirteen seconds once, eleven seconds per fresh image. Not fast, not a problem, and nothing like "does not finish."

Setup, in a REPL on the project classpath:

(load "~/quicklisp/dists/quicklisp/software/slime-v2.32/swank-loader.lisp")
(swank-loader:init)
;; On ABCL, start the server in its own thread - see below.
(threads:make-thread
 (lambda () (swank:create-server :port 4005 :dont-close t)))

then M-x slime-connect to 4005. Verified rather than assumed: a raw (:emacs-rex (swank:connection-info) ...) written to that socket comes back with 19 KB of plist naming ARMEDBEAR and the process id, which is the same handshake Emacs performs.

Why the thread

(swank:create-server ...) blocks the thread that calls it until a client connects. Called at the end of a --load script it would hold the script there, waiting for Emacs. Hence make-thread above.

Connecting to the running server

The setup above gives you a REPL beside the server. To get one inside it — which is what makes a running instance inspectable, and what chapter 10's hot-upgrade section depends on — start swank from the server image itself. spicemine does this when spice.swank.port is set:

JAVA_OPTS="-Dspice.swank.port=4005" spicemine-server

That is a development and operations tool, not a feature: a swank port is a remote code-execution port with no authentication. Bind it to localhost, reach it over an SSH tunnel, and never open it to a network.

If you do not want an editor integration at all, ABCL's bare REPL plus core/gradlew repl covers most of it, and that is how this project is mostly worked on.

Running while you work

core/gradlew -p core runCore -Pport=8643     # loads .lisp sources, no compile step
core/gradlew -p core installDist             # packaged, compiled to fasls

runCore restarts in about two seconds (chapter 11). Even so, the thing to avoid is a restart-per-change habit: work in the REPL, restart when you have changed the route table.

Tests

core/run-tests.sh                            # the Lisp suites

Each file in src/test/lisp/ runs against an installed distribution. Every check prints ok or FAIL with what it got, and the runner fails if any line says FAIL, if a test signals an error, or if a file never reaches its done line — a suite that dies halfway must not pass. 156 checks today.

A check is deliberately plain:

(check "reserved name refused"
       (nth-value 1 (slug-available-p "settings"))
       "that name is reserved")

No framework, no fixtures, no assertion DSL. The file is a script.

Browser tests live in src/test/e2e/ and need Playwright, which is not a project dependency — point an environment variable at an installation:

export PLAYWRIGHT=/path/to/node_modules/playwright/index.mjs
ADMIN_TOKEN=smt_... BASE=http://127.0.0.1:8744 node core/src/test/e2e/auth.e2e.mjs

136 checks, for things only a browser can answer: that a cookie is HttpOnly and dies server-side after sign-out; that a forged POST into someone else's space is refused by the handler and not merely hidden by the form; that a tooltip stays on screen at 390px; that every page renders in both languages; that every skin is legible at AA contrast.

Benchmarks

core/bench/ holds five control groups, so a number never stands alone:

core/bench/run-all.sh                  # six servers, the 35 KB page
core/bench/run-static.sh               # static files
core/bench/memprobe.sh                 # memory attribution via NMT
MATRIX=tuning core/bench/memprobe.sh   # what fits a small host

Each script starts one server at a time, verifies every payload against the Java reference before timing anything, and refuses to run below a memory floor. Chapter 11 has the results.

Read core/bench/README.md before changing any of it. Measuring a web server is mostly a matter of not measuring your own instrument, and that file is the list of ways this harness has been wrong — a generator with a 50,000 rps ceiling, worker processes drifting out of step, an interpreted contestant in a field of compiled ones, a heap cap that reads as a dying server. Each produced numbers that looked plausible and were wrong by between 2× and 50×.

Recovery

spicemine-admin list
spicemine-admin create NAME [--admin]
spicemine-admin passwd NAME
spicemine-admin token  NAME [label]
spicemine-admin invite ["note"]

It opens the same database the server does and needs no credential but a shell. It exists because an instance once locked its owner out — the first-boot token aged out of the system journal before anyone had set a password — and the documentation said recovery was "an admin acting on the database", which was a sentence with nothing behind it.

Three details worth copying into any tool like it: the password is read from the terminal with echo off and passed in the environment, because argv is readable by every user through ps; setting a password revokes that account's live sessions, because a reset that leaves them alive has not taken the account back; and it must run as the service user, or sqlite leaves root-owned journal files and the server loses write access to its own database.

← SkinsDeployment →