Relay current process output without spool

This commit is contained in:
npc0-hue
2026-08-31 23:35:14 +08:00
parent cde99e19fa
commit c8f5213954
9 changed files with 239 additions and 90 deletions
+26 -46
View File
@@ -132,7 +132,7 @@ func TestTypedProcessOutputCaptureResumesAfterRunRestart(t *testing.T) {
waitForManagedTailers(t, restarted.managed.(*OSManagedProcessSupervisor))
}
func TestManagedProcessOutputAfterCanceledRunContextIsSpooledOnRestart(t *testing.T) {
func TestManagedProcessOutputAfterCanceledRunContextIsNotSpooledOnRestart(t *testing.T) {
root := t.TempDir()
assignment := executionAssignment(protocol.RunCapabilityProcessStart)
assignment.ExecutionInput.LogSources = []protocol.RuntimeLogSourcePlan{
@@ -199,24 +199,14 @@ func TestManagedProcessOutputAfterCanceledRunContextIsSpooledOnRestart(t *testin
WithProcessLogSink(&SpoolLogSink{RunEndpointID: assignment.RunEndpointID, SessionToken: "new-session", Spool: restartedSpool}),
)
restarted.ResumeManagedProcessLogs(context.Background())
waitForSpooledText(t, restartedSpool, "managed stdout triggered after-cancel", "managed stderr triggered after-cancel")
restartedManaged := restarted.managed.(*OSManagedProcessSupervisor)
resumed := waitForManagedOffsets(t, restartedManaged, scope, identity.StdoutOffset, identity.StderrOffset)
if resumed.LogSessionID != identity.LogSessionID || resumed.StdoutOffset <= identity.StdoutOffset || resumed.StderrOffset <= identity.StderrOffset {
t.Fatalf("restart did not retain session and commit offsets: before=%+v after=%+v", identity, resumed)
time.Sleep(300 * time.Millisecond)
if pending := mustPendingLogs(t, restartedSpool); len(pending) != 0 {
t.Fatalf("restart replayed output observed before Run restart: %+v", pending)
}
for _, batch := range mustPendingLogs(t, restartedSpool) {
if batch.LogSessionID != identity.LogSessionID || !batch.SessionStartedAt.Equal(identity.StartedAt) {
t.Fatalf("spooled batch lost process session metadata: %+v", batch)
}
if batch.FirstSeq != 1 || len(batch.Entries) == 0 || batch.Entries[0].Seq != 1 {
t.Fatalf("fresh generation stream did not start durably at sequence 1: %+v", batch)
}
for index, entry := range batch.Entries {
if entry.Seq != uint64(index+1) {
t.Fatalf("fresh generation stream sequence is not contiguous: %+v", batch)
}
}
restartedManaged := restarted.managed.(*OSManagedProcessSupervisor)
resumed := restartedManaged.Status(ProcessIdentity{Scope: scope})
if resumed.LogSessionID != identity.LogSessionID || resumed.StdoutOffset != identity.StdoutOffset || resumed.StderrOffset != identity.StderrOffset {
t.Fatalf("restart changed output offsets without observing new output: before=%+v after=%+v", identity, resumed)
}
stop := executionAssignment(protocol.RunCapabilityProcessStop)
stop.TargetKey = "actions/stop.json"
@@ -293,7 +283,7 @@ func TestImmediateManagedProcessRestartTailsNewGeneration(t *testing.T) {
waitForManagedTailers(t, managed)
}
func TestManagedProcessRestartRetainsUndrainedRetiredGeneration(t *testing.T) {
func TestManagedProcessRestartDoesNotRetainUndrainedRetiredGeneration(t *testing.T) {
root := t.TempDir()
assignment := executionAssignment(protocol.RunCapabilityProcessStart)
assignment.ExecutionInput.LogSources = []protocol.RuntimeLogSourcePlan{{Kind: "process.stdout", StreamKey: "game.console.stdout"}, {Kind: "process.stderr", StreamKey: "game.console.stderr"}}
@@ -328,9 +318,9 @@ func TestManagedProcessRestartRetainsUndrainedRetiredGeneration(t *testing.T) {
t.Fatalf("process restart reused log session: first=%+v second=%+v", first, second)
}
oldManaged.mu.Lock()
if len(oldManaged.retired) != 1 {
if len(oldManaged.retired) != 0 {
oldManaged.mu.Unlock()
t.Fatalf("expected one durable retired generation, got %+v", oldManaged.retired)
t.Fatalf("expected no durable retired generation, got %+v", oldManaged.retired)
}
oldManaged.stopTailersLocked(first)
oldManaged.stopTailersLocked(second)
@@ -342,7 +332,10 @@ func TestManagedProcessRestartRetainsUndrainedRetiredGeneration(t *testing.T) {
}
restarted := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithProcessLogSink(&SpoolLogSink{RunEndpointID: assignment.RunEndpointID, SessionToken: "restart-session", Spool: logSpool}))
restarted.ResumeManagedProcessLogs(context.Background())
waitForSpooledText(t, logSpool, "retired-a", "current-b")
time.Sleep(300 * time.Millisecond)
if pending := mustPendingLogs(t, logSpool); len(pending) != 0 {
t.Fatalf("restart replayed retired output: %+v", pending)
}
restartedManaged := restarted.managed.(*OSManagedProcessSupervisor)
waitForManagedTailers(t, restartedManaged)
restartedManaged.mu.Lock()
@@ -353,7 +346,7 @@ func TestManagedProcessRestartRetainsUndrainedRetiredGeneration(t *testing.T) {
}
}
func TestManagedProcessSourceCursorDeduplicatesCommittedLineWithStaleJournalOffset(t *testing.T) {
func TestManagedProcessRestartDoesNotReplayHistoricalOutput(t *testing.T) {
root := t.TempDir()
stateDir := filepath.Join(root, "state")
outputDir := filepath.Join(stateDir, "process-output")
@@ -361,7 +354,7 @@ func TestManagedProcessSourceCursorDeduplicatesCommittedLineWithStaleJournalOffs
t.Fatalf("create process output dir: %v", err)
}
line := "committed before offset\n"
identity := ProcessIdentity{Scope: filepath.Join(root, "instances", "server-execution", "local"), ServerInstanceID: "server-execution", RunEndpointID: "run-execution", JobID: "execution-job", Capability: protocol.RunCapabilityProcessStart, ProfileKey: "local", LogSessionID: "session-stale-offset", PID: 12345, StartedAt: time.Now().UTC(), State: "exited", StdoutLogRef: "stale.stdout.log", StderrLogRef: "stale.stderr.log", StdoutStreamKey: "game.console.stdout", StderrStreamKey: "game.console.stderr"}
identity := ProcessIdentity{Scope: filepath.Join(root, "instances", "server-execution", "local"), ServerInstanceID: "server-execution", RunEndpointID: "run-execution", JobID: "execution-job", Capability: protocol.RunCapabilityProcessStart, ProfileKey: "local", LogSessionID: "session-stale-offset", PID: os.Getpid(), StartedAt: time.Now().UTC(), State: "running", StdoutLogRef: "stale.stdout.log", StderrLogRef: "stale.stderr.log", StdoutStreamKey: "game.console.stdout", StderrStreamKey: "game.console.stderr"}
if err := os.WriteFile(filepath.Join(outputDir, identity.StdoutLogRef), []byte(line), 0o600); err != nil {
t.Fatalf("write stale stdout: %v", err)
}
@@ -375,34 +368,21 @@ func TestManagedProcessSourceCursorDeduplicatesCommittedLineWithStaleJournalOffs
if err := os.WriteFile(filepath.Join(stateDir, "processes.json"), body, 0o600); err != nil {
t.Fatalf("write process journal: %v", err)
}
logSpool, err := spool.NewLogSpool(filepath.Join(root, "spool"))
if err != nil {
t.Fatalf("new log spool: %v", err)
}
assignment := assignmentFromProcessIdentity(identity)
sink := &SpoolLogSink{RunEndpointID: identity.RunEndpointID, SessionToken: "old-run-session", Spool: logSpool}
if err := sink.AppendWithCursor(context.Background(), assignment, "stdout", strings.TrimSpace(line), ProcessLogCursor{StartOffset: 0, EndOffset: int64(len(line))}); err != nil {
t.Fatalf("commit line before offset: %v", err)
}
streamID := logStreamIDForAssignment(assignment, identity.StdoutStreamKey)
if err := logSpool.Ack(protocol.LogBatchIngestResponse{LogStreamID: streamID, AcceptedFrom: 1, AcceptedTo: 1}); err != nil {
t.Fatalf("ack committed line: %v", err)
}
restartedSpool, err := spool.NewLogSpool(filepath.Join(root, "spool"))
if err != nil {
t.Fatalf("restart log spool: %v", err)
}
restarted := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithProcessLogSink(&SpoolLogSink{RunEndpointID: identity.RunEndpointID, SessionToken: "new-run-session", Spool: restartedSpool}))
sink := &recordingLogSink{}
restarted := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithProcessLogSink(sink))
restarted.ResumeManagedProcessLogs(context.Background())
managed := restarted.managed.(*OSManagedProcessSupervisor)
resumed := waitForManagedOffsets(t, managed, identity.Scope, 0, -1)
waitForManagedTailers(t, managed)
time.Sleep(100 * time.Millisecond)
if resumed.StdoutOffset != int64(len(line)) {
t.Fatalf("stale journal offset was not advanced: %+v", resumed)
t.Fatalf("restart did not move live tail to the current output boundary: %+v", resumed)
}
if pending := mustPendingLogs(t, restartedSpool); len(pending) != 0 {
t.Fatalf("committed source cursor was enqueued twice: %+v", pending)
if lines := sink.snapshot(); len(lines) != 0 {
t.Fatalf("restart replayed historical output: %+v", lines)
}
managed.mu.Lock()
managed.stopTailersLocked(identity)
managed.mu.Unlock()
}
func TestManagedProcessSupervisorMigratesLiveLegacySession(t *testing.T) {