HyukjinKwon commented on PR #57990:
URL: https://github.com/apache/spark/pull/57990#issuecomment-5288761266

   **Code review — commit `7a87d3b`** (observe-backed accumulator). Reviewed 
the committed PR head only; a few local uncommitted edits to these files were 
in flight at review time and are not covered here. Three points, roughly in 
order of severity.
   
   ### 1. The classic JVM registry is process-global keyed by name — same-named 
accumulators collide across sessions
   
   `ObservedAccumulatorRegistry` is a singleton `object`, and its store is 
keyed by accumulator name alone (`sql/core/.../ObservedAccumulator.scala:49`, 
read via `registryValue(name)` at `:55`). The harvest listener is registered 
per session, but every session's listener bumps the *same* global 
`registry[name]`. So two sessions that each do `spark.accumulator("bad")` write 
into one shared slot, and `acc.value` returns the summed contributions of both 
— silently wrong.
   
   The `checkSession` guard (`sql/api/.../ObservedAccumulator.scala:146`) does 
**not** prevent this: it only rejects a read while a *different* session is 
active. Reading `accA.value` with session A active passes the guard and still 
returns A+B, and even purely sequential use (A runs, B runs, A reads) is 
contaminated. Named accumulators (`"bad"`, `"errors"`, …) are exactly the 
common case, and `newSession()` on one context hits it immediately.
   
   Note this diverges from the Connect path, which already scopes state to the 
session instance (`sql/connect/common/.../SparkSession.scala:105`, 
`observedAccumulatorRegistry` on the session). The classic side should do the 
same — key the registry by session identity (or hang it off the session like 
Connect does) rather than by bare name. The existing `enabledSessions` set 
already tracks per-session identity, so the plumbing is there. Worth a test 
with two same-named accumulators on `spark` and `spark.newSession()`.
   
   ### 2. `accumulator(...)` positional args differ between Scala and Python
   
   Scala: `def accumulator(name: String)` 
(`sql/api/.../SparkSession.scala:191`). Python: `def accumulator(self, zero=0, 
name=None, merge=None)` (`python/pyspark/sql/session.py:1075`). So 
`spark.accumulator("bad")` binds `name` in Scala but `zero` in Python — in 
Python it produces an auto-named accumulator whose zero is the string `"bad"`, 
which then blows up on the numeric path (`"bad" + float`) when you read 
`.value`. The Scala doc/examples and the Python examples use different first 
args, so a user porting the Scala snippet gets a silently broken accumulator. 
Since the description says the API shape is open, worth settling on one arg 
order across languages (e.g. `name` first in both, or `zero` first in both).
   
   ### 3. (Likely already in progress) Scala UDF inside a higher-order function 
isn't rejected
   
   `rejectAccumulatorInHigherOrderFunction` only matches `PythonUDF` markers 
(`sql/core/.../ObservedAccumulator.scala:251`), so a `ScalaUDF` capturing an 
accumulator inside a `transform`/`filter` lambda is neither rewritten nor 
rejected — it just returns 0, rather than failing fast as the description 
promises for HOFs. There's a Python test for the fail-fast but no Scala 
equivalent. (Flagging for completeness — the in-flight local changes to this 
file appear to already be adding `ScalaUDF` handling here.)
   
   <sub>Reviewed with Claude Code.</sub>
   


-- 
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]

Reply via email to