Skip to content

[OPIK-7159] fix: recover stuck INITIALIZED optimization runs#7428

Draft
awkoy wants to merge 1 commit into
awkoy/opik-7043-single-run-overviewfrom
awkoy/opik-7159-stuck-init-reaper
Draft

[OPIK-7159] fix: recover stuck INITIALIZED optimization runs#7428
awkoy wants to merge 1 commit into
awkoy/opik-7043-single-run-overviewfrom
awkoy/opik-7159-stuck-init-reaper

Conversation

@awkoy

@awkoy awkoy commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Optimization runs could stall in INITIALIZED when the Python subprocess never advanced their status. This adds a 3-layer recovery:

  • Backend: OptimizationStalledReaperJob + OptimizationStalledReaperConfig mark stale runs as errored.
  • Python worker: mark-error on subprocess failure, plus structured studio errors (errors.py).
  • Frontend: stall / error UI (RunErrorPanel) on the optimization page.

Stacked on awkoy/opik-7043-single-run-overview (#7310) — retarget to main once that merges.

Test plan

  • Backend reaper marks a stale INITIALIZED run as errored
  • Python worker reports structured error on subprocess failure
  • FE shows error/stall panel for a failed run

🤖 Generated with Claude Code

Optimization runs could stall in INITIALIZED when the Python subprocess
never advanced their status. Adds a 3-layer recovery:

- backend: OptimizationStalledReaperJob + config marks stale runs as errored
- python worker: mark-error on subprocess failure, structured studio errors
- frontend: stall / error UI (RunErrorPanel) on the optimization page

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📋 PR Linter Failed

Missing Section. The description is missing the ## Details section.


Missing Section. The description is missing the ## Change checklist section.


Missing Section. The description is missing the ## Issues section.


Missing Section. The description is missing the ## Testing section.


Missing Section. The description is missing the ## Documentation section.

@github-actions github-actions Bot added python Pull requests that update Python code java Pull requests that update Java code Frontend Backend tests Including test files, or tests related like configuration. typescript *.ts *.tsx and removed baz: pending labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 3

1 tests   0 ✅  0s ⏱️
1 suites  0 💤
1 files    0 ❌  1 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 1

1 tests   0 ✅  1s ⏱️
1 suites  0 💤
1 files    0 ❌  1 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Unit Tests

1 910 tests   1 858 ✅  1m 17s ⏱️
  357 suites      2 💤
  357 files        0 ❌  50 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
🌐 typecheck — frontend Whole-project tsc type check 34.55s
☕ spotless — java backend Format Java code 8.48s
🌐 eslint — frontend Lint + autofix JS/TS 4.74s
Total (3 ran) 47.77s
⏭️ 38 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 2

1 tests   0 ✅  1s ⏱️
1 suites  0 💤
1 files    0 ❌  1 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 11

15 tests   7 ✅  10s ⏱️
10 suites  0 💤
10 files    0 ❌  8 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 6

14 tests   6 ✅  10s ⏱️
 9 suites  0 💤
 9 files    0 ❌  8 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 10

17 tests   8 ✅  11s ⏱️
10 suites  0 💤
10 files    0 ❌  9 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 8

9 tests   0 ✅  7s ⏱️
9 suites  0 💤
9 files    0 ❌  9 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 12

39 tests   31 ✅  32s ⏱️
14 suites   0 💤
14 files     0 ❌  8 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 14

23 tests   14 ✅  53s ⏱️
12 suites   0 💤
12 files     0 ❌  9 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 15

10 tests    0 ✅  8s ⏱️
10 suites   0 💤
10 files     0 ❌  10 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 16

87 tests   79 ✅  45s ⏱️
26 suites   1 💤
26 files     0 ❌  7 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 4

8 tests   0 ✅  27s ⏱️
8 suites  0 💤
8 files    0 ❌  8 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 5

8 tests   0 ✅  24s ⏱️
8 suites  0 💤
8 files    0 ❌  8 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 7

20 tests   12 ✅  27s ⏱️
 9 suites   0 💤
 9 files     0 ❌  8 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

Comment on lines +53 to +56
@Inject
public OptimizationStalledReaperJob(@NonNull OptimizationService optimizationService,
@NonNull LockService lockService,
@NonNull @Config("optimizationStalledReaper") OptimizationStalledReaperConfig config) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual DI constructor breaks convention

OptimizationStalledReaperJob hand-writes an @Inject constructor for three final dependencies, so it drifts from the repo’s DI convention — should we switch to @RequiredArgsConstructor(onConstructor_ = @Inject) like AlertJob and .agents/skills/opik-backend/SKILL.md suggest?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/jobs/OptimizationStalledReaperJob.java
around lines 53-60, the class defines a manual @Inject constructor that only assigns
three final fields (OptimizationService, LockService, and the
@Config("optimizationStalledReaper") OptimizationStalledReaperConfig) and has no
additional setup. Refactor to follow the repo DI guideline by removing the handwritten
constructor and adding Lombok @RequiredArgsConstructor(onConstructor_ = @Inject) so the
constructor is generated automatically. If the repo’s convention (e.g., AlertJob in
the same package) uses a field-level @Config instead of a constructor parameter
annotation, move the @Config annotation to the config field accordingly so dependency
injection still binds the same config entry.

Comment on lines +581 to +592
@Override
@WithSpan
public Mono<Long> reconcileStalledStudioOptimizations(@NonNull Duration initializedTimeout,
@NonNull Duration runningTimeout, int batchSize) {
return optimizationDAO.findStalledStudioOptimizations(initializedTimeout, runningTimeout, batchSize)
// Sequential: stalled runs are rare and this keeps the reaper's DB/Redis footprint small.
.concatMap(stalled -> markStalledOptimizationAsError(stalled, initializedTimeout, runningTimeout))
.reduce(0L, Long::sum);
}

/**
* Best-effort transition of a single stalled run to ERROR. Records the reason in the run's logs first

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reaper behavior untested

reconcileStalledStudioOptimizations now moves stalled Studio runs to ERROR and emits a system log before updating the record, but there’s no test for the public OptimizationStalledReaperJob path, so regressions in the thresholds, filtering, or log append call could slip through — should we add a focused regression test around findStalledStudioOptimizations that asserts the log sync and ERROR transition?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/domain/OptimizationService.java around
lines 581-642, add regression tests for the new `reconcileStalledStudioOptimizations` +
`markStalledOptimizationAsError` behavior (DAO query → log sync via
`appendSystemLogLine` → update via `update(..., ERROR)` with the built reason, and
returning the correct transitioned count). Create a focused unit/integration test class
for the reaper path (the `OptimizationStalledReaperJob` entry point if it exists, or
directly exercising `reconcileStalledStudioOptimizations`) that mocks
`optimizationDAO.findStalledStudioOptimizations`, `optimizationDAO.getById`,
`logSyncService.appendSystemLogLine`, and asserts `update` is invoked only when the
current status is still in `CANCELLABLE_STATUSES`. Write the test as parameterized over
`initializedTimeout`/`runningTimeout` (or at least two statuses/threshold values) and
cover at least: (1) happy path where a RUNNING/INIT-stalled record becomes ERROR and one
log line is appended, and (2) skip path where the run is already
terminal/non-cancellable so it appends no log and returns 0; use Reactor `StepVerifier`
to assert the Mono result and verify mock interactions.

Comment on lines 23 to +26
for (let i = lines.length - 1; i >= 0; i--) {
if (ERROR_LINE_RE.test(lines[i])) return lines[i];
if (lines[i].startsWith(SYSTEM_PREFIX)) {
return lines[i].slice(SYSTEM_PREFIX.length).trim() || null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing structured error detail

According to the PR description, both the worker and stalled-run reaper append curated [System] lines, but this end-scan returns only the last one, so a stalled-run notice can overwrite earlier worker context and the UI drops user-facing error detail — should we collect every lines[i] that starts with SYSTEM_PREFIX before falling back to the generic string?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-frontend/src/v2/pages/OptimizationPage/runError.ts around lines 23-26, the
extractSystemNotice function currently scans backward and immediately returns the first
line starting with SYSTEM_PREFIX, which truncates earlier curated “[System]”
notices. Refactor extractSystemNotice to collect all matching system notice lines from
the log (after ANSI stripping/trim) and return them concatenated (e.g., joined with “;
”) so both the worker and reaper messages are surfaced in the UI. Keep the existing
fallback behavior to return null/GENERIC_RUN_ERROR when no SYSTEM_PREFIX lines are
present.

Comment on lines +62 to +63
record StalledOptimization(UUID id, String workspaceId, OptimizationStatus status) {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal DTO skips builder contract

StalledOptimization omits @Builder(toBuilder = true) and @NonNull on required fields, so findStalledStudioOptimizations() has to use the positional constructor and the DTO misses the backend's construction/null-safety convention — should we add those annotations, as .agents/skills/opik-backend/SKILL.md requires?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/domain/OptimizationDAO.java around lines
62-63, update the internal carrier record `StalledOptimization` to follow the project
DTO/null-safety conventions: add Lombok `@Builder(toBuilder = true)` and mark its
required components (`id`, `workspaceId`, `status`) with `@NonNull`. Then, in the
`findStalledStudioOptimizations(...)` implementation around lines 1199-1202, replace the
positional `new StalledOptimization(...)` construction with the Lombok builder API so
required-field null-safety is enforced consistently with the rest of the backend.

Comment on lines +1184 to +1195
@Override
public Flux<StalledOptimization> findStalledStudioOptimizations(@NonNull Duration initializedTimeout,
@NonNull Duration runningTimeout, int limit) {
var details = "initializedTimeoutSeconds=%d, runningTimeoutSeconds=%d, limit=%d"
.formatted(initializedTimeout.toSeconds(), runningTimeout.toSeconds(), limit);
var template = FilterUtils.getSTWithLogComment(FIND_STALLED_STUDIO_OPTIMIZATIONS,
"find_stalled_studio_optimizations", "", "", details);
return Mono.from(connectionFactory.create())
.flatMapMany(connection -> {
var statement = connection.createStatement(template.render())
.bind("initialized_timeout_seconds", initializedTimeout.toSeconds())
.bind("running_timeout_seconds", runningTimeout.toSeconds())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connection pool exhaustion

findStalledStudioOptimizations creates a raw R2DBC Connection with Mono.from(connectionFactory.create()) and never closes it, so the reaper job can drain the pool and later calls start failing or stalling — should we wrap the work in Mono.usingWhen(connectionFactory.create(), connection -> …, Connection::close) or close it in doFinally?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
apps/opik-backend/src/main/java/com/comet/opik/domain/OptimizationDAO.java around lines
1184-1203, the OptimizationDAOImpl#findStalledStudioOptimizations method creates a raw
R2DBC Connection via connectionFactory.create() but never closes it, which can drain the
connection pool. Refactor the reactive chain to manage the resource with
Mono.usingWhen(connectionFactory.create(), connection -> …, Connection::close) so the
connection is released on completion, error, or cancellation. Keep the existing template
rendering, bind calls, and row-to-StalledOptimization mapping unchanged, but ensure the
statement/flux execution is performed inside the usingWhen connection scope.

@github-actions

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 13

13 tests    0 ✅  2m 20s ⏱️
16 suites   0 💤
16 files     0 ❌  13 🔥

For more details on these errors, see this check.

Results for commit 06a3772.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend baz: pending Frontend java Pull requests that update Java code python Pull requests that update Python code 🔴 size/XL tests Including test files, or tests related like configuration. typescript *.ts *.tsx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant