Skip to content

Test and document the runaway-warmup escape: daemon stop, then offline untrack #1029

Closed

Description

@zzet

Why

Sometimes the daemon starts warming a repository the user did not mean to track, such as a home directory, a monorepo parent or a huge vendored tree. That warmup can run for many minutes or hours and keep the machine busy. We do not yet promise that untracking the repo works while the daemon is still warming it. The supported way out is:

  1. stop the daemon;
  2. remove the repo from the config while the daemon is down;
  3. start the daemon again.

Today no test covers this path from start to finish, and no doc describes it.

Stop itself is slow right now. Every gortex daemon stop is force-killed after about 18 s: a 15 s exit grace plus up to 15 s for the checkpoint on close. A separate shutdown issue sets the stop budget:

  • ≤3 s when idle;
  • ≤5 s after a cold index or after churn;
  • 0 SIGKILLs in 20 runs;
  • the store is closed and its lock released.

This issue holds the escape hatch to that same budget. The v0.64.12 upgrade note will tell users: "to stop a runaway warmup: gortex daemon stop, then offline gortex untrack". This issue makes that sentence true and keeps it true.

Current state

Verified on main:

  • Stop (cmd/gortex/daemon.go, runDaemonStop):
    • It records a sticky stop intent (daemon.MarkStopIntent), so an editor's gortex mcp proxy does not respawn the daemon.
    • When systemd or launchd owns the daemon (serviceActive()), it stops it through serviceStop. Otherwise it calls stopRunningDaemon, which waits for the process to exit and release the store lock.
    • daemonStopExitGrace sets how long it waits before a force-kill: 15 s after an ack, 2 min with no ack, 5 min at most.
  • Untrack (cmd/gortex/track.go, runUntrack):
    • It asks untrackDaemonIndex whether a running daemon owns the target (daemon.IsRunning() plus daemonOwnsRepo in cmd/gortex/cli_daemon.go).
    • If no daemon owns it, it falls back to editing the config only: config.LoadGlobal → RemoveRepo → Save. It prints [gortex] untracked <path> (config only) and does not open the store.
  • Gotcha: untrack by prefix needs the daemon. RemoveRepo (internal/config/global.go) makes its argument absolute and matches it by path identity. A bare repo prefix such as gortex untrack backend, which the docs show, therefore resolves against the current directory when the daemon is down. It fails with repository not found unless the user runs it from the repo's parent. Offline untrack only works reliably with a path.
  • Next start (cmd/gortex/daemon_state.go): warmup runs an orphan-prefix purge first (orphan_maintenance, then orphan_prefix_scan, then orphan_prefix_purge). It purges every stored repo prefix that no tracked repo in the config maps to. A repo removed from the config while the daemon was down therefore has its rows purged on the next start instead of being indexed.
  • Tests: cmd/gortex/track_offline_test.go covers only offline track (TestTrack_OfflineSucceeds, TestTrack_OfflineIdempotent). No test covers offline untrack. No test covers stop → untrack → start against a real daemon in mid-warmup.
  • Docs: docs/cli.md says track and untrack fall back "to config-only when no daemon" is running. docs/onboarding.md has a Troubleshooting section, but it says nothing about recovering from a runaway warmup.

What to do

  • Gated end-to-end test. Skip it unless an opt-in env var is set (for example GORTEX_E2E=1), so the default go test ./... stays fast. The test:
    1. builds or reuses the gortex binary and runs it against an isolated HOME, config, socket and store;
    2. tracks a small fixture repo A and a large synthetic repo B, then starts the daemon. B must be big enough that its cold warmup is still running when the test acts;
    3. waits until gortex daemon status shows warmup of B in progress, then runs gortex daemon stop and records its wall-clock time;
    4. checks that the process exited without SIGKILL, the socket is gone and the store lock is released (a fresh store open succeeds);
    5. runs gortex untrack <absolute path of B> and checks that it took the config-only path, exited 0, removed B from the global config and kept A;
    6. starts the daemon again and checks that A becomes ready, B is neither warmed nor indexed, and B's repo prefix has no rows left after the orphan purge;
    7. logs each stop duration and whether a SIGKILL happened, so a repeated run can be audited.
  • Unit tests for the offline runUntrack path, next to the existing offline-track tests, with no daemon running:
    • removing a tracked path deletes that entry and keeps the others;
    • removing an unknown path returns a clear repository not found error and leaves the config unchanged;
    • a bare prefix given from a directory where it does not resolve returns the same clear error and does not delete some other entry.
  • Docs. Add a short "Recovering from a runaway warmup" section to the Troubleshooting part of docs/onboarding.md, and link it from docs/cli.md. It covers:
    • gortex daemon stop, and that it goes through the service manager when one is installed;
    • gortex untrack <path> with the daemon down, using a path rather than a bare prefix;
    • gortex daemon start;
    • what to expect: the repo's rows are purged on the next start;
    • how to confirm, with gortex daemon status.
  • Upgrade note. Use the same steps and wording in the v0.64.12 upgrade note.

