Skip to content

Commit 4375b13

Browse files
joe4devclaude
andcommitted
refactor(init): keep upstream server.go pristine; move init-timeout helper to its own file
The init-phase timeout / suppressed-init support previously edited the vendored upstream internal/lambda/rapidcore/server.go (exported InitCompletionResponse, extracted interpretInitFailure, added AwaitInitializedWithDetails and AwaitInitializedWithTimeout). Modifying vendored upstream files causes rebase conflicts when syncing with aws-lambda-runtime-interface-emulator. Revert server.go to byte-identical upstream and move the only load-bearing addition (AwaitInitializedWithTimeout, plus a local InitCompletionResponse and a duplicated interpretInitFailure) into a new same-package file server_localstack.go. It must stay in package rapidcore because it needs the unexported getInitFailuresChan()/Release()/setRuntimeState() helpers, but as a standalone file it never conflicts on rebase. AwaitInitializedWithDetails was unused by cmd/localstack and is dropped. No behavior change: RIE go test ./... and TestLambdaErrors both green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 41e5a65 commit 4375b13

2 files changed

Lines changed: 86 additions & 50 deletions

File tree

internal/lambda/rapidcore/server.go

Lines changed: 7 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -734,21 +734,14 @@ func (s *Server) Invoke(responseWriter http.ResponseWriter, invoke *interop.Invo
734734
return err
735735
}
736736

737-
// InitCompletionResponse carries the structured init failure cause (error type and
738-
// message) extracted from the InitFailure. It is exposed via AwaitInitializedWithDetails
739-
// so standalone callers can report the failure instead of only seeing the sentinel error.
740-
type InitCompletionResponse struct {
737+
type initCompletionResponse struct {
741738
InitErrorType fatalerror.ErrorType
742739
InitErrorMessage error
743740
}
744741

745-
func (s *Server) awaitInitialized() (InitCompletionResponse, error) {
742+
func (s *Server) awaitInitialized() (initCompletionResponse, error) {
746743
initFailure, awaitingInitStatus := <-s.getInitFailuresChan()
747-
return s.interpretInitFailure(initFailure, awaitingInitStatus)
748-
}
749-
750-
func (s *Server) interpretInitFailure(initFailure interop.InitFailure, awaitingInitStatus bool) (InitCompletionResponse, error) {
751-
resp := InitCompletionResponse{}
744+
resp := initCompletionResponse{}
752745

753746
if initFailure.ResetReceived {
754747
// Resets during Init are only received in standalone
@@ -775,51 +768,15 @@ func (s *Server) interpretInitFailure(initFailure interop.InitFailure, awaitingI
775768
// AwaitInitialized waits until init is complete. It must be idempotent,
776769
// since it can be called twice when a caller wants to wait until init is complete
777770
func (s *Server) AwaitInitialized() error {
778-
_, err := s.AwaitInitializedWithDetails()
779-
return err
780-
}
781-
782-
// AwaitInitializedWithDetails behaves like AwaitInitialized but, on failure, also returns
783-
// the structured init error (type and message) carried by the InitFailure. This lets
784-
// standalone callers report the failure to their control plane instead of only observing
785-
// the sentinel error (ErrInitDoneFailed / ErrInitResetReceived).
786-
func (s *Server) AwaitInitializedWithDetails() (InitCompletionResponse, error) {
787-
resp, err := s.awaitInitialized()
788-
if err != nil {
789-
if releaseErr := s.Release(); releaseErr != nil {
771+
if _, err := s.awaitInitialized(); err != nil {
772+
if releaseErr := s.Release(); err != nil {
790773
log.Infof("Error releasing after init failure %s: %s", err, releaseErr)
791774
}
792775
s.setRuntimeState(runtimeInitFailed)
793-
return resp, err
776+
return err
794777
}
795778
s.setRuntimeState(runtimeInitComplete)
796-
return resp, nil
797-
}
798-
799-
// AwaitInitializedWithTimeout behaves like AwaitInitializedWithDetails but returns early if
800-
// init does not complete within the timeout. On timeout it returns timedOut=true WITHOUT
801-
// consuming the init-failures channel, so a subsequent invoke's Reserve()/awaitInitialized()
802-
// can still observe the init outcome and trigger the suppressed init. The caller is expected
803-
// to reset the in-progress init so that outcome becomes available.
804-
func (s *Server) AwaitInitializedWithTimeout(timeout time.Duration) (resp InitCompletionResponse, timedOut bool, err error) {
805-
timer := time.NewTimer(timeout)
806-
defer timer.Stop()
807-
808-
select {
809-
case <-timer.C:
810-
return InitCompletionResponse{}, true, nil
811-
case initFailure, awaitingInitStatus := <-s.getInitFailuresChan():
812-
resp, err = s.interpretInitFailure(initFailure, awaitingInitStatus)
813-
if err != nil {
814-
if releaseErr := s.Release(); releaseErr != nil {
815-
log.Infof("Error releasing after init failure %s: %s", err, releaseErr)
816-
}
817-
s.setRuntimeState(runtimeInitFailed)
818-
return resp, false, err
819-
}
820-
s.setRuntimeState(runtimeInitComplete)
821-
return resp, false, nil
822-
}
779+
return nil
823780
}
824781

825782
func (s *Server) AwaitRelease() (*statejson.ReleaseResponse, error) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package rapidcore
2+
3+
// This file contains LocalStack-specific additions to the rapidcore Server. It is kept
4+
// separate from server.go (which is vendored upstream from
5+
// aws-lambda-runtime-interface-emulator) so that upstream stays byte-identical and rebases
6+
// never conflict. Because the logic needs the unexported init-failures channel and runtime
7+
// state helpers, it must live in package rapidcore rather than in cmd/localstack.
8+
9+
import (
10+
"time"
11+
12+
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/fatalerror"
13+
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/interop"
14+
15+
log "github.com/sirupsen/logrus"
16+
)
17+
18+
// InitCompletionResponse carries the structured init failure cause (error type and message)
19+
// extracted from the InitFailure. It lets standalone callers report the failure instead of
20+
// only seeing the sentinel error (ErrInitDoneFailed / ErrInitResetReceived).
21+
type InitCompletionResponse struct {
22+
InitErrorType fatalerror.ErrorType
23+
InitErrorMessage error
24+
}
25+
26+
// interpretInitFailure mirrors the upstream Server.awaitInitialized body, mapping an
27+
// InitFailure to the sentinel error and structured cause. It is duplicated here (rather than
28+
// refactored out of server.go) to keep the upstream file untouched.
29+
func (s *Server) interpretInitFailure(initFailure interop.InitFailure, awaitingInitStatus bool) (InitCompletionResponse, error) {
30+
resp := InitCompletionResponse{}
31+
32+
if initFailure.ResetReceived {
33+
// Resets during Init are only received in standalone
34+
// during an invoke timeout
35+
s.setRuntimeState(runtimeInitFailed)
36+
resp.InitErrorType = initFailure.ErrorType
37+
resp.InitErrorMessage = initFailure.ErrorMessage
38+
return resp, ErrInitResetReceived
39+
}
40+
41+
if awaitingInitStatus {
42+
// channel not closed, received init failure
43+
// Sandbox can be reserved even if init failed (due to function errors)
44+
s.setRuntimeState(runtimeInitFailed)
45+
resp.InitErrorType = initFailure.ErrorType
46+
resp.InitErrorMessage = initFailure.ErrorMessage
47+
return resp, ErrInitDoneFailed
48+
}
49+
50+
// not awaiting init status (channel closed)
51+
return resp, nil
52+
}
53+
54+
// AwaitInitializedWithTimeout behaves like the upstream AwaitInitialized but (1) returns the
55+
// structured init error on failure and (2) returns early if init does not complete within the
56+
// timeout. On timeout it returns timedOut=true WITHOUT consuming the init-failures channel and
57+
// without any side effects, so a subsequent invoke's Reserve()/awaitInitialized() can still
58+
// observe the init outcome and trigger the suppressed init. The caller is expected to reset the
59+
// in-progress init so that outcome becomes available.
60+
func (s *Server) AwaitInitializedWithTimeout(timeout time.Duration) (resp InitCompletionResponse, timedOut bool, err error) {
61+
timer := time.NewTimer(timeout)
62+
defer timer.Stop()
63+
64+
select {
65+
case <-timer.C:
66+
return InitCompletionResponse{}, true, nil
67+
case initFailure, awaitingInitStatus := <-s.getInitFailuresChan():
68+
resp, err = s.interpretInitFailure(initFailure, awaitingInitStatus)
69+
if err != nil {
70+
if releaseErr := s.Release(); releaseErr != nil {
71+
log.Infof("Error releasing after init failure %s: %s", err, releaseErr)
72+
}
73+
s.setRuntimeState(runtimeInitFailed)
74+
return resp, false, err
75+
}
76+
s.setRuntimeState(runtimeInitComplete)
77+
return resp, false, nil
78+
}
79+
}

0 commit comments

Comments
 (0)