Skip to content

Commit dc00020

Browse files
feat(serverless-init): add ProcessHooks for subprocess liveness tracking
RunInit and execute() gain a *ProcessHooks parameter (OnAlive/OnDead) so callers can track a spawned user process's liveness. mode.Conf.Runner is dropped for init-container mode since its func type can no longer match RunInit's new signature; main.go switches from modeConf.Runner(logConfig) to cloudService.Run(modeConf, logConfig) (already available on the CloudService interface) so init-container behavior is unaffected. mode_windows.go gains a matching ProcessHooks stub so packages that construct one (e.g. a future MicroVM CloudService) still cross-compile for GOOS=windows. PR stack — this is PR 7 (of the microvm-07 split), part 1/5: 1. This PR — ProcessHooks plumbing in mode package 2. MicroVM CloudService (cloudservice/microvm.go) 3. MicroVM tags/metrics/arch tests 4. MicroVM lifecycle-server tests 5. main.go wiring + CloudService registration
1 parent 20bbd1f commit dc00020

8 files changed

Lines changed: 191 additions & 18 deletions

File tree

cmd/serverless-init/cloudservice/service.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,13 @@ func (l *LocalService) Run(modeConf mode.Conf, logConfig *serverlessInitLog.Conf
171171

172172
// defaultRun is the standard Run implementation for cloud services that do not
173173
// manage a child process themselves. In sidecar mode it calls RunSidecar; in
174-
// init-container mode it spawns the user app directly via RunInit.
174+
// init-container mode it spawns the user app with no child handle (no
175+
// MarkAlive/MarkDead tracking). MicroVM overrides Run to supply its child.
175176
func defaultRun(modeConf mode.Conf, logConfig *serverlessInitLog.Config) error {
176177
if modeConf.SidecarMode {
177178
return mode.RunSidecar(logConfig)
178179
}
179-
return mode.RunInit(logConfig)
180+
return mode.RunInit(logConfig, nil) // no child tracking for non-MicroVM services
180181
}
181182

182183
// Shutdown emits the shutdown metric for LocalService

cmd/serverless-init/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func run(
374374
cloudService, tagConfig, metricTags, demux,
375375
)
376376

377-
err := modeConf.Runner(logConfig)
377+
err := cloudService.Run(modeConf, logConfig)
378378

379379
// Defers are LIFO. Order of execution:
380380
// 1. Watchdog timer starts (debug log if shutdown exceeds the budget).

cmd/serverless-init/mode/initcontainer_mode.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,33 @@ import (
2121
"github.com/spf13/afero"
2222
)
2323

24-
// Run is the entrypoint of the init process. It will spawn the customer process
25-
func RunInit(logConfig *serverlessLog.Config) error {
24+
// ProcessHooks carries optional callbacks that are invoked around subprocess
25+
// lifecycle transitions. MicroVM supplies OnAlive/OnDead to drive its /ready
26+
// alive-check; other cloud services pass nil.
27+
type ProcessHooks struct {
28+
OnAlive func() // called after cmd.Start succeeds
29+
OnDead func() // called when cmd.Wait returns (deferred, fires on panic too)
30+
}
31+
32+
// RunInit is the entrypoint of the init process. It spawns the customer
33+
// process and, when hooks is non-nil, invokes hooks.OnAlive on cmd.Start
34+
// success and hooks.OnDead via defer so the caller can track liveness.
35+
func RunInit(logConfig *serverlessLog.Config, hooks *ProcessHooks) error {
2636
if len(os.Args) < 2 {
2737
panic("[datadog init process] invalid argument count, did you forget to set CMD ?")
2838
}
2939

3040
args := os.Args[1:]
3141

3242
log.Debugf("Launching subprocess %v\n", args)
33-
err := execute(logConfig, args)
34-
if err != nil {
43+
if err := execute(logConfig, args, hooks); err != nil {
3544
log.Errorf("ERROR: Failed to execute command: %v\n", err)
3645
return err
3746
}
3847
return nil
3948
}
4049

41-
func execute(logConfig *serverlessLog.Config, args []string) error {
50+
func execute(logConfig *serverlessLog.Config, args []string, hooks *ProcessHooks) error {
4251
commandName, commandArgs := buildCommandParam(args)
4352

4453
// Add our tracer settings
@@ -55,15 +64,21 @@ func execute(logConfig *serverlessLog.Config, args []string) error {
5564
cmd.Stderr = io.MultiWriter(os.Stderr, serverlessLog.NewChannelWriter(logConfig.Channel, true))
5665
}
5766

58-
err := cmd.Start()
59-
if err != nil {
67+
if err := cmd.Start(); err != nil {
6068
return err
6169
}
70+
if hooks != nil && hooks.OnAlive != nil {
71+
hooks.OnAlive()
72+
// Defer OnDead so it fires even on panic / runtime.Goexit. The
73+
// child process is no longer being supervised once we leave this frame.
74+
if hooks.OnDead != nil {
75+
defer hooks.OnDead()
76+
}
77+
}
6278
sigs := make(chan os.Signal, 1)
6379
signal.Notify(sigs)
6480
go forwardSignals(cmd.Process, sigs)
65-
err = cmd.Wait()
66-
return err
81+
return cmd.Wait()
6782
}
6883

6984
func buildCommandParam(cmdArg []string) (string, []string) {

cmd/serverless-init/mode/initcontainer_mode_test.go

Lines changed: 151 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,74 @@ import (
1212
"os/exec"
1313
"runtime"
1414
"strconv"
15+
"sync/atomic"
1516
"syscall"
1617
"testing"
18+
"time"
1719

1820
"github.com/spf13/afero"
1921

22+
"github.com/DataDog/datadog-agent/cmd/serverless-init/lifecycle"
2023
serverlessLog "github.com/DataDog/datadog-agent/cmd/serverless-init/log"
2124

2225
"github.com/stretchr/testify/assert"
2326
)
2427

28+
// TestDetectMode_Sidecar_HasRunnerSet verifies that sidecar mode (no args)
29+
// sets Runner to RunSidecar so main.go can call it directly.
30+
func TestDetectMode_Sidecar_HasRunnerSet(t *testing.T) {
31+
saved := os.Args
32+
defer func() { os.Args = saved }()
33+
os.Args = []string{"datadog-init"}
34+
35+
conf := DetectMode()
36+
assert.True(t, conf.SidecarMode)
37+
assert.NotNil(t, conf.Runner, "sidecar mode must set Runner to RunSidecar")
38+
}
39+
40+
// TestDetectMode_Init_RunnerIsNil verifies that init mode (args present) leaves
41+
// Runner nil. main.go delegates to cloudService.Run(modeConf, logConfig) which
42+
// each CloudService implements directly, so modeConf.Runner is not used in
43+
// init-container mode.
44+
func TestDetectMode_Init_RunnerIsNil(t *testing.T) {
45+
saved := os.Args
46+
defer func() { os.Args = saved }()
47+
os.Args = []string{"datadog-init", "sh", "-c", "exit 0"}
48+
49+
conf := DetectMode()
50+
assert.False(t, conf.SidecarMode)
51+
assert.Nil(t, conf.Runner, "init mode Runner must be nil; main.go builds the closure with child")
52+
}
53+
54+
// TestHandleTerminationSignals_SIGTERM and _SIGINT exercise handleTerminationSignals
55+
// via its injectable notify parameter, avoiding real OS signals and verifying
56+
// that either signal unblocks stopCh.
57+
func TestHandleTerminationSignals_SIGTERM(t *testing.T) {
58+
stopCh := make(chan struct{}, 1)
59+
notify := func(c chan<- os.Signal, _ ...os.Signal) {
60+
go func() { c <- syscall.SIGTERM }()
61+
}
62+
handleTerminationSignals(stopCh, notify)
63+
select {
64+
case <-stopCh:
65+
default:
66+
t.Fatal("stopCh must be closed after SIGTERM")
67+
}
68+
}
69+
70+
func TestHandleTerminationSignals_SIGINT(t *testing.T) {
71+
stopCh := make(chan struct{}, 1)
72+
notify := func(c chan<- os.Signal, _ ...os.Signal) {
73+
go func() { c <- syscall.SIGINT }()
74+
}
75+
handleTerminationSignals(stopCh, notify)
76+
select {
77+
case <-stopCh:
78+
default:
79+
t.Fatal("stopCh must be closed after SIGINT")
80+
}
81+
}
82+
2583
func TestBuildCommandParamWithArgs(t *testing.T) {
2684
name, args := buildCommandParam([]string{"superCmd", "--verbose", "path", "-i", "."})
2785
assert.Equal(t, "superCmd", name)
@@ -36,19 +94,110 @@ func TestBuildCommandParam(t *testing.T) {
3694

3795
func TestPropagateChildSuccess(t *testing.T) {
3896
runTestOnLinuxOnly(t, func(t *testing.T) {
39-
err := execute(&serverlessLog.Config{}, []string{"bash", "-c", "exit 0"})
97+
err := execute(&serverlessLog.Config{}, []string{"bash", "-c", "exit 0"}, nil)
4098
assert.Equal(t, nil, err)
4199
})
42100
}
43101

44102
func TestPropagateChildError(t *testing.T) {
45103
runTestOnLinuxOnly(t, func(t *testing.T) {
46104
expectedError := 123
47-
err := execute(&serverlessLog.Config{}, []string{"bash", "-c", "exit " + strconv.Itoa(expectedError)})
105+
err := execute(&serverlessLog.Config{}, []string{"bash", "-c", "exit " + strconv.Itoa(expectedError)}, nil)
48106
assert.Equal(t, expectedError<<8, int(err.(*exec.ExitError).ProcessState.Sys().(syscall.WaitStatus)))
49107
})
50108
}
51109

110+
// When cmd.Start fails (e.g. binary not found), execute must return the error
111+
// and never invoke OnAlive — the user app never ran.
112+
func TestExecute_StartFailure_NeverCallsOnAlive(t *testing.T) {
113+
var onAliveCalled bool
114+
hooks := &ProcessHooks{
115+
OnAlive: func() { onAliveCalled = true },
116+
OnDead: func() {},
117+
}
118+
err := execute(&serverlessLog.Config{}, []string{"/nonexistent/binary/that/cannot/be/found"}, hooks)
119+
assert.Error(t, err, "cmd.Start must fail for a missing binary")
120+
assert.False(t, onAliveCalled, "OnAlive must not be called when cmd.Start fails")
121+
}
122+
123+
// On a successful run, execute must call OnAlive after cmd.Start and OnDead
124+
// via defer after cmd.Wait. The mid-run probe pins the ordering.
125+
func TestExecute_SuccessfulRun_InvokesHooksInOrder(t *testing.T) {
126+
child := lifecycle.NewChild()
127+
hooks := &ProcessHooks{
128+
OnAlive: child.MarkAlive,
129+
OnDead: child.MarkDead,
130+
}
131+
var midRunAlive atomic.Bool
132+
probeDone := make(chan struct{})
133+
go func() {
134+
defer close(probeDone)
135+
time.Sleep(100 * time.Millisecond)
136+
midRunAlive.Store(child.IsAlive())
137+
}()
138+
err := execute(&serverlessLog.Config{}, []string{"sh", "-c", "sleep 0.5"}, hooks)
139+
<-probeDone
140+
assert.NoError(t, err)
141+
assert.True(t, midRunAlive.Load(), "OnAlive must fire before cmd.Wait returns")
142+
assert.False(t, child.IsAlive(), "OnDead must fire after cmd.Wait returns")
143+
}
144+
145+
// MicroVM init-container mode: ProcessHooks drive liveness tracking through
146+
// the public RunInit entry point. Pins the alive→dead transition.
147+
func TestRunInit_MicroVM_ChildSupplied_TracksLiveness(t *testing.T) {
148+
saved := os.Args
149+
defer func() { os.Args = saved }()
150+
os.Args = []string{"datadog-init", "sh", "-c", "sleep 0.5"}
151+
152+
child := lifecycle.NewChild()
153+
var midRunAlive atomic.Bool
154+
probeDone := make(chan struct{})
155+
go func() {
156+
defer close(probeDone)
157+
time.Sleep(100 * time.Millisecond)
158+
midRunAlive.Store(child.IsAlive())
159+
}()
160+
err := RunInit(&serverlessLog.Config{}, &ProcessHooks{OnAlive: child.MarkAlive, OnDead: child.MarkDead})
161+
<-probeDone
162+
assert.NoError(t, err)
163+
assert.True(t, midRunAlive.Load(), "child must be marked alive while RunInit is blocked")
164+
assert.False(t, child.IsAlive(), "child must be marked dead after RunInit returns")
165+
}
166+
167+
// Non-MicroVM mode: nil hooks. RunInit must execute the user app without
168+
// panicking. Pins the `if hooks != nil` guard.
169+
func TestRunInit_NonMicroVM_NoHooks_StillExecutes(t *testing.T) {
170+
saved := os.Args
171+
defer func() { os.Args = saved }()
172+
os.Args = []string{"datadog-init", "sh", "-c", "exit 0"}
173+
174+
err := RunInit(&serverlessLog.Config{}, nil)
175+
assert.NoError(t, err)
176+
}
177+
178+
// On a non-zero exit, OnDead must still fire. Pins the defer-on-error path.
179+
func TestExecute_NonZeroExit_OnDeadFiresAfterExit(t *testing.T) {
180+
child := lifecycle.NewChild()
181+
// Simulate MicroVM wiring: OnAlive fires first so child is alive, then
182+
// OnDead fires on defer.
183+
hooks := &ProcessHooks{OnAlive: child.MarkAlive, OnDead: child.MarkDead}
184+
err := execute(&serverlessLog.Config{}, []string{"sh", "-c", "exit 7"}, hooks)
185+
assert.Error(t, err, "non-zero exit must surface as an error")
186+
assert.False(t, child.IsAlive(), "OnDead must fire after non-zero exit")
187+
}
188+
189+
// ProcessHooks with OnAlive set but OnDead nil must not panic. Pins the
190+
// nil-guard on hooks.OnDead added to execute().
191+
func TestExecute_OnAliveSetOnDeadNil_DoesNotPanic(t *testing.T) {
192+
hooks := &ProcessHooks{
193+
OnAlive: func() {},
194+
OnDead: nil, // intentionally absent
195+
}
196+
assert.NotPanics(t, func() {
197+
_ = execute(&serverlessLog.Config{}, []string{"sh", "-c", "exit 0"}, hooks)
198+
})
199+
}
200+
52201
func TestForwardSignalToChild(t *testing.T) {
53202
runTestOnLinuxOnly(t, func(t *testing.T) {
54203
resultChan := make(chan error)

cmd/serverless-init/mode/mode.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ func DetectMode() Conf {
5757
log.Infof("Arguments provided, launching in Init mode")
5858
return Conf{
5959
LoggerName: loggerNameInit,
60-
Runner: RunInit,
6160
TagVersionMode: "_dd.datadog_init_version",
6261
TagVersionModeEnhancedMetrics: "datadog_init_version",
6362
SidecarMode: false,

cmd/serverless-init/mode/mode_windows.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,16 @@ type Conf struct {
2727
EnvDefaults map[string]string
2828
}
2929

30+
// ProcessHooks mirrors the init-container ProcessHooks type so that packages
31+
// which construct it (e.g. cloudservice.MicroVM) still build on windows.
32+
// serverless-init is not supported on windows, so these hooks are never invoked.
33+
type ProcessHooks struct {
34+
OnAlive func()
35+
OnDead func()
36+
}
37+
3038
// RunInit is unsupported on windows.
31-
func RunInit(_ *serverlessLog.Config) error {
39+
func RunInit(_ *serverlessLog.Config, _ *ProcessHooks) error {
3240
return errors.New("serverless-init is not supported on windows")
3341
}
3442

cmd/serverless-init/mode/mode_windows_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
func TestRunInitUnsupportedOnWindows(t *testing.T) {
17-
err := RunInit(nil)
17+
err := RunInit(nil, nil)
1818
assert.EqualError(t, err, "serverless-init is not supported on windows")
1919
}
2020

cmd/serverless-init/mode/sidecarcontainer_mode.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import (
1717
"github.com/DataDog/datadog-agent/pkg/util/log"
1818
)
1919

20-
// Run is the entrypoint of the init process. It will spawn the customer process
20+
// RunSidecar is the entrypoint when serverless-init runs as a sidecar
21+
// container. It blocks until SIGINT/SIGTERM.
2122
func RunSidecar(_ *serverlessLog.Config) error {
2223
stopCh := make(chan struct{})
2324
go handleTerminationSignals(stopCh, signal.Notify)

0 commit comments

Comments
 (0)