Out of scope

  • Making stop itself fast. The separate shutdown issue owns that, and the stop-timing targets below can only pass once it is merged.
  • Supporting untrack against a daemon that is still warming the target.
  • Making offline untrack accept a bare repo prefix (it is documented and tested as a clear error here, not fixed).
  • Windows service-manager specifics beyond what the existing stop path already does.

Done when

  • The gated end-to-end test exists and is skipped when the opt-in env var is unset.
  • With the env var set, the test passes on macOS and Linux in 20 consecutive runs (-count=20).
  • In those runs, gortex daemon stop during an active cold warmup returns in ≤5 s wall clock every time. There are 0 SIGKILLs across all 20 runs, and the store lock is released before the next step.
  • Offline gortex untrack <path> returns in ≤1 s, exits 0 and removes only the target entry from the global config.
  • After the restart:
    • the untracked repo is not warmed and gortex daemon status does not list it;
    • its repo prefix has 0 rows in the store;
    • the other repo reaches ready.
  • Unit tests for offline runUntrack cover the three cases above (tracked path, unknown path, unresolvable bare prefix) and pass under go test -race ./cmd/gortex/.
  • The troubleshooting section is merged in docs/onboarding.md with a link from docs/cli.md, and the v0.64.12 upgrade note uses the same steps.
  • The gated test is part of the release-gate run on the reference Linux host and the reference Mac.

How to verify

  • go test -race -run 'Untrack.*Offline' ./cmd/gortex/
  • GORTEX_E2E=1 go test -run TestOfflineUntrackEscapeHatch -count=20 ./cmd/gortex/, then read the logged stop durations and SIGKILL count. Run this on macOS and on Linux.
  • By hand:
    1. Track a large directory and run gortex daemon start.
    2. Wait for warmup to begin, then run time gortex daemon stop.
    3. Run gortex untrack /abs/path/to/dir, then gortex daemon start.
    4. Run gortex daemon status: the directory is gone and its rows were purged (look for the orphan_prefix_purge step in the daemon log).

Dependencies

Blocked by (must land first):

Related issues

Planning

  • Milestone: v0.64.12
  • Priority: enabler
  • Estimate: ~0.5 engineer-day
  • Isolated enough for a community contributor.

