48614e622d
The dictionary lives on a persistent volume seeded from the image once and never re-seeded, so a redeploy that bumped BACKEND_DICT_VERSION left the new DAWGs unreachable (the volume mount shadows the image copy) and the active version stuck at whatever the admin console last installed — a native offline-first client then fetched the server's older pinned version over the network instead of using its bundled dict. On boot the backend now DELIVERS the build version onto the volume add-only, from a second unshadowed image copy at BACKEND_DICT_SEED_DIR, into DICT_DIR/<version>/ when absent (the flat seed and prior uploads untouched, so in-flight games keep theirs), and InitActiveVersion makes the build version active — except a console-installed version NEWER than the build (compared numerically) is not downgraded by a restart. The .seed_version guard still protects the flat seed's label (a bump is a new subdirectory, never a relabel). The console stays for out-of-band updates. Docs: ARCHITECTURE.md §5, deploy/README.md, seedmarker.go. Tests: engine.DeliverVersion (add-only, idempotent, flat-seed skip), compareDictVersions, and the dictionary-update integration test's deploy-delivers path.
54 lines
2.3 KiB
Go
54 lines
2.3 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// seedMarkerFile names the file, in the flat dictionary directory, that records the
|
|
// version the directory was first seeded as. It is dot-prefixed so OpenWithVersions'
|
|
// version scan skips it (like the .staging upload area).
|
|
const seedMarkerFile = ".seed_version"
|
|
|
|
// resolveSeedVersion returns the version label the flat dictionary directory is
|
|
// addressed by, recording it on first use.
|
|
//
|
|
// The contour's dictionary lives on a named volume seeded from the image once and
|
|
// never re-seeded (deploy/docker-compose.yml). The flat DAWGs carry no embedded
|
|
// version, so the version a volume was first seeded as is recorded in a
|
|
// .seed_version marker and is **authoritative** from then on:
|
|
//
|
|
// - fresh directory (no marker): record bootVersion (the build's
|
|
// BACKEND_DICT_VERSION) and return it — the seed of a fresh volume;
|
|
// - already-seeded directory: return the recorded marker and ignore bootVersion.
|
|
//
|
|
// So a later build seed never relabels the already-seeded flat bytes — which would void
|
|
// games pinned to the prior label and mis-serve new ones. The new build version is instead
|
|
// delivered as a NEW subdirectory (DeliverVersion) and made active (Service.InitActiveVersion),
|
|
// so a redeploy still moves new games to it while old games keep the flat label. New games
|
|
// pin the active version (DB-persisted); a console upload can also set it out-of-band.
|
|
//
|
|
// A directory that cannot be written makes the first record fail; that also breaks
|
|
// the admin console (which writes version subdirectories here), so the error is
|
|
// returned rather than swallowed, matching the package's fail-loud dictionary setup.
|
|
func resolveSeedVersion(dir, bootVersion string) (string, error) {
|
|
path := filepath.Join(dir, seedMarkerFile)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return "", fmt.Errorf("engine: read dictionary seed marker %s: %w", path, err)
|
|
}
|
|
if err == nil {
|
|
if recorded := strings.TrimSpace(string(data)); recorded != "" {
|
|
return recorded, nil
|
|
}
|
|
// An empty/corrupt marker falls through and is rewritten from bootVersion.
|
|
}
|
|
if werr := os.WriteFile(path, []byte(bootVersion+"\n"), 0o644); werr != nil {
|
|
return "", fmt.Errorf("engine: record dictionary seed marker %s: %w", path, werr)
|
|
}
|
|
return bootVersion, nil
|
|
}
|