Skip to content

Engine shutdown leaks a relay port and a hub subscription per aux child #707

Description

@rainmanjam

From the poka-yoke audit of 2026-09-04. Lens: fixed-value. Today: rung 3 (incomplete). Device reaches: rung 1 (Control). Silent. MEASURED.

The mistake available

Read stop(recorder) as equivalent to teardownDest(d). It is not.

internal/engine/engine.go:1021-1027:

stop := func(p *supervisor.Process) {
	if p == nil { return }
	wg.Add(1)
	go func() { defer wg.Done(); _ = p.Stop(ctx) }()
}

That is the whole of it. Destinations, loudness, clips, captions, feeds,
silence, backup and playlist go through a teardown* that releases the port and
unsubscribes the hub. Recorder, preview, meters and renditions go through
stop, which does neither.

Verified mechanically: StopWithin contains zero occurrences of
alloc.Release or Unsubscribe. teardownDest contains two.

Measured

A temporary test in internal/engine with a span-1 allocator, control included:

CONTROL OK: destination port returned                        PASS
LEAK: e.recorderPort was never released by StopWithin        FAIL
LEAK: e.previewPort  was never released by StopWithin        FAIL
LEAK: e.metersPort   was never released by StopWithin        FAIL
LEAK: rendition.port was never released by StopWithin        FAIL
LEAK: hub still holds subscribers after StopWithin: [rend:1] FAIL

Blast radius

e.alloc is one 500-port pool shared across every engine
(manager.go:144, engine.go:609). Manager.Sync stops an engine on every
source delete while the daemon keeps running, so each source delete burns
three ports plus one per rendition, for the life of the process.

Nothing reports it until Allocate starts failing — and then it fails
everywhere at once and reads as an unrelated fault. buildBackup says "the
primary is unaffected"; reconcileMeters just returns.

manager.go:334 states the invariant this does not hold: "Stop first, so a
source that was deleted releases its ports."

Today

Detection, with the same missing members as the code:
dest_shutdown_test.go:63 and engine_gap_shutdown_test.go:92 assert release
for destinations and feeds only.

Device → rung 1, Control

Allocate() returns a lease the engine holds in a set; StopWithin drains the
set rather than enumerating fields. A child kind added later cannot obtain a
port without joining the set, so it cannot leak one.

What stops it being free: thirteen sites store a bare int
(destination.port, rendition.port, e.recorderPort, …) and all change
shape. Shares one device with the double-release issue — do both together.

Interim → Warning: have the allocator record an owner and log at error if
any lease survives StopWithin.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions