Skip to content

Commit a34955c

Browse files
joe4devclaude
andcommitted
fix(init): emit START after suppressed init to match AWS log ordering
The synthetic START line was written eagerly when LocalStack dispatched /invoke, i.e. before the suppressed init re-runs the function's static code. AWS emits START upon the Invoke event reaching the runtime, which rapidcore sequences after any inline (suppressed) init (doInvoke -> sendInvokeStartLogEvent). Emit START from a minimal LocalStackEventsAPI.SendInvokeStart override (riding rapidcore's correctly-placed invoke-start event) and drop the eager write in the /invoke handler. Correct for warm, cold, and suppressed-init invocations; fixes test_lambda_timeout_init_phase against the unmodified AWS snapshot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1f18f60 commit a34955c

3 files changed

Lines changed: 38 additions & 3 deletions

File tree

cmd/localstack/custom_interop.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ func NewCustomInteropServer(lsOpts *LsOpts, adapter *LocalStackAdapter, delegate
137137
}
138138

139139
invokeResp := &standalone.ResponseWriterProxy{}
140-
functionVersion := GetEnvOrDie("AWS_LAMBDA_FUNCTION_VERSION") // default $LATEST
141-
_, _ = fmt.Fprintf(logCollector, "START RequestId: %s Version: %s\n", invokeR.InvokeId, functionVersion)
140+
// The synthetic START line is emitted via LocalStackEventsAPI.SendInvokeStart so it
141+
// lands after any inline (suppressed) init, matching AWS — see events.go.
142142

143143
initDuration := ""
144144
if !server.warmStart && !server.initTimedOut.Load() {

cmd/localstack/events.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/interop"
7+
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/rapidcore/standalone/telemetry"
8+
)
9+
10+
// LocalStackEventsAPI rides rapidcore's invoke lifecycle events to emit the synthetic START
11+
// log line at the AWS-faithful point. rapidcore calls SendInvokeStart after any inline
12+
// (suppressed) init and before the runtime handles the invocation (see doInvoke in
13+
// internal/lambda/rapid/handlers.go), so emitting START here — rather than eagerly when
14+
// LocalStack dispatches /invoke — places it after a re-run init's logs, matching AWS.
15+
type LocalStackEventsAPI struct {
16+
*telemetry.StandaloneEventsAPI
17+
logCollector *LogCollector
18+
}
19+
20+
func NewLocalStackEventsAPI(logCollector *LogCollector) *LocalStackEventsAPI {
21+
return &LocalStackEventsAPI{
22+
StandaloneEventsAPI: new(telemetry.StandaloneEventsAPI),
23+
logCollector: logCollector,
24+
}
25+
}
26+
27+
func (e *LocalStackEventsAPI) SendInvokeStart(data interop.InvokeStartData) error {
28+
_, _ = fmt.Fprintf(e.logCollector, "START RequestId: %s Version: %s\n", data.RequestID, data.Version)
29+
return e.StandaloneEventsAPI.SendInvokeStart(data)
30+
}

cmd/localstack/main.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ func main() {
196196
RuntimeId: lsOpts.RuntimeId,
197197
}
198198

199+
// Events API rides rapidcore's invoke lifecycle to emit the synthetic START log line after
200+
// any inline (suppressed) init, matching AWS's ordering.
201+
lsEventsAPI := NewLocalStackEventsAPI(logCollector)
202+
199203
// build sandbox
200204
sandbox := rapidcore.
201205
NewSandboxBuilder().
@@ -207,7 +211,8 @@ func main() {
207211
SetExtensionsFlag(true).
208212
SetInitCachingFlag(true).
209213
SetLogsEgressAPI(localStackLogsEgressApi).
210-
SetTracer(tracer)
214+
SetTracer(tracer).
215+
SetEventsAPI(lsEventsAPI)
211216

212217
// Corresponds to the 'AWS_LAMBDA_RUNTIME_API' environment variable.
213218
// We need to ensure the runtime server is up before the INIT phase,

0 commit comments

Comments
 (0)