package stopevidence import ( "context" "errors" "path/filepath" "sort" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/controlname" ) var ( ErrFence = errors.New("stopevidence: matching stop fence required") ErrNoCgroupEvidence = errors.New("stopevidence: cgroup observation required") ErrPopulated = errors.New("stopevidence: cgroup populated") ErrMembers = errors.New("stopevidence: cgroup has members") ErrReplicatorsOpen = errors.New("stopevidence: replicators not closed") ErrFDHolders = errors.New("stopevidence: database descriptors still held") ) type Inputs struct { Host string Generation uint64 Fence *activator.StopEvidence // Cgroup is the host's PARENT handle (cgroup.Manager.OpenCleanup), never // one generation's child: under the generation layout the parent is // empty only when every generation is (zero-downtime spec ยง7.3), and its // Members walks every cgroup below it. Cgroup interface { Populated() (bool, error) Members() ([]int, error) } DBPaths []string ProcRoot string ReplicatorsClosed bool } type Evidence struct { Host string Generation uint64 CheckedAt time.Time FenceClosed bool SpawnAttemptsDrained bool CgroupUnpopulated bool CgroupMembersEmpty bool ReplicatorsClosed bool FDScanComplete bool CheckedPaths []string ProcRoot string } func Collect(ctx context.Context, in Inputs) (Evidence, error) { if ctx == nil { return Evidence{}, ErrInvalidInput } if err := ctx.Err(); err != nil { return Evidence{}, err } if !controlname.ValidHostShape(in.Host) || in.Generation == 0 { return Evidence{}, ErrInvalidInput } if in.Fence == nil || !in.Fence.Matches(in.Host, in.Generation) { return Evidence{}, ErrFence } if in.Cgroup == nil { return Evidence{}, ErrNoCgroupEvidence } if !in.ReplicatorsClosed { return Evidence{}, ErrReplicatorsOpen } if len(in.DBPaths) == 0 { return Evidence{}, ErrInvalidInput } seen := map[string]bool{} for _, db := range in.DBPaths { if !filepath.IsAbs(db) { return Evidence{}, ErrInvalidInput } db = filepath.Clean(db) seen[db], seen[db+"-wal"], seen[db+"-shm"] = true, true, true } paths := make([]string, 0, len(seen)) for path := range seen { paths = append(paths, path) } sort.Strings(paths) populated, err := in.Cgroup.Populated() if err != nil { return Evidence{}, err } if populated { return Evidence{}, ErrPopulated } members, err := in.Cgroup.Members() if err != nil { return Evidence{}, err } if len(members) != 0 { return Evidence{}, ErrMembers } root := in.ProcRoot if root == "" { root = "/proc" } holders, err := scanFDs(ctx, root, paths) if err != nil { return Evidence{}, err } if len(holders) != 0 { return Evidence{}, ErrFDHolders } if err := ctx.Err(); err != nil { return Evidence{}, err } return Evidence{ Host: in.Host, Generation: in.Generation, CheckedAt: time.Now().UTC(), FenceClosed: true, SpawnAttemptsDrained: true, CgroupUnpopulated: true, CgroupMembersEmpty: true, ReplicatorsClosed: true, FDScanComplete: true, CheckedPaths: paths, ProcRoot: root, }, nil } func Cleanup(ctx context.Context, c *cgroup.Cleanup) error { if c == nil { return ErrInvalidInput } return c.KillAndEmpty(ctx) }