hashicorp/nomad · error

taskConfig with ID '%s' already started

Error message

taskConfig with ID '%s' already started

What it means

StartTask guards against starting two task instances with the same task ID: it first checks the driver's in-memory task handle store (d.tasks.Get(cfg.ID)), and if a handle already exists it refuses to start a duplicate. This keeps driver state consistent — each task ID maps to exactly one running QEMU process and handle.

Source

Thrown at drivers/qemu/driver.go:458

	if len(pluginConfigAllowList) > 0 {
		allowed := map[string]struct{}{}
		for _, arg := range pluginConfigAllowList {
			allowed[arg] = struct{}{}
		}
		for _, arg := range args {
			if strings.HasPrefix(strings.TrimSpace(arg), "-") {
				if _, ok := allowed[arg]; !ok {
					return fmt.Errorf("%q is not in args_allowlist", arg)
				}
			}
		}
	}
	return nil
}

func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drivers.DriverNetwork, error) {
	if _, ok := d.tasks.Get(cfg.ID); ok {
		return nil, nil, fmt.Errorf("taskConfig with ID '%s' already started", cfg.ID)
	}

	var driverConfig TaskConfig

	if err := cfg.DecodeDriverConfig(&driverConfig); err != nil {
		return nil, nil, fmt.Errorf("failed to decode driver config: %v", err)
	}

	// ensure that PortMap variables are populated early on
	cfg.Env = taskenv.SetPortMapEnvs(cfg.Env, driverConfig.PortMap)

	handle := drivers.NewTaskHandle(taskHandleVersion)
	handle.Config = cfg

	if err := validateEmulator(driverConfig.Emulator, d.config.EmulatorsAllowList); err != nil {
		return nil, nil, err
	}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Call DestroyTask for the conflicting task ID (or wait for it to finish) before retrying StartTask.
  2. If the task is truly gone, recover the allocation (nomad alloc stop / client restart) so the driver's task map is cleared.
  3. Generate a fresh allocation (new ID) rather than reusing the old task ID.

Example fix

// before
driver.StartTask(cfg) // panics with 'already started' if handle exists
// after
if _, err := driver.DestroyTask(cfg.ID, true); err != nil { /* log */ }
handle, net, err := driver.StartTask(cfg)
Defensive patterns

Strategy: try-catch

Validate before calling

if _, exists := knownTaskIDs[cfg.ID]; exists {
    return fmt.Errorf("task %s already started; destroy it first", cfg.ID)
}

Try / catch

_, _, err := d.StartTask(cfg)
if err != nil && strings.Contains(err.Error(), "already started") {
    _ = d.DestroyTask(cfg.ID, true) // force cleanup, then retry once
    handle, net, err = d.StartTask(cfg)
}

Prevention

When it happens

Trigger: Calling StartTask with a drivers.TaskConfig whose ID matches a task previously started by this driver instance and not yet destroyed; also occurs on client restarts/recovery paths where a handle was restored but the task still occupies the driver's task map.

Common situations: Retrying a failed allocation without DestroyTask succeeding first; duplicate allocation IDs after a scheduler race; recovering a client where the handle exists but the QEMU process was assumed dead.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/5cd901f5be9f79a4. Report an issue: GitHub.