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

Subprocess-isolate all native RLP verifiers (bridges/galaxies/pattern)

Browse files

bridges, galaxies, and pattern were still verifying in-process via the
same RLP C library (loaded with ctypes) that took down the loopy verifier
on the live Space. The crash is environment-specific to the long-lived
server process (not reproducible in single-shot local runs), so the only
safe option is to isolate every native check, not just the one observed
to fail.

Extend SUBPROCESS_RLP_PUZZLES to {undead, loopy, bridges, galaxies,
pattern} so all of them run the solved-check in a short-lived subprocess.
Pure-Python structural checks (board_valid/board_modified, including the
bridges/galaxies start-board modification check) still run in-process.
Silence subprocess warnings via PYTHONWARNINGS=ignore. flow_free is
already isolated (it shells out to its own compiled solver).

Add a guard test asserting every native family stays subprocess-routed.

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

space_app/verification.py CHANGED
@@ -13,6 +13,8 @@ if str(EVALS_SRC_DIR) not in sys.path:
13
  sys.path.insert(0, str(EVALS_SRC_DIR))
14
 
15
  from evals_verifier import ( # type: ignore
 
 
16
  check_undead_structural_validity,
17
  verify_flow_free,
18
  verify_with_rlp,
@@ -20,21 +22,34 @@ from evals_verifier import ( # type: ignore
20
  from puzzles import PUZZLES # type: ignore
21
 
22
 
23
- # Puzzle families whose "solved" check runs in the native RLP C library. That
24
- # library can segfault or hang when its puzzle objects are reused across many
25
- # requests in a long-lived server process, which crashes the web worker and
26
- # surfaces as an opaque HTTP 500 (and, sometimes, a hung request). We run those
27
- # checks in a short-lived subprocess so a native crash/hang kills only the child
28
- # and we return a clean "not solved" result instead of taking down the worker.
29
- SUBPROCESS_RLP_PUZZLES = {"undead", "loopy"}
 
 
30
  RLP_VERIFIER_TIMEOUT_SECONDS = 20
31
 
32
 
33
  class VerificationService:
34
- def _structural_validity(self, *, puzzle_type: str, board_ascii: str) -> bool:
35
- # Pure-Python structural pre-checks are safe to run in-process. Puzzle
36
- # families without one are treated as structurally valid here; the
37
- # subprocess solved-check is the source of truth for correctness.
 
 
 
 
 
 
 
 
 
 
 
38
  if puzzle_type == "undead":
39
  return bool(check_undead_structural_validity(board_ascii))
40
  return True
@@ -43,6 +58,7 @@ class VerificationService:
43
  self,
44
  *,
45
  puzzle_type: str,
 
46
  board_ascii: str,
47
  args: str,
48
  ) -> dict[str, Any]:
@@ -56,7 +72,7 @@ class VerificationService:
56
  return result
57
 
58
  result["board_valid"] = self._structural_validity(
59
- puzzle_type=puzzle_type, board_ascii=board_ascii
60
  )
61
  if not result["board_valid"]:
62
  result["board_modified"] = True
@@ -65,8 +81,14 @@ class VerificationService:
65
  verifier_path = ROOT_DIR / "submodules" / "rlp" / "verifier.py"
66
  command = [sys.executable, str(verifier_path), puzzle_type, "--arg", args]
67
  # The RLP libraries pull in pygame, which noisily probes audio/video on
68
- # import; force the dummy drivers so a headless verifier run stays silent.
69
- env = {**os.environ, "SDL_AUDIODRIVER": "dummy", "SDL_VIDEODRIVER": "dummy"}
 
 
 
 
 
 
70
  try:
71
  completed = subprocess.run(
72
  command,
@@ -101,6 +123,7 @@ class VerificationService:
101
  if spec.verifier_type in SUBPROCESS_RLP_PUZZLES:
102
  return self._verify_rlp_with_subprocess(
103
  puzzle_type=spec.verifier_type,
 
104
  board_ascii=board_ascii,
105
  args=args,
106
  )
 
13
  sys.path.insert(0, str(EVALS_SRC_DIR))
14
 
15
  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,
 
22
  from puzzles import PUZZLES # type: ignore
23
 
24
 
25
+ # Puzzle families whose "solved" check runs in the native RLP C library (loaded
26
+ # in-process via ctypes). That library can segfault or hang when its puzzle
27
+ # objects are reused across many requests in a long-lived server process, which
28
+ # crashes the web worker and surfaces as an opaque HTTP 500 (and, sometimes, a
29
+ # hung request). We run every native check in a short-lived subprocess so a
30
+ # native crash/hang kills only the child and we return a clean "not solved"
31
+ # result instead of taking down the worker. (flow_free already shells out to its
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:
38
+ def _structural_validity(
39
+ self, *, puzzle_type: str, board_ascii: str, problem_ascii: str
40
+ ) -> bool:
41
+ # Pure-Python structural pre-checks are safe to run in-process and also
42
+ # cover the "start board was modified" case for bridges/galaxies. Puzzle
43
+ # families without such a check are treated as structurally valid here;
44
+ # the subprocess solved-check is the source of truth for correctness.
45
+ if puzzle_type == "bridges":
46
+ return bool(
47
+ check_bridges_structural_validity(board_ascii, problem_ascii=problem_ascii)
48
+ )
49
+ if puzzle_type == "galaxies":
50
+ return bool(
51
+ check_galaxies_structural_validity(board_ascii, problem_ascii=problem_ascii)
52
+ )
53
  if puzzle_type == "undead":
54
  return bool(check_undead_structural_validity(board_ascii))
55
  return True
 
58
  self,
59
  *,
60
  puzzle_type: str,
61
+ problem_ascii: str,
62
  board_ascii: str,
63
  args: str,
64
  ) -> dict[str, Any]:
 
72
  return result
73
 
74
  result["board_valid"] = self._structural_validity(
75
+ puzzle_type=puzzle_type, board_ascii=board_ascii, problem_ascii=problem_ascii
76
  )
77
  if not result["board_valid"]:
78
  result["board_modified"] = True
 
81
  verifier_path = ROOT_DIR / "submodules" / "rlp" / "verifier.py"
82
  command = [sys.executable, str(verifier_path), puzzle_type, "--arg", args]
83
  # The RLP libraries pull in pygame, which noisily probes audio/video on
84
+ # import; force the dummy drivers (and silence warnings) so a headless
85
+ # verifier run does not pollute the captured stderr.
86
+ env = {
87
+ **os.environ,
88
+ "SDL_AUDIODRIVER": "dummy",
89
+ "SDL_VIDEODRIVER": "dummy",
90
+ "PYTHONWARNINGS": "ignore",
91
+ }
92
  try:
93
  completed = subprocess.run(
94
  command,
 
123
  if spec.verifier_type in SUBPROCESS_RLP_PUZZLES:
124
  return self._verify_rlp_with_subprocess(
125
  puzzle_type=spec.verifier_type,
126
+ problem_ascii=problem_ascii,
127
  board_ascii=board_ascii,
128
  args=args,
129
  )
tests/test_verification.py CHANGED
@@ -145,3 +145,12 @@ class VerificationTests(unittest.TestCase):
145
  )
146
  self.assertFalse(result["correct"])
147
  self.assertEqual(result.get("error"), "Loopy verification timed out.")
 
 
 
 
 
 
 
 
 
 
145
  )
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
152
+ # excluded (it shells out to its own compiled solver binary).
153
+ from space_app.verification import SUBPROCESS_RLP_PUZZLES
154
+
155
+ for family in ("loopy", "bridges", "galaxies", "pattern", "undead"):
156
+ self.assertIn(family, SUBPROCESS_RLP_PUZZLES)