fix: fix data race in agentscripts.Runner (#17630)

Fixes https://github.com/coder/internal/issues/604

Fixes a data race in `agentscripts.Runner` where a concurrent `Execute()` call races with `Init()`. We hit this race during shut down, which is not synchronized against starting up.

In this PR I've chosen to add synchronization to the `Runner` rather than try to synchronize the calls in the agent. When we close down the agent, it's OK to just throw an error if we were never initialized with a startup script---we don't want to wait for it since that requires an active connection to the control plane.
This commit is contained in:
Spike Curtis
2025-05-01 14:25:02 +04:00
committed by GitHub
parent 35d686caef
commit ef00ae54f4
+19 -4
View File
@@ -10,7 +10,6 @@ import (
"os/user"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
@@ -104,7 +103,6 @@ type Runner struct {
closed chan struct{}
closeMutex sync.Mutex
cron *cron.Cron
initialized atomic.Bool
scripts []runnerScript
dataDir string
scriptCompleted ScriptCompletedFunc
@@ -113,6 +111,9 @@ type Runner struct {
// execute startup scripts, and scripts on a cron schedule. Both will increment
// this counter.
scriptsExecuted *prometheus.CounterVec
initMutex sync.Mutex
initialized bool
}
// DataDir returns the directory where scripts data is stored.
@@ -154,10 +155,12 @@ func WithPostStartScripts(scripts ...codersdk.WorkspaceAgentScript) InitOption {
// It also schedules any scripts that have a schedule.
// This function must be called before Execute.
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript, scriptCompleted ScriptCompletedFunc, opts ...InitOption) error {
if r.initialized.Load() {
r.initMutex.Lock()
defer r.initMutex.Unlock()
if r.initialized {
return xerrors.New("init: already initialized")
}
r.initialized.Store(true)
r.initialized = true
r.scripts = toRunnerScript(scripts...)
r.scriptCompleted = scriptCompleted
for _, opt := range opts {
@@ -227,6 +230,18 @@ const (
// Execute runs a set of scripts according to a filter.
func (r *Runner) Execute(ctx context.Context, option ExecuteOption) error {
initErr := func() error {
r.initMutex.Lock()
defer r.initMutex.Unlock()
if !r.initialized {
return xerrors.New("execute: not initialized")
}
return nil
}()
if initErr != nil {
return initErr
}
var eg errgroup.Group
for _, script := range r.scripts {
runScript := (option == ExecuteStartScripts && script.RunOnStart) ||