Skip to content

Commit 5a7b326

Browse files
author
Vedant Madane
committed
address review: use Poststart hook instead of polling goroutine
Replace the runFirstRenderScripts polling goroutine with a RunFirstRenderScripts public method that is called from the template hook Poststart handler. The task runner framework guarantees Poststart fires once the task reaches running state, so the polling loop is unnecessary. Also adds TestTaskTemplateManager_FirstRenderScript covering the new code path.
1 parent 4989466 commit 5a7b326

3 files changed

Lines changed: 86 additions & 30 deletions

File tree

client/allocrunner/taskrunner/template/template.go

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ type TaskTemplateManager struct {
6868
// shutdownCh is used to signal and started goroutine to shutdown
6969
shutdownCh chan struct{}
7070

71+
// firstRenderScripts holds change_scripts that should fire after the
72+
// initial template render once the task reaches the running state.
73+
firstRenderScripts []*structs.ChangeScript
74+
7175
// shutdown marks whether the manager has been shutdown
7276
shutdown bool
7377
shutdownLock sync.Mutex
@@ -276,16 +280,13 @@ func (tm *TaskTemplateManager) Run() {
276280
// Unblock the task
277281
close(tm.config.UnblockCh)
278282

279-
// Collect any change_script templates that should fire on first render.
280-
// These scripts are launched in a goroutine because they need the task
281-
// to reach the running state before Exec can succeed.
282-
firstRenderScripts := tm.collectFirstRenderScripts()
283-
if len(firstRenderScripts) > 0 {
284-
go tm.runFirstRenderScripts(firstRenderScripts)
285-
}
283+
// Collect change_script templates that should fire on first render.
284+
// These are stored and executed later when the task reaches the running
285+
// state, triggered via RunFirstRenderScripts from the Poststart hook.
286+
tm.firstRenderScripts = tm.collectFirstRenderScripts()
286287

287288
// If all our templates are change mode no-op, then we can exit here
288-
if tm.allTemplatesNoop() && len(firstRenderScripts) == 0 {
289+
if tm.allTemplatesNoop() && len(tm.firstRenderScripts) == 0 {
289290
return
290291
}
291292

@@ -668,30 +669,22 @@ func (tm *TaskTemplateManager) collectFirstRenderScripts() []*structs.ChangeScri
668669
return scripts
669670
}
670671

671-
// runFirstRenderScripts waits for the task to reach the running state and then
672-
// executes the given change_scripts. It respects the shutdown channel so it
673-
// will not block forever if the task is killed before reaching running.
674-
func (tm *TaskTemplateManager) runFirstRenderScripts(scripts []*structs.ChangeScript) {
675-
const pollInterval = 500 * time.Millisecond
676-
ticker := time.NewTicker(pollInterval)
677-
defer ticker.Stop()
672+
// RunFirstRenderScripts executes the change_scripts collected during the
673+
// first template render. It is called by the template hook's Poststart
674+
// method once the task is running and Exec is available.
675+
func (tm *TaskTemplateManager) RunFirstRenderScripts() {
676+
scripts := tm.firstRenderScripts
677+
if len(scripts) == 0 {
678+
return
679+
}
680+
tm.firstRenderScripts = nil
678681

679-
for {
680-
select {
681-
case <-tm.shutdownCh:
682-
return
683-
case <-ticker.C:
684-
if tm.config.Lifecycle.IsRunning() {
685-
var wg sync.WaitGroup
686-
for _, script := range scripts {
687-
wg.Add(1)
688-
go tm.processScript(script, &wg)
689-
}
690-
wg.Wait()
691-
return
692-
}
693-
}
682+
var wg sync.WaitGroup
683+
for _, script := range scripts {
684+
wg.Add(1)
685+
go tm.processScript(script, &wg)
694686
}
687+
wg.Wait()
695688
}
696689

697690
// templateRunner returns a consul-template runner for the given templates and a

client/allocrunner/taskrunner/template/template_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,59 @@ OUTER:
13471347
}
13481348
}
13491349

1350+
// TestTaskTemplateManager_FirstRenderScript verifies that a template with
1351+
// change_mode "script" and RunOnFirstRender collects the script so it can
1352+
// be executed once the task reaches the running state via RunFirstRenderScripts.
1353+
func TestTaskTemplateManager_FirstRenderScript(t *testing.T) {
1354+
ci.Parallel(t)
1355+
clienttestutil.RequireConsul(t)
1356+
1357+
key := "first_render_key"
1358+
t1 := &structs.Template{
1359+
EmbeddedTmpl: `FOO={{key "first_render_key"}}` + "\n",
1360+
DestPath: "first_render.env",
1361+
ChangeMode: structs.TemplateChangeModeScript,
1362+
ChangeScript: &structs.ChangeScript{
1363+
Command: "/bin/foo",
1364+
Args: []string{},
1365+
Timeout: 5 * time.Second,
1366+
FailOnError: false,
1367+
RunOnFirstRender: true,
1368+
},
1369+
Envvars: true,
1370+
}
1371+
1372+
harness := newTestHarness(t, []*structs.Template{t1}, true, false)
1373+
harness.mockHooks.SetupExecTest(0, nil)
1374+
harness.start(t)
1375+
defer harness.stop()
1376+
1377+
// Write key so the template renders
1378+
harness.consul.SetKV(t, key, []byte("hello"))
1379+
1380+
// Wait for unblock (first render complete)
1381+
select {
1382+
case <-harness.mockHooks.UnblockCh:
1383+
case <-time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second):
1384+
t.Fatal("Task unblock should have been called")
1385+
}
1386+
1387+
// Simulate the Poststart hook by calling RunFirstRenderScripts directly
1388+
harness.mockHooks.HasHandle = true
1389+
harness.manager.RunFirstRenderScripts()
1390+
1391+
// Verify script execution event was emitted
1392+
timeout := time.After(time.Duration(5*testutil.TestMultiplier()) * time.Second)
1393+
select {
1394+
case ev := <-harness.mockHooks.EmitEventCh:
1395+
if !strings.Contains(ev.DisplayMessage, t1.ChangeScript.Command) {
1396+
t.Fatalf("expected script event, got: %s", ev.DisplayMessage)
1397+
}
1398+
case <-timeout:
1399+
t.Fatal("should have received a script execution event")
1400+
}
1401+
}
1402+
13501403
// TestTaskTemplateManager_ScriptExecutionFailTask tests whether we fail the
13511404
// task upon script execution failure if that's how it's configured.
13521405
func TestTaskTemplateManager_ScriptExecutionFailTask(t *testing.T) {

client/allocrunner/taskrunner/template_hook.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,16 @@ func (h *templateHook) newManager(tmpls []*structs.Template) (manager *template.
226226
return m, unblock, nil
227227
}
228228

229+
func (h *templateHook) Poststart(_ context.Context, _ *interfaces.TaskPoststartRequest, _ *interfaces.TaskPoststartResponse) error {
230+
h.managerLock.Lock()
231+
defer h.managerLock.Unlock()
232+
233+
if h.templateManager != nil {
234+
h.templateManager.RunFirstRenderScripts()
235+
}
236+
return nil
237+
}
238+
229239
func (h *templateHook) Stop(_ context.Context, req *interfaces.TaskStopRequest, resp *interfaces.TaskStopResponse) error {
230240
h.managerLock.Lock()
231241
defer h.managerLock.Unlock()

0 commit comments

Comments
 (0)