Spaces:
Running
Subprocess-isolate all native RLP verifiers (bridges/galaxies/pattern)
Browse filesbridges, 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 +37 -14
- tests/test_verification.py +9 -0
|
@@ -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
|
| 24 |
-
# library can segfault or hang when its puzzle
|
| 25 |
-
# requests in a long-lived server process, which
|
| 26 |
-
# surfaces as an opaque HTTP 500 (and, sometimes, a
|
| 27 |
-
#
|
| 28 |
-
# and we return a clean "not solved"
|
| 29 |
-
|
|
|
|
|
|
|
| 30 |
RLP_VERIFIER_TIMEOUT_SECONDS = 20
|
| 31 |
|
| 32 |
|
| 33 |
class VerificationService:
|
| 34 |
-
def _structural_validity(
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
)
|
|
@@ -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)
|