Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.0 KiB
План реализации: Per-job command timeout
Реализация пункта «Per-job command timeout» из ROADMAP.md,
построенная по образцу уже реализованного overlap_policy — тот же паттерн
наследования «пусто → глобальный дефолт».
Рекомендуемая модель
Claude Opus (Opus 4.8 или новее).
Обоснование: это не локальная правка, а сквозное изменение через слои
(domain → storage → runner → app seam → ui → tests) со сменой сигнатуры
RunJob и seam-типа Service.runJob, что затрагивает ~9 существующих тестов и
несколько UI-файлов. Нужна аккуратность в резолве эффективного значения под
mu и в сохранении контракта «раннер не знает о глобальном конфиге». Для такой
кросс-слойной работы с тестами уместен Opus; Sonnet справится с отдельными
шагами, но выше риск упустить обновление одного из вызовов/тестов.
Замечание по окружению: сборка и тесты GUI требуют CGO + MSYS2 UCRT64
(дефолтный Bash-env идёт с cgo off).
Модель данных
src/domain/job.go — добавить поле в Job:
TimeoutSeconds int `json:"timeout_seconds,omitempty"` // 0 = наследовать глобальный дефолт
src/domain/config.go — добавить в Config:
DefaultTimeoutSeconds int `json:"default_timeout_seconds,omitempty"`
Ключевое соглашение (как у OverlapPolicy): 0 на Job означает
«наследовать», поэтому normalizeJob не должен затирать 0 глобальным значением.
Дефолты и загрузка
src/storage/store.go — по образцу OverlapPolicy:
- в литерал дефолтного
ConfigдобавитьDefaultTimeoutSeconds: 30(сохраняет текущее поведение — 30 с); - в блоке нормализации после загрузки:
if config.DefaultTimeoutSeconds <= 0 { config.DefaultTimeoutSeconds = 30 }.
Это переносит нынешнюю константу commandTimeout = 30s из runner.go в конфиг
как значение по умолчанию.
Runner (сохранить чистоту контракта)
Раннер не должен знать о глобальном конфиге — эффективный таймаут резолвится в app-слое и передаётся внутрь.
- сигнатура
RunJob(ctx, job, trigger, logsDir)→RunJob(ctx, job, trigger, logsDir, timeout time.Duration); runCtx, cancel := context.WithTimeout(ctx, timeout)вместо константы;runStateDetail(...)принимаетtimeoutи печатает его в сообщении"Timed out after %s"вместоcommandTimeout;- StartOnly не трогаем: эта ветка использует
jobInvocation(ctx, …)(неrunCtx), поэтому run-таймаут её не касается — измерение launch latency сохраняется. КонстантуcommandTimeoutможно удалить,commandWaitDelayоставить.
App-слой: резолв и проброс
src/app/run.go — добавить рядом с
effectiveOverlapPolicy:
func (s *Service) effectiveTimeout(job *domain.Job) time.Duration {
secs := job.TimeoutSeconds
if secs <= 0 {
secs = s.store.Config.DefaultTimeoutSeconds
}
return time.Duration(secs) * time.Second
}
Резолвить под mu в startRunLocked и класть в снапшот runEnv (там же, где
logsDir/maxFiles) — новое поле timeout time.Duration. В executeRun
передавать env.timeout в s.runJob(...).
src/app/service.go — обновить тип seam-поля
runJob (добавить timeout time.Duration); присваивание
runJob: runner.RunJob останется валидным после смены сигнатуры.
Валидация
validateJob:if job.TimeoutSeconds < 0 { return errors.New("timeout must be zero (inherit) or a positive number of seconds") };validateConfig:if config.DefaultTimeoutSeconds <= 0 { return errors.New("default timeout must be a positive number of seconds") }.
UI
Диалог задачи src/ui/job_dialog.go — рядом с
«Overlap policy»: числовой widget.NewEntry «Timeout (s)» с плейсхолдером-
подсказкой про наследование; пусто → TimeoutSeconds = 0, иначе strconv.Atoi
с показом ошибки как у schedule.
Настройки src/ui/settings_view.go — в секцию
«Queue» добавить поле «Default timeout (s)» рядом с «Default overlap policy»:
инициализация из store.Config.DefaultTimeoutSeconds,
OnChanged → updateSaveState, запись в config.DefaultTimeoutSeconds при
сохранении и учёт в dirty-check.
Панель деталей src/ui/jobs_view_details.go — новая строка «Timeout», отображающая эффективное значение через новый хелпер в src/app/format.go:
func DisplayTimeout(job domain.Job, globalDefault int) string // "45 s" или "30 s (global default)"
по образцу DisplayOverlapPolicy. Прокинуть globalDefault в
newDetailsPanel/update так же, как уже прокинут globalOverlapPolicy.
Тесты
- src/runner/runner_test.go — все 9 вызовов
RunJobполучают новый аргумент; добавить кейс: короткий per-job таймаут →Failed / Timed out after …; StartOnly с малым таймаутом → не таймаутит. - src/app/run_test.go — тест
effectiveTimeout: инхерит приTimeoutSeconds==0, собственное значение перекрывает глобальное. - src/app/operations_test.go — отрицательный per-job timeout и неположительный default отклоняются.
- src/app/format_test.go —
DisplayTimeout(собственное значение vs «(global default)»). - settings / mainwindow тесты — при необходимости обновить конструкторы
Config.
Документация
- Удалить раздел из ROADMAP.md.
- Обновить упоминания сигнатуры/таймаута
RunJobв ARCHITECTURE.md. - Запись в CHANGELOG.md.
Порядок работ
domain→ storage-дефолты (компилируется, поведение прежнее);runner+ seam-сигнатура + прогон таймаута черезrunEnv→ чиним компиляцию тестов;- валидация;
- UI (диалог, настройки, детали);
- тесты + доки.