@@ -12,14 +12,17 @@ import (
1212 "net/http"
1313 "strconv"
1414 "strings"
15+ "sync/atomic"
1516 "time"
1617
1718 "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/core/statejson"
19+ "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/fatalerror"
1820 "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/interop"
1921 "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/rapidcore"
2022 "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/rapidcore/standalone"
2123 "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lsapi"
2224 "github.com/go-chi/chi/v5"
25+ "github.com/google/uuid"
2326 log "github.com/sirupsen/logrus"
2427)
2528
@@ -28,11 +31,23 @@ type CustomInteropServer struct {
2831 localStackAdapter * LocalStackAdapter
2932 port string
3033 upstreamEndpoint string
34+ // logCollector accumulates the runtime's stdout/stderr plus the synthetic START/REPORT/
35+ // INIT_REPORT lines that are flushed to LocalStack with each invocation's logs.
36+ logCollector * LogCollector
3137 // initStart is set once in Init() and warmStart is flipped on the first invoke.
3238 // Both are accessed only from the single sequential init -> invoke flow (the RIE
3339 // processes one invocation at a time), so they need no additional synchronization.
3440 initStart time.Time
3541 warmStart bool
42+ // initTimedOut is set by ReportInitTimeout when the init phase exceeds its timeout. It is
43+ // written from the init-await flow and read from the invoke flow, so it uses atomic access.
44+ // When set, the first invocation's REPORT omits Init Duration (init was already reported as
45+ // timed out and is re-run as a suppressed init during that invocation).
46+ initTimedOut atomic.Bool
47+ // initErrorForwarded is set once the runtime's own /init/error has been forwarded to
48+ // LocalStack via SendInitErrorResponse, so the crash-path fallback (SendInitError) does
49+ // not send a duplicate error status for the same failed initialization.
50+ initErrorForwarded atomic.Bool
3651}
3752
3853type LocalStackAdapter struct {
@@ -96,13 +111,13 @@ func (l *LocalStackAdapter) SendResult(invokeId string, body []byte, isError boo
96111 return nil
97112}
98113
99-
100114func NewCustomInteropServer (lsOpts * LsOpts , adapter * LocalStackAdapter , delegate interop.Server , logCollector * LogCollector ) (server * CustomInteropServer ) {
101115 server = & CustomInteropServer {
102116 delegate : delegate .(* rapidcore.Server ),
103117 port : lsOpts .InteropPort ,
104118 upstreamEndpoint : lsOpts .RuntimeEndpoint ,
105119 localStackAdapter : adapter ,
120+ logCollector : logCollector ,
106121 }
107122
108123 // TODO: extract this
@@ -126,7 +141,7 @@ func NewCustomInteropServer(lsOpts *LsOpts, adapter *LocalStackAdapter, delegate
126141 _ , _ = fmt .Fprintf (logCollector , "START RequestId: %s Version: %s\n " , invokeR .InvokeId , functionVersion )
127142
128143 initDuration := ""
129- if ! server .warmStart && ! invokeR . IsInitRetry {
144+ if ! server .warmStart && ! server . initTimedOut . Load () {
130145 initTimeMS := float64 (time .Since (server .initStart ).Nanoseconds ()) / float64 (time .Millisecond )
131146 initDuration = fmt .Sprintf ("Init Duration: %.2f ms\t " , initTimeMS )
132147 }
@@ -225,9 +240,15 @@ func (c *CustomInteropServer) SendErrorResponse(invokeID string, resp *interop.E
225240 return c .delegate .SendErrorResponse (invokeID , resp )
226241}
227242
228- // SendInitErrorResponse forwards the init error to LocalStack and then propagates it to the delegate.
243+ // SendInitErrorResponse forwards the init error reported by the runtime (via /init/error) to
244+ // LocalStack and then propagates it to the delegate. It marks initErrorForwarded so the
245+ // crash-path fallback in main.go (SendInitError) does not send a duplicate error status for
246+ // the same failed initialization.
229247func (c * CustomInteropServer ) SendInitErrorResponse (resp * interop.ErrorInvokeResponse ) error {
230248 log .Traceln ("SendInitErrorResponse called" )
249+ // Mark synchronously, before sending: this runs in the init flow before
250+ // AwaitInitializedWithDetails unblocks in main.go, so the fallback observes the flag.
251+ c .initErrorForwarded .Store (true )
231252
232253 // Deserialize the raw payload so we can include the requestId and structured fields.
233254 var parsed struct {
@@ -267,6 +288,49 @@ func (c *CustomInteropServer) SendInitErrorResponse(resp *interop.ErrorInvokeRes
267288 return c .delegate .SendInitErrorResponse (resp )
268289}
269290
291+ // SendInitError reports a structured init failure to LocalStack when the runtime failed to
292+ // initialize WITHOUT calling /init/error itself (e.g. it crashed, called sys.exit, or had an
293+ // invalid entrypoint). The init failure is detected by the existing rapidcore machinery
294+ // (watchEvents -> InitFailure -> AwaitInitializedWithDetails) and surfaced to main.go.
295+ // It is a no-op if SendInitErrorResponse already forwarded the runtime's own structured error.
296+ func (c * CustomInteropServer ) SendInitError (errType fatalerror.ErrorType , errMsg error ) {
297+ if c .initErrorForwarded .Load () {
298+ log .Debug ("Init error already forwarded to LocalStack; skipping duplicate" )
299+ return
300+ }
301+
302+ if errType == "" {
303+ errType = fatalerror .RuntimeExit
304+ }
305+
306+ message := "Runtime exited during initialization"
307+ if errMsg != nil {
308+ message = errMsg .Error ()
309+ }
310+
311+ // Match AWS's fault message format "RequestId: <id> Error: <msg>". No invocation is active
312+ // during the init phase (LocalStack only dispatches an invoke after the runtime reports
313+ // ready), so synthesize a request ID, preferring the current invoke ID if one exists.
314+ requestID := c .delegate .GetCurrentInvokeID ()
315+ if requestID == "" {
316+ requestID = uuid .NewString ()
317+ }
318+
319+ payload , err := json .Marshal (lsapi.ErrorResponse {
320+ ErrorType : string (errType ),
321+ ErrorMessage : fmt .Sprintf ("RequestId: %s Error: %s" , requestID , message ),
322+ })
323+ if err != nil {
324+ log .WithError (err ).Error ("Failed to marshal init error response" )
325+ return
326+ }
327+
328+ if err := c .localStackAdapter .SendStatus (Error , payload ); err != nil {
329+ log .WithError (err ).WithField ("runtime-id" , c .localStackAdapter .RuntimeId ).
330+ Error ("Failed to send init error to LocalStack" )
331+ }
332+ }
333+
270334func (c * CustomInteropServer ) GetCurrentInvokeID () string {
271335 log .Traceln ("GetCurrentInvokeID called" )
272336 return c .delegate .GetCurrentInvokeID ()
@@ -283,6 +347,16 @@ func (c *CustomInteropServer) Init(i *interop.Init, invokeTimeoutMs int64) error
283347 return c .delegate .Init (i , invokeTimeoutMs )
284348}
285349
350+ // ReportInitTimeout emits an AWS-style INIT_REPORT timeout line into the log collector and
351+ // marks the init as timed out. The init is then re-run as a suppressed init during the first
352+ // invocation (under the function timeout), and that invocation's REPORT omits Init Duration.
353+ func (c * CustomInteropServer ) ReportInitTimeout () {
354+ c .initTimedOut .Store (true )
355+ initTimeMS := float64 (time .Since (c .initStart ).Nanoseconds ()) / float64 (time .Millisecond )
356+ _ , _ = fmt .Fprintf (c .logCollector ,
357+ "INIT_REPORT Init Duration: %.2f ms\t Phase: init\t Status: timeout\n " , initTimeMS )
358+ }
359+
286360func (c * CustomInteropServer ) Invoke (responseWriter http.ResponseWriter , invoke * interop.Invoke ) error {
287361 log .Traceln ("Invoke called" )
288362 return c .delegate .Invoke (responseWriter , invoke )
0 commit comments