4 Commits
Author SHA1 Message Date
ClaudeandClaude deafb5a26b refactor: drop fetch_url body cap and modernize datetime usage
- fetch_url: remove the 1MB body cap and the streaming helper. The
  manual redirect loop with allowlist re-check (the SSRF fix) is kept
  intact; the body is now read in full via resp.text. Drop the now-
  meaningless `truncated` field from FetchUrlResult and the tests that
  asserted on it. Switched from httpx.stream() back to httpx.get()
  for the redirect loop — cleaner without the body cap.

- datetime: replace deprecated datetime.utcnow() with
  datetime.now(timezone.utc) in submit.py (3 sites) and
  core/job_writer.py (1 site). Update the stale comment in
  core/pending_store.py that referenced the old call.

- Clean up an unused `from common.config import settings` import in
  submit.py that ruff flagged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-09 14:35:16 +08:00
ClaudeandClaude Fable 5 f089793218 feat(job_writer): reject code that is not valid UTF-8
write_job_file now validates that the incoming code string is encodable
as UTF-8 BEFORE doing any file I/O. A lone surrogate (U+D800..U+DFFF) —
typically the result of a broken decode somewhere upstream in the
agent's pipeline — cannot be represented in UTF-8, and would otherwise
either silently produce mojibake on disk or crash with an opaque
UnicodeEncodeError from open().

Behavior:
  - try: code.encode('utf-8')
  - on UnicodeEncodeError: log a warning and raise ValueError with a
    message that points the agent at 're-generate the code as plain
    Unicode'. ValueError -> HTTP 400 via the existing server.py
    exception handler, so the agent gets a clean error and no file is
    written.
  - normal CJK / emoji / accented Latin still work — the encode check
    is essentially free.

Tests:
  - test_write_accepts_valid_utf8_including_cjk_and_emoji: positive
    case for the common LLM output patterns.
  - test_write_rejects_lone_surrogate: negative case, also asserts
    that tmp_path is empty (no half-written .py left behind).

Full suite 246 passed (+2 from 244).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-30 14:33:53 +08:00
Claude f8e367d43c refactor: unify env-var config in common/config.py
Before: SPARK_EXECUTOR_DATA_DIR, SPARK_EXECUTOR_JOBS_DIR, and
YARN_RESOURCE_MANAGER_URL were each read directly via os.environ.get()
inside the module that used them. Log level was hardcoded DEBUG in
common/logging.py. There was no single file showing what the full set
of env vars the app reads is.

After: common/config.py defines a single Settings dataclass that
reads all env vars at import time and exposes them as fields on a
module-level singleton. App code uses "from common.config import
settings; settings.data_dir" etc. New SPARK_EXECUTOR_LOG_LEVEL env
var controls stderr + info file verbosity (debug file always gets full
DEBUG).

Improvements:
  - One file lists every env var the app reads (was: grep the codebase)
  - Tests can monkeypatch fields on the settings singleton directly
    instead of monkeypatching the env + reloading
  - Adding a new env var means adding one field in config.py, not
    editing 3+ call sites
  - settings.reload() method for tests that prefer env-var style

Out of scope (kept where they are):
  - GUNICORN_* env vars live in gunicorn.conf.py (gunicorn concept)
  - PYTHONUNBUFFERED in Dockerfile (Python runtime flag)
  - SPARK_SUBMIT_OPTS not in config (JVM flag, not Python)

Test changes:
  - test_job_writer.py: settings.jobs_dir instead of monkeypatching
    SPARK_EXECUTOR_JOBS_DIR
  - test_yarn_client.py: settings.yarn_resource_manager_url instead of
    monkeypatching YARN_RESOURCE_MANAGER_URL
  - test_generate_tool.py: same as job_writer
  - Each test file gets an autouse fixture that snapshots+restores
    settings so one test mutation does not leak into the next

116/116 still pass. Live verified: SPARK_EXECUTOR_LOG_LEVEL=INFO
suppresses DEBUG loguru output as expected.
2026-06-25 10:52:53 +08:00
Claude 9340061b30 feat(job_writer): write PySpark code to disk with env-var override
spark_executor/core/job_writer.py provides write_job_file(code, jobs_dir=None)
that resolves the output directory in this order:
  1. explicit jobs_dir argument (for tests + programmatic override)
  2. SPARK_EXECUTOR_JOBS_DIR environment variable (operator override)
  3. DEFAULT_JOBS_DIR = './data/jobs' (gitignored, persists via volume mount)

Filenames are job_<UTC-stamp>_<random-hex>.py — same-second writes still
get distinct names via the random suffix, so concurrent agents won't
clobber each other.

8 tests cover: arg>env>default precedence, dir auto-creation, env-var
override, default fallback in a tmp cwd, and uniqueness under concurrent
calls.
2026-06-25 10:17:12 +08:00