| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405 |
- from __future__ import annotations
- import os
- from concurrent.futures import ThreadPoolExecutor
- import polars as pl
- import pytest
- from tests.core.data_rules.test_polars_compiler import (
- _backend,
- _binding,
- _published_rule,
- _schema,
- )
- def _compile_worker_plan(
- *,
- input_schema,
- output_schema,
- steps,
- lookup_context=None,
- ):
- from app.core.data_rules.compilers.polars import PolarsRuleCompiler
- input_binding = _binding(input_schema, access_mode="read")
- output_binding = _binding(output_schema, access_mode="write")
- backend = _backend(
- max_rows=1_000_000,
- max_artifact_bytes=256 * 1024 * 1024,
- memory_limit_bytes=16 * 1024 * 1024,
- lookup_bindings=lookup_context or {},
- )
- rule = _published_rule(
- input_schema,
- output_schema,
- steps,
- )
- return PolarsRuleCompiler().compile(
- rule_version=rule,
- input_schema=input_schema,
- output_schema=output_schema,
- input_binding=input_binding,
- output_binding=output_binding,
- backend=backend,
- )["plan"]
- def _assert_worker_resource_failure(plan, tmp_path, *, lookup_paths=None):
- from app.runner.polars_worker import (
- PolarsWorkerResourceError,
- execute_isolated_polars_plan,
- )
- with pytest.raises(
- PolarsWorkerResourceError,
- match="hard memory limit|bounded execution time",
- ):
- execute_isolated_polars_plan(
- {
- "plan": plan,
- "input_path": str(tmp_path / "input.parquet"),
- "lookup_paths": lookup_paths or {},
- "output_path": str(tmp_path / "output.parquet"),
- "masking_policies": {},
- },
- memory_limit_bytes=plan["resource_limits"][
- "memory_limit_bytes"
- ],
- )
- def test_isolated_polars_worker_runs_outside_runner_process():
- from app.runner.polars_worker import run_isolated_memory_probe
- result = run_isolated_memory_probe(
- allocate_bytes=1_024,
- memory_limit_bytes=16 * 1024 * 1024,
- )
- assert result["worker_pid"] != os.getpid()
- assert result["allocated_bytes"] == 1_024
- def test_isolated_polars_worker_fails_deterministically_at_hard_memory_limit():
- from app.runner.polars_worker import (
- PolarsWorkerResourceError,
- run_isolated_memory_probe,
- )
- with pytest.raises(
- PolarsWorkerResourceError,
- match="hard memory limit",
- ):
- run_isolated_memory_probe(
- allocate_bytes=128 * 1024 * 1024,
- memory_limit_bytes=8 * 1024 * 1024,
- )
- def test_worker_start_failure_closes_pipe_endpoints_and_maps_safe_error(
- monkeypatch,
- ):
- from app.runner import polars_worker
- class Endpoint:
- def __init__(self):
- self.closed = False
- def close(self):
- self.closed = True
- class Process:
- pid = None
- def __init__(self):
- self.joined = False
- self.closed = False
- def start(self):
- raise OSError("sensitive spawn detail")
- def is_alive(self):
- return False
- def join(self, timeout=None):
- self.joined = True
- def close(self):
- self.closed = True
- parent = Endpoint()
- child = Endpoint()
- process = Process()
- class Context:
- def Pipe(self, duplex):
- assert duplex is False
- return parent, child
- def Process(self, **_kwargs):
- return process
- monkeypatch.setattr(
- polars_worker.multiprocessing,
- "get_context",
- lambda _method: Context(),
- )
- with pytest.raises(
- polars_worker.PolarsWorkerError,
- match="failed to start safely",
- ) as error:
- polars_worker.run_isolated_memory_probe(
- allocate_bytes=1,
- memory_limit_bytes=1024,
- )
- assert "sensitive" not in str(error.value)
- assert parent.closed is True
- assert child.closed is True
- assert process.closed is True
- def test_worker_rejects_golden_row_drift_inside_isolated_boundary(tmp_path):
- from app.runner.polars_worker import (
- PolarsWorkerError,
- execute_isolated_polars_plan,
- )
- schema = _schema("bd:golden:input", [("value", "string", False)])
- plan = _compile_worker_plan(
- input_schema=schema,
- output_schema={
- **schema,
- "id": _schema(
- "bd:golden:output", [("value", "string", False)]
- )["id"],
- "schema_ref": "bd:golden:output",
- },
- steps=[
- {
- "id": "trim_value",
- "op": "normalize_text",
- "column": "value",
- "trim": True,
- }
- ],
- )
- plan["resource_limits"]["memory_limit_bytes"] = 128 * 1024 * 1024
- input_path = tmp_path / "golden-input.parquet"
- golden_path = tmp_path / "golden-expected.parquet"
- pl.DataFrame({"value": ["actual"]}).write_parquet(input_path)
- pl.DataFrame({"value": ["expected"]}).write_parquet(golden_path)
- with pytest.raises(PolarsWorkerError, match="execution failed"):
- execute_isolated_polars_plan(
- {
- "plan": plan,
- "input_path": str(input_path),
- "lookup_paths": {},
- "output_path": str(tmp_path / "golden-output.parquet"),
- "golden_path": str(golden_path),
- "golden_digest": "a" * 64,
- "masking_policies": {},
- },
- memory_limit_bytes=plan["resource_limits"][
- "memory_limit_bytes"
- ],
- )
- def test_concurrent_large_golden_comparisons_fail_closed(tmp_path):
- from app.runner.polars_worker import (
- PolarsWorkerError,
- execute_isolated_polars_plan,
- )
- schema = _schema("bd:golden:large", [("value", "string", False)])
- plan = _compile_worker_plan(
- input_schema=schema,
- output_schema={
- **schema,
- "id": _schema(
- "bd:golden:large-output",
- [("value", "string", False)],
- )["id"],
- "schema_ref": "bd:golden:large-output",
- },
- steps=[
- {
- "id": "trim_value",
- "op": "normalize_text",
- "column": "value",
- "trim": True,
- }
- ],
- )
- plan["resource_limits"]["memory_limit_bytes"] = 128 * 1024 * 1024
- input_path = tmp_path / "large-input.parquet"
- golden_path = tmp_path / "large-golden.parquet"
- pl.DataFrame({"value": ["actual"]}).write_parquet(input_path)
- pl.DataFrame(
- {"value": [f"expected-{index:08d}" for index in range(200_000)]}
- ).write_parquet(golden_path)
- def compare(index):
- try:
- execute_isolated_polars_plan(
- {
- "plan": plan,
- "input_path": str(input_path),
- "lookup_paths": {},
- "output_path": str(
- tmp_path / f"large-output-{index}.parquet"
- ),
- "golden_path": str(golden_path),
- "golden_digest": "b" * 64,
- "masking_policies": {},
- },
- memory_limit_bytes=plan["resource_limits"][
- "memory_limit_bytes"
- ],
- )
- except PolarsWorkerError:
- return "rejected"
- return "unsafe-success"
- with ThreadPoolExecutor(max_workers=2) as executor:
- results = list(executor.map(compare, range(2)))
- assert results == ["rejected", "rejected"]
- def test_regex_peak_allocation_fails_inside_isolated_worker(tmp_path):
- schema = _schema(
- "bd:regex:raw",
- [("text", "string", False)],
- )
- plan = _compile_worker_plan(
- input_schema=schema,
- output_schema={
- **schema,
- "id": _schema(
- "bd:regex:clean",
- [("text", "string", False)],
- )["id"],
- "schema_ref": "bd:regex:clean",
- },
- steps=[
- {
- "id": "expand",
- "op": "regex_replace",
- "column": "text",
- "pattern": "a",
- "replacement": "x" * 64,
- }
- ],
- )
- pl.DataFrame({"text": ["a" * 1_000_000]}).write_parquet(
- tmp_path / "input.parquet"
- )
- _assert_worker_resource_failure(plan, tmp_path)
- def test_group_peak_allocation_fails_inside_isolated_worker(tmp_path):
- input_schema = _schema(
- "bd:group:raw",
- [("group_key", "string", False), ("amount", "integer", False)],
- )
- output_schema = _schema(
- "bd:group:clean",
- [
- ("group_key", "string", False),
- ("total_amount", "integer", False),
- ],
- )
- plan = _compile_worker_plan(
- input_schema=input_schema,
- output_schema=output_schema,
- steps=[
- {
- "id": "sum_by_key",
- "op": "aggregate",
- "group_by": ["group_key"],
- "aggregations": {
- "total_amount": {
- "function": "sum",
- "column": "amount",
- }
- },
- }
- ],
- )
- row_count = 300_000
- pl.DataFrame(
- {
- "group_key": [
- f"group-{index:040d}" for index in range(row_count)
- ],
- "amount": [1] * row_count,
- }
- ).write_parquet(tmp_path / "input.parquet")
- _assert_worker_resource_failure(plan, tmp_path)
- def test_join_peak_allocation_fails_inside_isolated_worker(tmp_path):
- input_schema = _schema(
- "bd:join:raw",
- [("id", "integer", False)],
- )
- lookup_schema = _schema(
- "bd:join:lookup",
- [("lookup_id", "integer", False), ("label", "string", False)],
- )
- output_schema = _schema(
- "bd:join:clean",
- [("id", "integer", False), ("label", "string", False)],
- )
- lookup_binding = _binding(
- lookup_schema,
- access_mode="read",
- object_ref="join-lookup",
- )
- plan = _compile_worker_plan(
- input_schema=input_schema,
- output_schema=output_schema,
- steps=[
- {
- "id": "join_label",
- "op": "lookup_join",
- "lookup": {
- "binding_id": lookup_binding["id"],
- "left_on": ["id"],
- "right_on": ["lookup_id"],
- "select": {"label": "label"},
- "how": "left",
- },
- }
- ],
- lookup_context={
- lookup_binding["id"]: {
- "binding": lookup_binding,
- "schema": lookup_schema,
- }
- },
- )
- row_count = 250_000
- pl.DataFrame({"id": range(row_count)}).write_parquet(
- tmp_path / "input.parquet"
- )
- lookup_path = tmp_path / "lookup.parquet"
- pl.DataFrame(
- {
- "lookup_id": range(row_count),
- "label": [f"label-{index:048d}" for index in range(row_count)],
- }
- ).write_parquet(lookup_path)
- _assert_worker_resource_failure(
- plan,
- tmp_path,
- lookup_paths={lookup_binding["id"]: str(lookup_path)},
- )
|