diff --git a/docs/plans/per-job-timeout.md b/docs/plans/per-job-timeout.md deleted file mode 100644 index ae2e8ec..0000000 --- a/docs/plans/per-job-timeout.md +++ /dev/null @@ -1,154 +0,0 @@ -# План реализации: Per-job command timeout - -Реализация пункта **«Per-job command timeout»** из [ROADMAP.md](../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](../../src/domain/job.go)** — добавить поле в `Job`: - -```go -TimeoutSeconds int `json:"timeout_seconds,omitempty"` // 0 = наследовать глобальный дефолт -``` - -**[src/domain/config.go](../../src/domain/config.go)** — добавить в `Config`: - -```go -DefaultTimeoutSeconds int `json:"default_timeout_seconds,omitempty"` -``` - -Ключевое соглашение (как у `OverlapPolicy`): `0` на `Job` означает -«наследовать», поэтому `normalizeJob` не должен затирать 0 глобальным значением. - -## Дефолты и загрузка - -**[src/storage/store.go](../../src/storage/store.go)** — по образцу `OverlapPolicy`: - -- в литерал дефолтного `Config` добавить `DefaultTimeoutSeconds: 30` - (сохраняет текущее поведение — 30 с); -- в блоке нормализации после загрузки: - `if config.DefaultTimeoutSeconds <= 0 { config.DefaultTimeoutSeconds = 30 }`. - -Это переносит нынешнюю константу `commandTimeout = 30s` из `runner.go` в конфиг -как значение по умолчанию. - -## Runner (сохранить чистоту контракта) - -Раннер не должен знать о глобальном конфиге — эффективный таймаут резолвится в -app-слое и передаётся внутрь. - -**[src/runner/runner.go](../../src/runner/runner.go)**: - -- сигнатура `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](../../src/app/run.go)** — добавить рядом с -`effectiveOverlapPolicy`: - -```go -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](../../src/app/service.go)** — обновить тип seam-поля -`runJob` (добавить `timeout time.Duration`); присваивание -`runJob: runner.RunJob` останется валидным после смены сигнатуры. - -## Валидация - -**[src/app/operations.go](../../src/app/operations.go)**: - -- `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](../../src/ui/job_dialog.go)** — рядом с -«Overlap policy»: числовой `widget.NewEntry` «Timeout (s)» с плейсхолдером- -подсказкой про наследование; пусто → `TimeoutSeconds = 0`, иначе `strconv.Atoi` -с показом ошибки как у schedule. - -**Настройки [src/ui/settings_view.go](../../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](../../src/ui/jobs_view_details.go)** -— новая строка «Timeout», отображающая эффективное значение через новый хелпер -в [src/app/format.go](../../src/app/format.go): - -```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](../../src/runner/runner_test.go)** — все 9 вызовов - `RunJob` получают новый аргумент; добавить кейс: короткий per-job таймаут → - `Failed / Timed out after …`; StartOnly с малым таймаутом → не таймаутит. -- **[src/app/run_test.go](../../src/app/run_test.go)** — тест `effectiveTimeout`: - инхерит при `TimeoutSeconds==0`, собственное значение перекрывает глобальное. -- **[src/app/operations_test.go](../../src/app/operations_test.go)** — - отрицательный per-job timeout и неположительный default отклоняются. -- **[src/app/format_test.go](../../src/app/format_test.go)** — `DisplayTimeout` - (собственное значение vs «(global default)»). -- **settings / mainwindow тесты** — при необходимости обновить конструкторы - `Config`. - -## Документация - -- Удалить раздел из [ROADMAP.md](../ROADMAP.md). -- Обновить упоминания сигнатуры/таймаута `RunJob` в - [ARCHITECTURE.md](../ARCHITECTURE.md). -- Запись в [CHANGELOG.md](../CHANGELOG.md). - -## Порядок работ - -1. `domain` → storage-дефолты (компилируется, поведение прежнее); -2. `runner` + seam-сигнатура + прогон таймаута через `runEnv` → чиним - компиляцию тестов; -3. валидация; -4. UI (диалог, настройки, детали); -5. тесты + доки.