ericm-db opened a new pull request, #57102:
URL: https://github.com/apache/spark/pull/57102
### What changes were proposed in this pull request?
Adds a second opt-in fast path for local Spark Connect development,
complementing the
persistent-server reuse mode of #56907 (SPARK-57787): a pool of
**pre-warmed, single-use** local
Connect servers.
With `SPARK_LOCAL_CONNECT_POOL=1` (or `.config("spark.local.connect.pool",
"true")`),
`SparkSession.builder.remote("local[*]").getOrCreate()`:
1. **claims** a fresh, never-used server from the pool directory
(`~/.spark/connect-local-pool`,
override via `SPARK_LOCAL_CONNECT_POOL_DIR`) -- a claim is an atomic
rename of the server's
discovery file, so exactly one racing client wins;
2. asynchronously spawns replacements to keep
`spark.local.connect.pool.size` (default 2) members
warm;
3. tears the claimed server down asynchronously when the session stops or
the client exits --
**no server ever serves two runs**.
Mechanics:
- Every member binds an ephemeral port with its own auth token, is seeded
with the spawning
client's start-up confs, and is stamped with a conf fingerprint; a run
only claims members whose
fingerprint matches its own confs, so a warm JVM is never handed to a run
whose static confs
differ from what it was booted with.
- A best-effort `fcntl` lock serializes spawn *accounting* so concurrent
cold starters share one
complement of launches instead of each spawning their own; claims
themselves are lock-free
atomic renames. Platforms without `fcntl` may transiently over-spawn;
extras are reclaimed by
the idle timeout (same degradation posture as the reuse path's start lock).
- A janitor reaps stale state on every acquire: launches whose daemon died,
unreachable or
version-mismatched members, servers claimed by clients that died uncleanly
(e.g. SIGKILL), and
retired servers that hang in shutdown (SIGTERM at release, escalated to a
hard kill after 30s).
- Server-side backstops: pool members run with `--exit-after-use`
(self-terminate once their one
client's session has come and gone) and an idle timeout
(`SPARK_LOCAL_CONNECT_POOL_IDLE_TIMEOUT`, default 1800s), so an idle
machine drains to zero.
- `SparkSession._purge_local_connect_pool()` force-kills every member (warm,
in-flight, or
claimed) and empties the pool directory -- a one-command "really clean
state" escape hatch.
Relationship to the reuse mode (#56907): reuse keeps one long-lived server
that all runs
reconnect to -- cheapest, but state backed by the shared `SparkContext`
(persistent catalog,
global temp views, cached data) carries across runs and the JVM ages. The
pool trades standing
memory (~550 MB RSS per warm member measured on macOS with default confs;
the default pool of 2
is about 1.1 GB while actively iterating) for per-run isolation identical to
today's default
behavior. This directly addresses the determinism concern raised on the dev
list about reusing a
long-lived backend server:
https://lists.apache.org/thread/sg9o2gbb3nttz74f0s01v8f167zy8ltt
**Stacked on #56907**: the earlier commits belong to that PR; only the last
commit is new here.
Will rebase once #56907 merges.
### Why are the changes needed?
Local `getOrCreate()` pays a multi-second JVM + `SparkContext` cold start in
every process.
#56907 removes that cost by reusing one long-lived server, but reuse was
flagged on the dev list
as a determinism/isolation risk if it ever became a default: a shared
backend can be left in a
bad state that no fresh environment reproduces. The pool keeps the latency
win (measured on a dev
laptop: ~0.35-0.4s to a usable session vs ~3.2-4.6s cold) while preserving
fresh-server-per-run semantics, making it a candidate for an eventual
default-on story.
### Does this PR introduce _any_ user-facing change?
Only when the opt-in is enabled. With `SPARK_LOCAL_CONNECT_POOL=1` (or
`spark.local.connect.pool=true`), a `local` remote `getOrCreate()` claims a
pre-warmed, never-used
local Connect server and replaces it in the background, instead of booting
an in-process server.
With the opt-in unset (the default), behavior is unchanged. If both this and
`spark.local.connect.reuse` are set, the pool takes precedence.
### How was this patch tested?
New `python/pyspark/sql/tests/connect/test_connect_local_server_pool.py`:
- Unit tests (no real servers): claim atomicity, fingerprint matching, and
consumption of launch
bookkeeping; janitor rules (dead launches, unreachable members, claims
orphaned by SIGKILLed
clients, retired-server escalation, conf-seed cleanup) with live process
groups standing in for
servers; release writes the retired marker and stops the server; purge
kills all members and
empties the directory; pool lock roundtrip; pool-size parsing.
- End-to-end (real servers): sequential runs get distinct fresh servers and
each used server is
torn down afterwards; two concurrent cold clients get distinct servers.
Also verified manually on macOS: two simultaneous cold clients share one
spawn complement under
the pool lock; a global temp view and a cached table created in one run are
not visible to the
next (the state that leaks by design under the reuse mode); a SIGKILLed
client's orphaned server
is reaped by the next run's janitor while that run still connects in ~0.4s;
purge leaves zero
pool processes; idle RSS per warm member measured at 534-557 MB.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Fable 5)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]