Activity

  1. added this to the v0.64.12 milestone on Oct 25, 2026
  2. added
    documentationImprovements or additions to documentation
    help wantedExtra attention is needed
    daemonDaemon lifecycle, sessions and repo registry
    ci-testsCI pipeline and test-suite health
    enablerMeasurement or groundwork other items depend on
    on Oct 25, 2026
  3. moved this to Todo in Gortexon Oct 25, 2026
  4. moved this from Todo to In Progress in Gortexon Oct 28, 2026
  5. zzet commented on Oct 28, 2026

    @zzet
    OwnerAuthor

    Implementation is prepared in an isolated worktree at 40565f6a6, based on the shutdown work in #1022.

    • Three offline untrack controls cover tracked-path removal, unknown paths and bare-prefix use from an unrelated directory.
    • A gated native test uses a private daemon and synthetic repositories. It requires an observed active cold index, measures the real CLI stop through process exit and socket removal within 5 seconds, then checks offline absolute-path untrack within 1 second and restart/orphan-prefix cleanup.
    • Recovery steps are documented in onboarding, CLI help and the v0.64.12 upgrade note. Release instructions retain verbose timing output and require 20 consecutive native passes on each of macOS and Linux.

    Source review is complete; automated checks and the native repetitions are still pending. The issue remains In Progress and is not accepted. #1022 must land first. Validation is queued behind the current #1005 checks so only one heavy local job runs at a time.

  6. zzet commented on Oct 28, 2026

    @zzet
    OwnerAuthor

    Native acceptance for #1029 now passes on both platforms at d55ae9c9e616c1f5947bb0d116db20e863f8c25f in #1172:

    Platform Consecutive passes Maximum complete stop Maximum offline untrack Forced kills
    macOS arm64 20/20 218.707 ms 258.553 ms 0
    Linux arm64 20/20 206.190 ms 28.215 ms 0

    Each platform also passed a separate pilot. The test verified active cold indexing before stop, released store locks, config-only removal of the absolute path, the retained repository reaching ready, and zero removed-prefix rows after restart. Independent cleanup checks found no owned processes, sockets or handles left behind. Linux ran under an 8-CPU/16-GiB cgroup with swap disabled; its VM is now stopped.

    Four focused race controls and all prescribed pinned local checks passed. Raw logs, artifact hashes and cleanup receipts are saved locally. Administrative setup failures are retained separately and were not counted as native runs.

    The issue remains In Progress: hosted CI and #1022 landing are still required. This result completes the native test requirement; it does not replace #1022's broader shutdown workload gates.

  7. zzet commented on Oct 29, 2026

    @zzet
    OwnerAuthor

    The native escape-hatch gate also passes on the prepared integration with the latest shutdown implementation.

    Test source: 517d4d146a781da3d4730e5698e4a4e588bca73a (currently local; PR push is held until #1022 lands). Non-test Go source matches the measured shutdown product at 5e223d667e9eca8d23e07f51787ddf21883362f1; the Linux product retains that source attribution. Both platforms used freshly built race test binaries from the current test source.

    Native gate Consecutive passes Complete stops Maximum complete stop Maximum offline untrack Forced kills
    macOS 20/20 40 301.858 ms 41.767 ms 0
    Linux 20/20 40 175.393 ms 28.139 ms 0

    Every case observed active cold indexing, stopped normally within 5 seconds, released the store lock, removed only the requested absolute path through the offline config-only command within 1 second, and restarted with the remaining repository ready and the removed prefix purged. Actual test/child processes, fixture/socket paths and handles were checked after completion. The Linux VM has been stopped normally.

    The first Linux compile attempt failed before any product execution because an owned copy inherited read-only cache directory permissions. Only that owned copy was corrected; the failed preflight evidence is retained. There were no failed or retried native acceptance cases.

    Linux raw evidence archive SHA-256: 6b06d6152c6264c079ca763b1c7eb6d71dcc765967c25b05f902e8d7032057b8; current Linux race test binary: 80920e4b784dbd6c81d5115c17966a5879873217947fbd0867af82930e00606d. Local evidence is retained under the external SSD's gortex/validation/v06412-1029/ directory. Earlier results at d55ae9c9 remain historical observations, not relabeled current-source measurements.

    Remaining: #1022 must merge; then retarget #1172 to main, incorporate any final dependency test fixes, run the prescribed checks before pushing, and pass the standard affected CI. The issue remains In Progress until those steps complete.

  8. moved this from In Progress to Done in Gortexon Oct 29, 2026
  9. zzet commented on Oct 29, 2026

    @zzet
    OwnerAuthor

    Accepted after PR #1172 passed its required CI and merged, following prerequisite #1022.

    The opt-in escape-hatch test passed 20 consecutive native cases on macOS and 20 on Linux, covering 80 normal daemon exits with zero forced kills. Maximum complete stop was 301.858 ms on macOS and 175.393 ms on Linux (5-second limit); maximum offline untrack was 41.767 ms and 28.139 ms respectively (1-second limit). Every case checked active cold indexing, store-lock release, config-only removal of the absolute target path, the remaining repository becoming ready and removed-prefix cleanup after restart.

    Offline unit controls, affected race tests and the pinned lint/format/build checks passed. Native evidence retains source 517d4d146 and the Linux product's 5e223d667 attribution; final integration e6a8b4b799 changes only a tested upstream fixture. Earlier preflight failures remain recorded separately. The CLI, onboarding, upgrade and release-gate documentation now carry the recovery procedure.

    The issue is closed and moved to Done. Detailed native evidence.

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

    ci-testsCI pipeline and test-suite healthdaemonDaemon lifecycle, sessions and repo registrydocumentationImprovements or additions to documentationenablerMeasurement or groundwork other items depend onhelp wantedExtra attention is needed

    Projects

    • Status
      Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions