feat(workload): container index reconciler

Background worker that keeps the containers table in sync with
docker ps. Runs one boot pass and ticks every 30s.

Dispatch precedence per container:
  1. tinyforge.workload.id label   (canonical, new)
  2. tinyforge.instance-id label   (legacy project — joins via instances)
  3. tinyforge.static-site label   (legacy site)
  4. com.docker.compose.project    (stacks — joins via ComposeProjectName)

Rows whose Docker container ID is no longer present are flipped
to state='missing'. Placeholder rows (empty container_id, e.g.
a deploy mid-flight) are left alone so a tick that races a
deploy doesn't mark them as missing.

DockerLister interface lets tests substitute a fake daemon —
6 unit tests cover the dispatch matrix, missing-sweep, and
state normalization.

Wired into cmd/server/main.go between docker.New and the
existing startup chain. Boot pass populates the containers
table from any pre-refactor running containers.
This commit is contained in:
2026-05-09 13:45:13 +03:00
parent b6f20599d7
commit af82be3fb8
4 changed files with 659 additions and 0 deletions
+12
View File
@@ -30,6 +30,7 @@ import (
"github.com/alexei/tinyforge/internal/notify"
"github.com/alexei/tinyforge/internal/npm"
"github.com/alexei/tinyforge/internal/proxy"
"github.com/alexei/tinyforge/internal/reconciler"
"github.com/alexei/tinyforge/internal/registry"
"github.com/alexei/tinyforge/internal/stale"
"github.com/alexei/tinyforge/internal/stack"
@@ -94,6 +95,17 @@ func main() {
}
defer dockerClient.Close()
// Start the container index reconciler. Runs one boot pass and then
// ticks every 30s. Boot pass populates the containers table from any
// running containers that predate the workload refactor; subsequent
// ticks catch state drift the deployer didn't witness (e.g., a stack
// service that exited on its own).
reconcilerCtx, reconcilerCancel := context.WithCancel(context.Background())
defer reconcilerCancel()
rec := reconciler.New(db, dockerClient, 30*time.Second)
rec.Start(reconcilerCtx)
defer rec.Stop()
// Read settings for NPM URL and polling interval.
settings, err := db.GetSettings()
if err != nil {