Run index_repository in a supervised worker subprocess with skip-and-continue * (Stage 3c). Returns the response string (caller frees): * - the worker's own response on a clean first run (the common path); * - after a crash/hang, the response from a clean single-threaded RECOVERY run * that quarantines the culprit file(s) — status="indexed" with them listed in * skipped[] as phase
| 3643 | * Returns NULL only when the worker could not be spawned at all, so the caller |
| 3644 | * degrades to the in-process path. */ |
| 3645 | static char *index_run_supervised(cbm_mcp_server_t *srv, const char *args) { |
| 3646 | supervisor_invalidate_store(srv); |
| 3647 | |
| 3648 | /* First attempt: normal parallel run. */ |
| 3649 | cbm_index_worker_result_t wr; |
| 3650 | int rc = cbm_index_spawn_worker(args, false, NULL, NULL, &wr); |
| 3651 | |
| 3652 | if (rc != 0 || wr.outcome == CBM_PROC_SPAWN_FAILED) { |
| 3653 | cbm_index_worker_result_free(&wr); |
| 3654 | supervisor_invalidate_store(srv); |
| 3655 | return NULL; /* degrade to in-process */ |
| 3656 | } |
| 3657 | if (wr.outcome == CBM_PROC_CLEAN) { |
| 3658 | /* Clean exit → transfer the worker's response (the common path). If the |
| 3659 | * worker exited clean but wrote no response (a degenerate case, e.g. a |
| 3660 | * self binary that does not act as an index worker), resp is NULL and the |
| 3661 | * caller degrades to the in-process path — a clean run never needs the |
| 3662 | * crash-recovery loop. */ |
| 3663 | char *resp = wr.response; /* transfer ownership to caller (may be NULL) */ |
| 3664 | wr.response = NULL; |
| 3665 | cbm_index_worker_result_free(&wr); |
| 3666 | supervisor_invalidate_store(srv); |
| 3667 | return resp; |
| 3668 | } |
| 3669 | |
| 3670 | /* Crash / hang / nonzero exit → skip-and-continue recovery. Re-run the |
| 3671 | * worker PARALLEL (there are no sequential production runs) with the |
| 3672 | * per-file marker JOURNAL armed; after each failed run the journal's |
| 3673 | * open-S set is the in-flight SUSPECT set. A file is quarantined only |
| 3674 | * when it appears in the suspect sets of TWO CONSECUTIVE failed runs |
| 3675 | * (intersection — a stale or merely unlucky in-flight file rotates out), |
| 3676 | * and only ONE file per round: the OLDEST open S in the intersection |
| 3677 | * (for a hang the oldest still-open file IS the stuck one; for a crash |
| 3678 | * it is the longest-running suspect — the best single deterministic |
| 3679 | * pick). A clean run then indexes the good files and reports the |
| 3680 | * quarantined ones as phase="crash"/"hang" skips via the ordinary |
| 3681 | * Stage-2 skip plumbing. The old design re-ran SINGLE-THREADED to keep |
| 3682 | * one exact marker; at scale that fell into the sequential crawl, went |
| 3683 | * quiet, was killed as a hang mid-pass, and the stale marker got FOUR |
| 3684 | * innocent ms-typescript fixtures quarantined one 15-minute retry at a |
| 3685 | * time. */ |
| 3686 | cbm_proc_outcome_t last_outcome = wr.outcome; |
| 3687 | cbm_index_worker_result_free(&wr); |
| 3688 | |
| 3689 | char marker_path[CBM_SZ_1K]; |
| 3690 | char quarantine_path[CBM_SZ_1K]; |
| 3691 | supervisor_tmp_path(marker_path, sizeof(marker_path), ".marker"); |
| 3692 | supervisor_tmp_path(quarantine_path, sizeof(quarantine_path), ".quarantine"); |
| 3693 | (void)remove(marker_path); |
| 3694 | /* Start the quarantine list empty (truncate any stale file). */ |
| 3695 | FILE *qinit = cbm_fopen(quarantine_path, "wb"); |
| 3696 | if (qinit) { |
| 3697 | (void)fclose(qinit); |
| 3698 | } |
| 3699 | |
| 3700 | int cap = 100; |
| 3701 | const char *cap_env = getenv("CBM_INDEX_MAX_RESTARTS"); |
| 3702 | if (cap_env && cap_env[0]) { |
no test coverage detected