261026.45 · 5f6ee550d2d23f8cb4b04d6d99eb10c407166b5e
make install's unit is www-dev, and a trim spares a locked repo
make install wrote its user unit under the package's name, so it hid the package's user unit. It also stood aside for www@USER. Since --restart serves only its own binary's trees, one install's persistent trees then stayed down at every login. The unit is now www-dev.service, from systemd/dev.service.in, with no condition; it runs beside the package's units. Only the package's two units still exclude each other, since they run one binary. install disables and removes an older www.service it wrote.
Every server shares ~/.cache/www, but a trim deleted repos without their lock. One server could delete a repo another was fetching. drop now takes the lock without waiting and skips a held repo, and a new repo stays when only such repos keep the cache over its limit.
reading declarations
@@ -47,15 +47,20 @@ clean: # binary started, so the package keeps its servers. install-deb removes the # make install link, so it takes every server with --force. A server on # port 80 stays put under the binary from make install, which cannot bind-# it. install also ships the-# user unit, disabled, which serves .LOCK trees again at login; it never-# uses sudo, so the system template comes only with the package.-USERUNIT := $(HOME)/.config/systemd/user/$(NAME).service+# it. install also ships a+# user unit of its own name, disabled, which serves its build's .LOCK trees+# again at login, beside the package's units; it never uses sudo, so the+# system template comes only with the package. An older install named its+# unit as the package's, which hid the package's user unit; install+# disables and removes it.+USERUNIT := $(HOME)/.config/systemd/user/$(NAME)-dev.service+OLDUNIT := $(HOME)/.config/systemd/user/$(NAME).service install: build ln -sf $(CURDIR)/build/$(NAME) $(HOME)/.local/bin/$(NAME) mkdir -p $(dir $(USERUNIT))- sed 's/@NAME@/$(NAME)/g; s|@BIN@|$(HOME)/.local/bin/$(NAME)|' systemd/user.service.in > $(USERUNIT)+ sed 's/@NAME@/$(NAME)/g; s|@BIN@|$(HOME)/.local/bin/$(NAME)|' systemd/dev.service.in > $(USERUNIT)+ -grep -qs '^ExecStart=$(HOME)/.local/bin/' $(OLDUNIT) && { systemctl --user disable $(NAME); rm $(OLDUNIT); } -systemctl --user daemon-reload -build/$(NAME) --restart
@@ -40,7 +40,7 @@ To reach the server by name, add a line such as `127.31.1.1 www` to `/etc/hosts` - `-p`, `--persist`: write a persistent lock, such as `127.31.1.1-8080.LOCK`, so the tree is served again after a reboot; see [Serving after reboot](#serving-after-reboot) - `--debug`: show what serves debugging: above each page's header, the build stamp, see [debugging](doc/debugging.md); on the index, each server's build and start time and each lock's time. `make run` sets it; `--restart` passes it to the new servers when given, and leaves it off otherwise - `--cache=SIZE`: memory a new server may spend on cached git answers, eg `64M`; `0` caches nothing. The default is `32M`, and `--restart` goes back to it-- `--disk=SIZE`: disk a new server may fill with online repos, eg `4G`; it drops the repos read longest ago. The default is `2G`, and `--restart` goes back to it+- `--disk=SIZE`: disk a new server may fill with online repos, eg `4G`; it drops the repos read longest ago, but not one that another server is fetching, since every server shares the cache. The default is `2G`, and `--restart` goes back to it - `--restart`: replace every server this binary started with one running it now, serving the same trees, and print each tree's URL; an address with persistent trees and no server gets one, when this binary served it last; with `-a` or `--share`, only the server on that address, whichever binary runs it; with a directory, it also serves that directory there and opens it in the browser unless a page of it was open, and says which - `-f`, `--force`: with `--restart`, take the servers and persistent trees of other binaries too - `-l`, `--list`: print each served tree's directory, URL and binary as keyval, on every address; with `-a` or `--share`, on that address only; with a directory, only the trees at or below it. A directory that holds no served tree prints nothing; one that does not exist fails with `E2`@@ -75,8 +75,8 @@ A fourth holds a share's token. `WWW_SECRET` gives `--secret` a token of letters `www -p` writes a lock in capitals, such as `127.31.1.1-8080.LOCK`. It holds no PID, and it outlives its server: Kill server, `--kill`, SIGTERM and a reboot all stop the server but leave the `.LOCK` in place. The next server on that address serves the tree again, and so does `www --restart`, which starts a server for every address whose persistent trees have none. Deleting the `.LOCK`, or Delete lock on the tree's pages, ends the tree for good. `www -p` in a tree served by a plain `.lock` moves it to a `.LOCK`, and a plain `www` in a tree that holds a `.LOCK` keeps it. -A systemd user unit runs `www --restart` at login. `make install` puts it in `~/.config/systemd/user/` and the package in `/usr/lib/systemd/user/`, both disabled; enable it with `systemctl --user enable www`. It serves from login to logout, and linger stays off.-The package also ships a system template, `www@.service`, which serves a user's trees from boot, before login, with the capability for port 80. Enable it with `sudo systemctl enable www@<user>`. While either unit runs for a user, the other stands aside, since both would serve the same trees.+A systemd user unit runs `www --restart` at login. The package puts `www.service` in `/usr/lib/systemd/user/`; enable it with `systemctl --user enable www`. `make install` puts `www-dev.service` in `~/.config/systemd/user/`; enable it with `systemctl --user enable www-dev`. Both ship disabled. They serve from login to logout, and linger stays off. Each brings back only the trees its own binary served last, so with both installs both units run.+The package also ships a system template, `www@.service`, which serves a user's trees from boot, before login, with the capability for port 80. Enable it with `sudo systemctl enable www@<user>`. While the template or the package's user unit runs for a user, the other stands aside, since both run one binary on the same trees. `www-dev` runs beside either. ## Debugging
@@ -0,0 +1,17 @@+# make install's user unit serves the trees that its build's .LOCK files+# name, from login to logout. Enable it with:+# systemctl --user enable @NAME@-dev+# --restart starts a server for each address whose registry links a .LOCK+# and names this build. The package's units serve the trees its binary+# served last, so this unit runs beside them.+# Stopping the unit sends its servers SIGTERM, which keeps every .LOCK.+[Unit]+Description=@NAME@-dev: serve the trees that make install's build last served++[Service]+Type=oneshot+RemainAfterExit=yes+ExecStart=@BIN@ --restart++[Install]+WantedBy=default.target
@@ -799,7 +799,7 @@ func (w *web) lockRepo(key, dir string) (func(), error) { w.mu.Lock() w.busy[key] = true w.mu.Unlock()- unlock, err := flockDir(dir)+ unlock, err := flockDir(dir, syscall.LOCK_EX) if err != nil { unlock = func() {} }@@ -812,13 +812,14 @@ func (w *web) lockRepo(key, dir string) (func(), error) { } // flockDir takes the repo directory's lock, which keeps two servers from-// fetching one repo at once.-func flockDir(dir string) (func(), error) {+// fetching one repo at once, and one from deleting a repo another fetches.+// how is LOCK_EX, or LOCK_EX|LOCK_NB to fail with EWOULDBLOCK when held.+func flockDir(dir string, how int) (func(), error) { f, err := os.OpenFile(filepath.Join(dir, ".lock"), os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, err }- if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {+ if err := flock(f, how); err != nil { f.Close() return nil, err }@@ -881,8 +882,9 @@ func diskUse(dir string) int64 { } // trim drops the repos requested longest ago until the cache fits its-// limit. keep, just fetched, goes last; alone over the limit it goes too,-// and trim says so.+// limit. A repo in use, fetched here or locked by another server, stays.+// keep, just fetched, goes last, and only when it alone is over the limit;+// trim then says so. func (w *web) trim(keep string) error { repos := w.onDisk() var total int64@@ -908,21 +910,48 @@ func (w *web) trim(keep string) error { if busy { continue }+ if c.key == keep && c.size <= w.limit {+ // Repos in use kept the cache over its limit; a later trim+ // drops them.+ logInfo("the cache stays over %s while repos in use fill it", sizeText(w.limit))+ return nil+ } if c.key == keep {- w.drop(c.key)+ // The caller holds keep's lock.+ w.remove(c.key) return &errno{syscall.EFBIG, fmt.Sprintf("%s takes %s, over the cache's limit of %s, so it was dropped; --disk sets the limit", c.key, sizeText(c.size), sizeText(w.limit))}- } else {- logInfo("drops %s, %s, last requested %s, since the cache is over %s", c.key, sizeText(c.size), c.used.Format(time.DateTime), sizeText(w.limit)) }- w.drop(c.key)+ if !w.drop(c.key) {+ logInfo("keeps %s, %s, though the cache is over %s, since another server is fetching it", c.key, sizeText(c.size), sizeText(w.limit))+ continue+ }+ logInfo("dropped %s, %s, last requested %s, since the cache is over %s", c.key, sizeText(c.size), c.used.Format(time.DateTime), sizeText(w.limit)) total -= c.size } return nil } -// drop stops serving the repo key and deletes it from the cache.-func (w *web) drop(key string) {+// drop stops serving the repo key and deletes it from the cache, unless+// another server holds its lock, and reports whether it did. The servers+// on every address share the cache, and each knows only its own busy+// repos.+func (w *web) drop(key string) bool {+ dir := filepath.Join(w.root, filepath.FromSlash(key))+ unlock, err := flockDir(dir, syscall.LOCK_EX|syscall.LOCK_NB)+ if errors.Is(err, syscall.EWOULDBLOCK) {+ return false+ }+ if err == nil {+ defer unlock()+ }+ w.remove(key)+ return true+}++// remove stops serving the repo key and deletes it from the cache. The+// caller holds its lock.+func (w *web) remove(key string) { w.mu.Lock() r := w.repos[key] w.mu.Unlock()
@@ -12,6 +12,7 @@ import ( "path/filepath" "strings" "sync/atomic"+ "syscall" "testing" "time" )@@ -297,6 +298,38 @@ func TestWebTrimsTheCache(t *testing.T) { } } +// The servers on every address share the cache, so a trim leaves a repo+// whose lock another server holds, eg while it fetches the repo.+func TestWebTrimLeavesALockedRepo(t *testing.T) {+ f := newForge(t, nil)+ f.repo(t, "acct/a", map[string]string{"README.md": strings.Repeat("a", 1<<16)})+ f.repo(t, "acct/b", map[string]string{"README.md": strings.Repeat("b", 1<<16)})+ w := testWeb(t, defaultDisk)+ read(t, w, "/"+f.host+"/acct/a/")+ a := filepath.Join(w.root, f.host, "acct", "a")+ past := time.Now().Add(-time.Hour)+ os.Chtimes(filepath.Join(a, ".used"), past, past)+ w.limit = w.onDisk()[0].size * 3 / 2+ // A lock of its own open file stands for another server's.+ unlock, err := flockDir(a, syscall.LOCK_EX)+ if err != nil {+ t.Fatal(err)+ }+ if rec := read(t, w, "/"+f.host+"/acct/b/"); rec.Code != http.StatusOK {+ t.Fatalf("b: %d\n%s", rec.Code, rec.Body)+ }+ if !exists(filepath.Join(a, ".git", "HEAD")) {+ t.Error("a went while another server held its lock")+ }+ unlock()+ if err := w.trim(""); err != nil {+ t.Fatal(err)+ }+ if exists(a) {+ t.Error("a stayed once its lock was free")+ }+}+ // A path that names no repo lists the account's repos: from the forge's // API where it answers, else from the cache. func TestWebLists(t *testing.T) {