forked from aws/aws-lambda-runtime-interface-emulator
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathevents.go
More file actions
175 lines (163 loc) · 8.42 KB
/
Copy pathevents.go
File metadata and controls
175 lines (163 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main
import (
"fmt"
"sync"
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/fatalerror"
"github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/interop"
lambdatelemetry "github.com/aws/aws-lambda-runtime-interface-emulator/internal/lambda/telemetry"
)
// LocalStackEventsAPI rides rapidcore's lifecycle events (see doRuntimeDomainInit and doInvoke
// in internal/lambda/rapid/handlers.go) to render the synthetic AWS log lines at the
// AWS-faithful points and to record the outcome of the Init phase:
//
// - START is emitted on SendInvokeStart, which rapid fires after any inline (suppressed)
// init and before the runtime handles the invocation — so a re-run init's logs land
// before START, matching AWS.
// - INIT_REPORT is emitted on SendInitReport for failed or timed-out inits (carrying a
// Status and, for failures, an Error Type) and for successful provisioned-concurrency /
// Managed Instances inits (a bare line carrying only the duration and phase). It uses
// rapid's authoritative duration and phase (init for the eager cold-start init, invoke for
// a suppressed init folded into an invocation). A successful on-demand cold-start init logs
// no INIT_REPORT; its duration surfaces as the first invocation's REPORT "Init Duration"
// instead (see TakeColdStartInitDuration).
// - The scrubbed fatal error type of the most recent failed init (e.g. Runtime.ExitError,
// Runtime.Unknown) is recorded for the invoke handler's REPORT Status/Error Type line and
// for the init-failure report to LocalStack (see InitErrorType).
//
// rapid emits, per init attempt: SendInitStart, then SendInitRuntimeDone (status + scrubbed
// error type), then SendInitReport (duration) — RuntimeDone is registered later in
// doRuntimeDomainInit and so runs first on the deferred LIFO unwind. All three fire even when
// the init is aborted by a reset or dies before the runtime starts.
type LocalStackEventsAPI struct {
// NoOpEventsAPI satisfies the rest of interop.EventsAPI without retaining anything.
// Do not embed StandaloneEventsAPI here: it appends every platform event to an
// in-memory event log that is only drained via FetchTailLogs, which this deployment
// never calls — i.e. unbounded memory growth in warm environments.
lambdatelemetry.NoOpEventsAPI
logCollector *LogCollector
// onDemand is true for on-demand functions (see the onDemand classification in main.go):
// only those report the cold-start init duration in their first invocation's REPORT line
// (AWS omits it for provisioned-concurrency and Managed Instances invokes).
onDemand bool
mu sync.Mutex
// lastInitStatus/lastInitErrorType hold the SendInitRuntimeDone outcome of the init
// attempt currently being reported, reset on SendInitStart. An empty status at
// SendInitReport time means the init died before the runtime was started (e.g. an
// extension or bootstrap failure): rapid registers the RuntimeDone callback only after
// starting the runtime process, so treat empty as an error.
lastInitStatus string
lastInitErrorType string
// initErrorType is the scrubbed fatal error type of the most recent failed init attempt,
// reset on SendInitStart. No cross-attempt stickiness is needed: 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 — see Server.Invoke in
// rapidcore/server.go), so each failing invocation re-records the failure via its own
// SendInitReport, and a successful re-run leaves it empty — a recovered environment is not
// tainted by the original failure.
initErrorType string
// initTimedOut is set by main.go before it resets a timed-out init phase, so the aborted
// init's INIT_REPORT renders as AWS's "Status: timeout" (without an error type) instead of
// the generic reset error. Consumed by that init's SendInitReport.
initTimedOut bool
// coldStartInitDuration buffers rapid's measured duration of a successful on-demand
// cold-start Init phase until the first invocation's REPORT line consumes it
// (take-once via TakeColdStartInitDuration).
coldStartInitDuration float64
hasColdStartInitDuration bool
}
func NewLocalStackEventsAPI(logCollector *LogCollector, onDemand bool) *LocalStackEventsAPI {
return &LocalStackEventsAPI{
logCollector: logCollector,
onDemand: onDemand,
}
}
func (e *LocalStackEventsAPI) SendInitStart(data interop.InitStartData) error {
e.mu.Lock()
e.lastInitStatus, e.lastInitErrorType, e.initErrorType = "", "", ""
e.mu.Unlock()
return nil
}
func (e *LocalStackEventsAPI) SendInitRuntimeDone(data interop.InitRuntimeDoneData) error {
e.mu.Lock()
e.lastInitStatus = data.Status
e.lastInitErrorType = ""
if data.ErrorType != nil {
e.lastInitErrorType = *data.ErrorType
}
e.mu.Unlock()
return nil
}
func (e *LocalStackEventsAPI) SendInitReport(data interop.InitReportData) error {
e.mu.Lock()
status, errorType := e.lastInitStatus, e.lastInitErrorType
if status == "" {
// Init died before the runtime was started (see field doc); no scrubbed type known.
status = lambdatelemetry.RuntimeDoneError
}
line := ""
switch {
case e.initTimedOut && data.Phase == lambdatelemetry.InitInsideInitPhase:
// The RIE timed out this init phase and is about to reset it (suppressed-init retry
// at the first invocation); AWS reports it as a timeout, not as the reset error.
e.initTimedOut = false
line = fmt.Sprintf("INIT_REPORT Init Duration: %.2f ms\tPhase: %s\tStatus: timeout\n",
data.Metrics.DurationMs, data.Phase)
case status != lambdatelemetry.RuntimeDoneSuccess:
if errorType == "" {
// Init died before the runtime was started: rapid recorded no scrubbed type.
errorType = string(fatalerror.RuntimeExit)
}
e.initErrorType = errorType
line = fmt.Sprintf("INIT_REPORT Init Duration: %.2f ms\tPhase: %s\tStatus: %s\tError Type: %s\n",
data.Metrics.DurationMs, data.Phase, status, errorType)
case data.Phase == lambdatelemetry.InitInsideInitPhase && e.onDemand:
// Successful on-demand cold-start init: reported as the first invocation's
// REPORT "Init Duration" instead of an INIT_REPORT line.
e.coldStartInitDuration = data.Metrics.DurationMs
e.hasColdStartInitDuration = true
case data.Phase == lambdatelemetry.InitInsideInitPhase:
// Successful provisioned-concurrency / Managed Instances init (non-on-demand). Those
// environments initialize ahead of time, so the duration cannot fold into a first
// invocation's REPORT (their invokes omit Init Duration); AWS instead emits a bare
// INIT_REPORT carrying only the duration — no Phase, Status, or Error Type (those
// fields appear only on the failed/timed-out init lines above).
line = fmt.Sprintf("INIT_REPORT Init Duration: %.2f ms\n", data.Metrics.DurationMs)
}
e.mu.Unlock()
if line != "" {
_, _ = e.logCollector.Write([]byte(line))
}
return nil
}
func (e *LocalStackEventsAPI) SendInvokeStart(data interop.InvokeStartData) error {
_, _ = fmt.Fprintf(e.logCollector, "START RequestId: %s Version: %s\n", data.RequestID, data.Version)
return nil
}
// SetInitPhaseTimedOut marks the in-flight init phase as timed out by the RIE, so the
// INIT_REPORT rendered when the aborted init unwinds reports "Status: timeout". Must be
// called before the reset that aborts the init. It also discards a cold-start init duration
// recorded by an init that completed concurrently with the timeout decision.
func (e *LocalStackEventsAPI) SetInitPhaseTimedOut() {
e.mu.Lock()
defer e.mu.Unlock()
e.initTimedOut = true
e.hasColdStartInitDuration = false
}
// TakeColdStartInitDuration returns rapid's measured duration of the successful on-demand
// cold-start Init phase, at most once (the duration belongs to the first invocation's REPORT
// only). ok is false on warm starts, after failed or timed-out inits, and for
// non-on-demand (provisioned-concurrency / Managed Instances) environments.
func (e *LocalStackEventsAPI) TakeColdStartInitDuration() (durationMS float64, ok bool) {
e.mu.Lock()
defer e.mu.Unlock()
durationMS, ok = e.coldStartInitDuration, e.hasColdStartInitDuration
e.hasColdStartInitDuration = false
return durationMS, ok
}
// InitErrorType returns the scrubbed fatal error type (e.g. Runtime.ExitError) of the most
// recent failed init attempt, or "" if the latest init attempt succeeded or none was recorded.
func (e *LocalStackEventsAPI) InitErrorType() string {
e.mu.Lock()
defer e.mu.Unlock()
return e.initErrorType
}