1d92d37f9e
On 0.9.0 -> 0.10.2, `update -y` auto-confirmed deleting other projects' indexes, removed two .db files (~59 MB), and THEN aborted on an interactive prompt it could not answer in a non-interactive shell. The run failed; the data was already gone. Reported by @PsyTech-Rob. The current code is already correct: both activation flows prompt early but delete late, after the binary swap and agent configuration, and both return before the deletion when configuration fails. #1538 also removed the specific prompt that aborted. So this commit fixes nothing — it stops the fix from being undone, which is the part that was missing. The reporter made exactly this point: removing one bad prompt addressed the instance, not the rule. Contract: in each flow that deletes indexes, cbm_remove_indexes() must appear after the agent-configuration call. It keys on the CALL rather than a local result variable, because the two flows spell that result differently, and it fails loudly if a flow is renamed rather than passing vacuously. Verified by reverting: moving the deletion ahead of configuration reproduces Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> #1558's shape and the contract goes red.
93 lines
3.6 KiB
Bash
93 lines
3.6 KiB
Bash
#!/usr/bin/env bash
|
|
# Contract: irreversible work must never run before a step that can still abort.
|
|
#
|
|
# This exists because it destroyed a user's data. On 0.9.0 -> 0.10.2,
|
|
# `update -y` auto-confirmed deleting OTHER projects' indexes, removed two .db
|
|
# files (~59 MB), and THEN aborted on an interactive prompt it could not answer
|
|
# under a non-interactive shell (#1558). The run failed, and the data was
|
|
# already gone. The prompt that aborted has since been removed, but that fixed
|
|
# one instance rather than the rule.
|
|
#
|
|
# The rule: in both activation flows, the index deletion must come AFTER the
|
|
# agent-configuration step, so any failure that can still stop the run happens
|
|
# while the indexes are intact. Both flows already return early when agent
|
|
# configuration fails, so ordering is the whole guarantee.
|
|
#
|
|
# This is a source-order check rather than a behavioural one, deliberately: the
|
|
# property is "the destructive call sits late in this function", and reordering
|
|
# it is exactly the regression to catch. A behavioural test would need to drive
|
|
# a full activation to failure, which is far more machinery for a weaker signal.
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
CLI="$ROOT/src/cli/cli.c"
|
|
|
|
[ -f "$CLI" ] || { echo "FAIL: cannot find $CLI" >&2; exit 1; }
|
|
|
|
python3 - "$CLI" <<'PY'
|
|
import re, sys
|
|
|
|
src = open(sys.argv[1], encoding="utf-8").read()
|
|
failures = []
|
|
|
|
# The two activation entry points that may delete indexes. Each is checked in
|
|
# isolation so a call in one cannot satisfy the contract for the other.
|
|
FLOWS = [
|
|
("cli_install_activate", "install"),
|
|
("cli_update_activate_binary", "update"),
|
|
]
|
|
|
|
def body_of(name):
|
|
"""Source text of a static function, from its signature to the next
|
|
top-level closing brace."""
|
|
m = re.search(r"^static int " + re.escape(name) + r"\(.*?\{", src, re.S | re.M)
|
|
if not m:
|
|
return None
|
|
start = m.end()
|
|
depth = 1
|
|
i = start
|
|
while i < len(src) and depth:
|
|
if src[i] == "{":
|
|
depth += 1
|
|
elif src[i] == "}":
|
|
depth -= 1
|
|
i += 1
|
|
return src[start:i]
|
|
|
|
for func, label in FLOWS:
|
|
body = body_of(func)
|
|
if body is None:
|
|
# Renamed or restructured: fail loudly rather than pass vacuously.
|
|
failures.append(f"{label}: could not find {func}() — update this contract "
|
|
f"rather than deleting it")
|
|
continue
|
|
|
|
delete_at = body.find("cbm_remove_indexes")
|
|
if delete_at < 0:
|
|
# No deletion in this flow at all is fine — nothing to order.
|
|
continue
|
|
|
|
# Key on the configuration CALL, not a local variable name: the two flows
|
|
# spell the result differently (one stores agent_config_rc, the other tests
|
|
# the call inline), and the call is the thing that must precede deletion.
|
|
config_at = body.find("cbm_install_agent_configs")
|
|
if config_at < 0:
|
|
failures.append(f"{label}: deletes indexes but no agent-configuration step "
|
|
f"was found before it; the ordering guarantee cannot hold")
|
|
continue
|
|
|
|
if delete_at < config_at:
|
|
failures.append(
|
|
f"{label}: cbm_remove_indexes() runs BEFORE the agent-configuration\n"
|
|
f" step. A failure after that point destroys data the user was\n"
|
|
f" told would be rebuilt, on a run that then aborts (#1558).")
|
|
|
|
if failures:
|
|
for f in failures:
|
|
print("FAIL: " + f, file=sys.stderr)
|
|
print(f"destructive-ordering contract FAILED with {len(failures)} violation(s)",
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
print("PASS: index deletion runs after every step that can still abort the run")
|
|
PY
|