Skip to content

[Bug][Windows]: service repair can fail at 20s just before the proxy becomes healthy #3009

Closed

Description

@SeanChengN

Client or integration

OpenCodex dashboard

Area

Service lifecycle

Summary

On Windows, ocx service repair can report failure at its fixed 20-second health deadline even though the scheduled service is still starting and becomes healthy immediately afterward.

I saw this while recovering a dashboard update on 2.36.0. The repair command printed that no proxy answered on port 11000 within 20 seconds and returned exit 1. The Task Scheduler job remained running. Shortly afterward, /healthz returned status: ok from the same scheduled startup, and the proxy remained stable.

Expected: a slow but valid Windows cold start should not be reported as a terminal repair failure. A final probe at the deadline, or a slightly longer/phase-aware bounded wait, would avoid the false negative while still failing a service that never binds.

Reproduction

  1. Install OpenCodex 2.36.0 through npm on Windows.
  2. Install the opencodex-proxy Task Scheduler service.
  3. Use an environment where startup work can take around 20 seconds. In this run the service log also showed an NTFS ACL hardening timeout and previous-session journal recovery before the proxy announced its listener.
  4. Run ocx service repair.
  5. Observe the command exit 1 after the 20-second serving check.
  6. Check the scheduled task and /healthz a few seconds later. The same service startup is running and healthy.

Observed command result:

Service repaired, but no proxy answered on port 11000 within 20s.

Follow-up state:

{
  "status": "ok",
  "service": "opencodex",
  "version": "2.36.0",
  "pid": 39388,
  "port": 11000
}

The process was created at 2026-08-30T21:33:19+08:00; the scheduled task run was recorded at 21:33:33+08:00. After it became healthy, repeated checks and the Web UI stayed available. This was not a crash/restart loop.

The current code uses one fixed deadline:

export const SERVICE_INSTALL_HEALTH_MS = 20_000;

and returns failure as soon as that deadline is reached. A final identity probe at the boundary would at least close the narrow race. If 20 seconds is intentionally the normal budget, a separate bounded grace period for a still-running Windows service may be another option.

Related but not duplicate: #720 fixed the GUI update worker's 15-second restart-confirmation boundary. This report is the separate confirmServiceServing() path used by service install/start/repair, which still has a 20-second deadline on current main.

Version

2.36.0; current main still uses the 20-second service-install health deadline.

Operating system

Windows 10 22H2, build 19045, x64

Provider and model

Not provider-specific

Logs or error output

[opencodex] ACL hardening timed out (ETIMEDOUT) — continuing without NTFS ACL harden
Previous session did not shut down cleanly. Codex state restored from journal.
opencodex proxy running on http://localhost:11000

Screenshots and supporting files

Current source: https://github.com/lidge-jun/opencodex/blob/main/src/service.ts#L554-L617

Redacted configuration

