Mayug Maniparambil Claude Opus 4.8 (1M context) commited on
Commit
30865cf
·
1 Parent(s): b266f7c

Isolate flow_free verification in a timeout-bounded subprocess

Browse files

flow_free was the last verifier running in-process. Its FlowFree solver
shells out to a compiled C binary with no internal timeout (a slow board
hangs the worker) and raises SystemExit on a missing compiler/binary
(which the submit handler's `except Exception` would not catch). Run it in
a short-lived subprocess (30s timeout) via flow_free_runner.py, matching
the isolation already applied to the native RLP families.

Every verifier family (flow_free, undead, loopy, bridges, galaxies,
pattern) now runs its solve-check in an isolated subprocess with a
timeout, so no native crash/hang can take down the web worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

space_app/flow_free_runner.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone runner for flow_free verification.
2
+
3
+ Invoked as a subprocess by VerificationService so that the FlowFree solver
4
+ (which compiles/runs a native binary, has no internal timeout, and can raise
5
+ SystemExit on a missing compiler/binary) cannot hang or crash the web worker.
6
+
7
+ Reads a JSON object {"problem_ascii": ..., "board_ascii": ...} on stdin and
8
+ writes the verification result dict as JSON to stdout.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ ROOT_DIR = Path(__file__).resolve().parents[1]
17
+ EVALS_SRC_DIR = ROOT_DIR / "evals" / "src"
18
+ if str(EVALS_SRC_DIR) not in sys.path:
19
+ sys.path.insert(0, str(EVALS_SRC_DIR))
20
+
21
+
22
+ def main() -> None:
23
+ payload = json.loads(sys.stdin.read())
24
+ from evals_verifier import verify_flow_free # type: ignore
25
+
26
+ result = verify_flow_free(
27
+ payload.get("problem_ascii", ""),
28
+ payload.get("board_ascii", ""),
29
+ )
30
+ sys.stdout.write(json.dumps(result))
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
space_app/verification.py CHANGED
@@ -1,5 +1,6 @@
1
  from __future__ import annotations
2
 
 
3
  import os
4
  import subprocess
5
  import sys
@@ -16,7 +17,6 @@ from evals_verifier import ( # type: ignore
16
  check_bridges_structural_validity,
17
  check_galaxies_structural_validity,
18
  check_undead_structural_validity,
19
- verify_flow_free,
20
  verify_with_rlp,
21
  )
22
  from puzzles import PUZZLES # type: ignore
@@ -32,6 +32,11 @@ from puzzles import PUZZLES # type: ignore
32
  # own compiled solver binary, so it is naturally isolated.)
33
  SUBPROCESS_RLP_PUZZLES = {"undead", "loopy", "bridges", "galaxies", "pattern"}
34
  RLP_VERIFIER_TIMEOUT_SECONDS = 20
 
 
 
 
 
35
 
36
 
37
  class VerificationService:
@@ -109,6 +114,48 @@ class VerificationService:
109
  result["stderr"] = completed.stderr.strip()[:500]
110
  return result
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  def verify(
113
  self,
114
  *,
@@ -119,7 +166,9 @@ class VerificationService:
119
  ) -> dict[str, Any]:
120
  spec = PUZZLES[puzzle_type]
121
  if spec.verifier_type == "flow_free":
122
- return verify_flow_free(problem_ascii, board_ascii)
 
 
123
  if spec.verifier_type in SUBPROCESS_RLP_PUZZLES:
124
  return self._verify_rlp_with_subprocess(
125
  puzzle_type=spec.verifier_type,
 
1
  from __future__ import annotations
2
 
3
+ import json
4
  import os
5
  import subprocess
6
  import sys
 
17
  check_bridges_structural_validity,
18
  check_galaxies_structural_validity,
19
  check_undead_structural_validity,
 
20
  verify_with_rlp,
21
  )
22
  from puzzles import PUZZLES # type: ignore
 
32
  # own compiled solver binary, so it is naturally isolated.)
33
  SUBPROCESS_RLP_PUZZLES = {"undead", "loopy", "bridges", "galaxies", "pattern"}
34
  RLP_VERIFIER_TIMEOUT_SECONDS = 20
35
+ # flow_free shells out to a compiled C solver with no internal timeout and can
36
+ # raise SystemExit on a missing compiler/binary; isolate it too so it can never
37
+ # hang or crash the worker. Enumerating all solutions can be slower than the
38
+ # RLP solved-check, so it gets a more generous budget.
39
+ FLOW_FREE_TIMEOUT_SECONDS = 30
40
 
41
 
42
  class VerificationService:
 
114
  result["stderr"] = completed.stderr.strip()[:500]
115
  return result
116
 
117
+ def _verify_flow_free_with_subprocess(
118
+ self, *, problem_ascii: str, board_ascii: str
119
+ ) -> dict[str, Any]:
120
+ result = {
121
+ "board_exists": bool(board_ascii),
122
+ "board_valid": False,
123
+ "board_modified": False,
124
+ "correct": False,
125
+ }
126
+ if not board_ascii:
127
+ return result
128
+
129
+ runner_path = Path(__file__).resolve().parent / "flow_free_runner.py"
130
+ payload = json.dumps({"problem_ascii": problem_ascii, "board_ascii": board_ascii})
131
+ env = {**os.environ, "PYTHONWARNINGS": "ignore"}
132
+ try:
133
+ completed = subprocess.run(
134
+ [sys.executable, str(runner_path)],
135
+ input=payload,
136
+ text=True,
137
+ capture_output=True,
138
+ cwd=ROOT_DIR,
139
+ timeout=FLOW_FREE_TIMEOUT_SECONDS,
140
+ check=False,
141
+ env=env,
142
+ )
143
+ except subprocess.TimeoutExpired:
144
+ result["error"] = "Flow_free verification timed out."
145
+ return result
146
+
147
+ if completed.returncode == 0 and completed.stdout.strip():
148
+ try:
149
+ parsed = json.loads(completed.stdout)
150
+ if isinstance(parsed, dict):
151
+ return parsed
152
+ except json.JSONDecodeError:
153
+ pass
154
+ result["error"] = "Flow_free verification failed."
155
+ if completed.stderr.strip():
156
+ result["stderr"] = completed.stderr.strip()[:500]
157
+ return result
158
+
159
  def verify(
160
  self,
161
  *,
 
166
  ) -> dict[str, Any]:
167
  spec = PUZZLES[puzzle_type]
168
  if spec.verifier_type == "flow_free":
169
+ return self._verify_flow_free_with_subprocess(
170
+ problem_ascii=problem_ascii, board_ascii=board_ascii
171
+ )
172
  if spec.verifier_type in SUBPROCESS_RLP_PUZZLES:
173
  return self._verify_rlp_with_subprocess(
174
  puzzle_type=spec.verifier_type,
tests/test_verification.py CHANGED
@@ -146,6 +146,41 @@ class VerificationTests(unittest.TestCase):
146
  self.assertFalse(result["correct"])
147
  self.assertEqual(result.get("error"), "Loopy verification timed out.")
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def test_all_native_rlp_families_are_subprocess_isolated(self) -> None:
150
  # Every puzzle family verified via the in-process RLP C library must be
151
  # routed through the crash-isolating subprocess path. flow_free is
 
146
  self.assertFalse(result["correct"])
147
  self.assertEqual(result.get("error"), "Loopy verification timed out.")
148
 
149
+ def test_flow_free_runs_in_isolated_subprocess(self) -> None:
150
+ verifier = VerificationService()
151
+ problem = "BA...A\n..E...\n..D.F.\n..F..D\n..C.CE\nB....."
152
+ solution = "BAAAAA\nBEEDDD\nBEDDFD\nBEFFFD\nBECCCE\nBEEEEE"
153
+ with patch(
154
+ "space_app.verification.subprocess.run", wraps=subprocess.run
155
+ ) as run_spy:
156
+ try:
157
+ result = verifier.verify(
158
+ puzzle_type="flow_free",
159
+ problem_ascii=problem,
160
+ board_ascii=solution,
161
+ args="6x6",
162
+ )
163
+ except BaseException as caught: # pragma: no cover - native best effort
164
+ self.skipTest(f"FlowFree solver unavailable locally: {caught}")
165
+ return
166
+ run_spy.assert_called_once()
167
+ self.assertTrue(result["correct"])
168
+
169
+ def test_flow_free_verification_timeout_returns_clean_error(self) -> None:
170
+ verifier = VerificationService()
171
+ with patch(
172
+ "space_app.verification.subprocess.run",
173
+ side_effect=subprocess.TimeoutExpired("cmd", 30),
174
+ ):
175
+ result = verifier.verify(
176
+ puzzle_type="flow_free",
177
+ problem_ascii="A..A",
178
+ board_ascii="AAAA",
179
+ args="2x2",
180
+ )
181
+ self.assertFalse(result["correct"])
182
+ self.assertEqual(result.get("error"), "Flow_free verification timed out.")
183
+
184
  def test_all_native_rlp_families_are_subprocess_isolated(self) -> None:
185
  # Every puzzle family verified via the in-process RLP C library must be
186
  # routed through the crash-isolating subprocess path. flow_free is