Skip to content

Commit a6c21d0

Browse files
joe4devclaude
andcommitted
refactor(init): derive the init error type per attempt; drop ClearInitError
The recorded init error type was sticky across init attempts and cleared cross-component by the invoke handler after a successful invocation. Stickiness is unnecessary: every invocation into a failed-init environment starts a fresh suppressed Init phase (rapidcore shuts the runtime down after an init failure so the next FastInvoke re-inits), so each failing attempt re-records its error type via its own SendInitReport. Resetting the field in SendInitStart therefore keeps the same behavior with one mechanism instead of two. This also fixes a mislabeling bug: previously, when a suppressed init re-run recovered but the invocation itself then died fatally (ErrInvokeDoneFailed), the stale sticky error type tainted that invocation's REPORT line with the original init failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6446325 commit a6c21d0

3 files changed

Lines changed: 28 additions & 27 deletions

File tree

cmd/localstack/custom_interop.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,7 @@ func NewCustomInteropServer(lsOpts *LsOpts, adapter *LocalStackAdapter, delegate
159159
timeout := int(server.delegate.GetInvokeTimeout().Seconds())
160160
isErr := false
161161
status := ""
162-
if err == nil {
163-
// The invocation succeeded: if an earlier init failure was folded into it
164-
// and the suppressed init re-run recovered, the result stands on its own —
165-
// AWS reports it as successful — so clear the recorded init failure.
166-
server.eventsAPI.ClearInitError()
167-
} else {
162+
if err != nil {
168163
switch {
169164
case errors.Is(err, rapidcore.ErrInvokeTimeout):
170165
log.Debugf("Got invoke timeout")

cmd/localstack/events.go

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ type LocalStackEventsAPI struct {
4949
// starting the runtime process, so treat empty as an error.
5050
lastInitStatus string
5151
lastInitErrorType string
52-
// initErrorType is the scrubbed fatal error type of the most recent failed init. Unlike
53-
// lastInitErrorType it is sticky across init attempts (each failing invocation re-runs the
54-
// init as a suppressed init and AWS re-emits the failure envelope) and is cleared by the
55-
// invoke handler once an invocation succeeds, so a recovered environment is not tainted by
56-
// the original failure.
52+
// initErrorType is the scrubbed fatal error type of the most recent failed init attempt,
53+
// reset on SendInitStart. No cross-attempt stickiness is needed: every invocation into a
54+
// failed-init environment starts a fresh suppressed Init phase (rapidcore shuts the runtime
55+
// down after an init failure so the next FastInvoke re-inits — see Server.Invoke in
56+
// rapidcore/server.go), so each failing invocation re-records the failure via its own
57+
// SendInitReport, and a successful re-run leaves it empty — a recovered environment is not
58+
// tainted by the original failure.
5759
initErrorType string
5860
// initTimedOut is set by main.go before it resets a timed-out init phase, so the aborted
5961
// init's INIT_REPORT renders as AWS's "Status: timeout" (without an error type) instead of
@@ -75,7 +77,7 @@ func NewLocalStackEventsAPI(logCollector *LogCollector, onDemand bool) *LocalSta
7577

7678
func (e *LocalStackEventsAPI) SendInitStart(data interop.InitStartData) error {
7779
e.mu.Lock()
78-
e.lastInitStatus, e.lastInitErrorType = "", ""
80+
e.lastInitStatus, e.lastInitErrorType, e.initErrorType = "", "", ""
7981
e.mu.Unlock()
8082
return nil
8183
}
@@ -156,18 +158,9 @@ func (e *LocalStackEventsAPI) TakeColdStartInitDuration() (durationMS float64, o
156158
}
157159

158160
// InitErrorType returns the scrubbed fatal error type (e.g. Runtime.ExitError) of the most
159-
// recent failed init, or "" if the latest init succeeded or none was recorded.
161+
// recent failed init attempt, or "" if the latest init attempt succeeded or none was recorded.
160162
func (e *LocalStackEventsAPI) InitErrorType() string {
161163
e.mu.Lock()
162164
defer e.mu.Unlock()
163165
return e.initErrorType
164166
}
165-
166-
// ClearInitError resets the recorded init failure once an invocation succeeded: the
167-
// suppressed init re-run recovered, so later invocations must not be tainted by the
168-
// original failure.
169-
func (e *LocalStackEventsAPI) ClearInitError() {
170-
e.mu.Lock()
171-
defer e.mu.Unlock()
172-
e.initErrorType = ""
173-
}

cmd/localstack/events_test.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,13 @@ func TestEventsAPI_TimedOutInit_RendersTimeoutStatusOnce(t *testing.T) {
119119
logs.getLogs().Logs)
120120
}
121121

122-
func TestEventsAPI_RecoveredSuppressedInit_NoLineAndErrorClearable(t *testing.T) {
122+
func TestEventsAPI_RecoveredSuppressedInit_NoLineAndErrorCleared(t *testing.T) {
123123
logs := NewLogCollector()
124124
e := NewLocalStackEventsAPI(logs, true)
125125

126126
// Cold-start init fails ...
127127
sendInit(t, e, "init", "error", "Runtime.Unknown", 5.0)
128+
assert.Equal(t, "Runtime.Unknown", e.InitErrorType())
128129
logs.reset()
129130
// ... and the suppressed init re-run at the first invocation recovers.
130131
sendInit(t, e, "invoke", "success", "", 6.0)
@@ -135,13 +136,25 @@ func TestEventsAPI_RecoveredSuppressedInit_NoLineAndErrorClearable(t *testing.T)
135136
_, ok := e.TakeColdStartInitDuration()
136137
assert.False(t, ok)
137138

138-
// The sticky failure record survives until the invoke handler observes a successful
139-
// invocation and clears it.
140-
assert.Equal(t, "Runtime.Unknown", e.InitErrorType())
141-
e.ClearInitError()
139+
// The failure record is per init attempt: the successful re-run resets it, so the
140+
// recovered invocation's REPORT is not tainted by the original failure — even if the
141+
// invocation itself later dies fatally.
142142
assert.Empty(t, e.InitErrorType())
143143
}
144144

145+
func TestEventsAPI_RepeatedFailingSuppressedInit_ReRecordsEachAttempt(t *testing.T) {
146+
logs := NewLogCollector()
147+
e := NewLocalStackEventsAPI(logs, true)
148+
149+
// Cold-start init fails, then every invocation re-runs the suppressed init,
150+
// which fails again — each attempt re-records its own error type.
151+
sendInit(t, e, "init", "error", "Runtime.Unknown", 5.0)
152+
sendInit(t, e, "invoke", "error", "Runtime.ExitError", 2.0)
153+
assert.Equal(t, "Runtime.ExitError", e.InitErrorType())
154+
sendInit(t, e, "invoke", "error", "Runtime.Unknown", 2.1)
155+
assert.Equal(t, "Runtime.Unknown", e.InitErrorType())
156+
}
157+
145158
func TestEventsAPI_InvokeStart_EmitsStartLine(t *testing.T) {
146159
logs := NewLogCollector()
147160
e := NewLocalStackEventsAPI(logs, true)

0 commit comments

Comments
 (0)