{
  "installer": "npm",
  "service": "Windows Task Scheduler",
  "port": 11000
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Aug 30, 2026
  2. lidge-jun commented on Aug 30, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 69 / 80

    이 이슈는 윈도우에서 ocx service repair 가 20초 건강 검사에 걸려 실패를 찍지만, 같은 시작은 바로 뒤에 살아 있는 버그다. 작성자는 2.36.0 대시보드 업데이트를 되돌리던 중, repair가 포트 11000에서 20초 안에 답이 없다고 종료 코드 1을 냈다고 한다. 작업 스케줄러 작업은 계속 돌고 있었고, 몇 초 뒤 healthz 는 status ok 였다. 로그에는 NTFS ACL 단단히 하기 타임아웃과, 이전 세션 저널 복구가 리스너 안내보다 앞에 있었다. 지금 dev HEAD df8b388 에서도 상수와 루프는 그대로다. 미리보기 배포는 계획에 없다. types.ts/config.ts 분할과도 안 겹친다.

    코드는 src/service.ts 다. SERVICE_INSTALL_HEALTH_MS 는 655행에서 20000 이다. confirmServiceServing (672-693행) 은 설치/시작/repair가 만든 포트에 프록시가 답하는지 500ms마다 두드린다. 마감이 지나면 ok false 를 돌려준다. reportServiceServing (704-721행) 은 그 실패를 경고로 찍고 process.exitCode 를 1로 넣는다. GUI 업데이트 워커가 자식 종료 코드를 읽기 때문에, 등록만 되고 침묵하는 서비스를 성공으로 보고하지 않으려는 설계다. 그 설계 자체는 맞다. 문제는 윈도우 콜드 스타트가 그 20초를 가끔 넘는다는 점이다.

    작성자가 본 순서와 코드가 맞물린다. ACL 단단히 하기가 타임아웃으로 넘어가고, 저널에서 Codex 상태를 되돌린 뒤에야 리스너가 뜬다. repair 명령은 20초에 포기한다. 같은 pid는 그 직후 건강하다. 크래시 루프가 아니다. 이슈 720은 GUI 업데이트 워커가 재시작을 15초에 확인하는 경계였다. 이번은 confirmServiceServing 경로라 중복이 아니다.

    이슈 2983은 윈도우 런처 테스트의 PROXY_HEALTH_TIMEOUT_MS 를 25초에서 90초로 올렸다. 주석대로 프로덕션 런처는 그대로고, 이 service install/repair 20초도 그대로다. 이슈 2963의 ACL 건너뛰기는 기본이 꺼져 있어서, 이번 콜드 스타트에는 ACL 타임아웃이 그대로 붙는다. 루프는 마감 뒤에 한 번 더 두드리지 않고 바로 실패를 돌려줄 수 있다. 에러 글도 715행에서 timeoutMs 가 아니라 상수 20초를 찍는다. 이슈 3008과 같은 날 같은 복구에서 나왔지만 코드는 다르다. 한 PR에 섞지 않는 편이 맞다.

    라인 655 (src/service.ts SERVICE_INSTALL_HEALTH_MS) - 설치/시작/repair 건강 대기가 20초 고정이다. 윈도우에서 ACL 타임아웃과 저널 복구가 붙으면 이 값을 넘길 수 있다.

    라인 688-691 (confirmServiceServing) - 프로브가 마감 시점에 실패하면 바로 ok false 다. 그 직후 바인드가 끝나도 한 번 더 두드리지 않는다.

    라인 713-720 (reportServiceServing) - 실패 글자가 항상 상수 20초를 찍는다. timeoutMs 로 다른 값을 넣어도 글자는 20으로 남는다. 종료 코드 1은 GUI가 죽은 포트를 성공으로 착각하지 않게 하려는 설계라서 유지하는 편이 맞다.

    메인테이너의 판단이 필요한 지점

    • 20초를 전 플랫폼 기본으로 두고 윈도우만 더 긴 상한을 둘지, 서비스가 아직 뛰고 있으면 짧은 유예만 줄지
    • 마감 직후 최종 프로브만으로 충분한지, 콜드 스타트 예산 자체를 늘려야 하는지. 이번 재현은 후자에 가깝다
    • 이슈 3008과 한 이슈로 묶지 말지. 같은 복구 이야기지만 파일과 실패 지점이 다르다

    너의 추천
    별도 고침 PR을 받으세요. 윈도우 콜드 스타트에서는 confirmServiceServing 대기를 더 길게 두세요. 이슈 2983이 런처 테스트에서 보여 준 것처럼 20초는 짧다. 마감에 최종 프로브 한 번도 넣으세요. 실패 글자는 실제 기다린 밀리초를 찍게 고치세요. 테스트는 마감 직전 실패 후 직후 성공이면 ok가 되는지와, 끝까지 안 뜨면 실패하는지를 고정하면 됩니다. 이슈 720과 합치지 말고, 이슈 3008과도 파일을 섞지 마세요.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Sep 1, 2026

    @lidge-jun
    Owner

    Fixed on dev by b14b741dc (#3134, rebase carry of #3104).

    confirmServiceServing had a fixed 20s deadline and returned the moment the clock passed it. A Windows cold start does NTFS ACL hardening and previous-session journal recovery before the listener exists, so a service that bound a few seconds late and then stayed healthy was reported as a terminal failure with exit 1 — and the caller's fallback then starts a second proxy against a port that is about to be taken, which is worse than waiting.

    Windows now gets a 45s budget; no other platform changes. There is also a final probe at the deadline rather than a bare timeout, so a service that comes up in the last moment is seen.

    Worth noting because it is unusually direct evidence: the CI windows-schtasks job on the merged branch printed

    ⚠️  Service installed, but no proxy answered on port 10199 within 45s.
    

    That is the new budget running on a real Windows runner. The job passed on rerun — the runner was simply slower than 45s once — which is the same class of slowness you reported, at the far end of it.

    Closing manually: PRs here target dev, so GitHub's auto-close on Closes # does not fire.

  4. github-actions commented on Sep 1, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)serviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions