gaogaotiantian opened a new pull request, #59120:
URL: https://github.com/apache/spark/pull/59120

   ### What changes were proposed in this pull request?
   
   This PR fixes a deadlock in the PySpark Spark Connect client's 
`StreamingQueryListenerBus`
   (`python/pyspark/sql/connect/streaming/query.py`) that can occur when the 
last streaming query
   listener is removed while listener events are still being delivered.
   
   The bus previously used a single lock (`_lock`) for two distinct 
responsibilities:
   
   - serializing listener lifecycle operations (`append` / `remove`), and
   - guarding `_listener_bus` during event delivery in `post_to_all()`.
   
   `remove()` held that lock across `self._execution_thread.join()`. When an 
event was in flight,
   the event-handling thread was blocked in `post_to_all()` trying to acquire 
the same lock, so it
   could never finish draining — while `remove()` waited forever for it via 
`join()`. That is a
   classic lock-ordering deadlock: the thread holding the lock waits for a 
thread that needs the
   lock to make progress.
   
   This PR splits the single lock into two:
   
   - `_thread_lock` — serializes `append` / `remove` end-to-end, held across 
the `join()`. This
     guarantees at most one event-handling thread exists at a time, so no 
second handler thread can
     be started while `remove()` is joining.
   - `_bus_lock` — guards `_listener_bus` and is acquired by the handler thread 
in `post_to_all()`.
     It is intentionally released before the `join()`, so the handler thread 
can keep draining and
     delivering remaining events and then exit, allowing the join to complete.
   
   Lock ordering is consistent (`_thread_lock` outer, `_bus_lock` inner) and 
the handler thread only
   ever acquires `_bus_lock`, so there is no circular wait. `remove()` also 
snapshots the thread
   reference before joining and guards the final bus removal, since the 
handler's error path may
   clear the bus concurrently.
   
   ### Why are the changes needed?
   
   Calling `spark.streams.removeListener(listener)` on the last remaining 
listener could hang
   indefinitely if a streaming query event was being delivered at the same 
time. This is a real
   user-facing hang, not merely a test problem; it has also been observed as an 
intermittent CI
   timeout in the `pyspark.sql.connect.dataframe` doctest, where the process 
had to be killed after
   the test timeout with a thread dump showing the main thread blocked in 
`remove()` -> `join()`
   and the handler thread blocked in `post_to_all()` -> `with self._lock`.
   
   ### Does this PR introduce _any_ user-facing change?
   
   No behavior change other than fixing the hang. `removeListener` still blocks 
until remaining
   events are processed, but no longer deadlocks.
   
   ### How was this patch tested?
   
   The affected path is exercised by existing Spark Connect streaming listener 
tests — notably the
   `pyspark.sql.connect.dataframe` doctest (which had been intermittently 
timing out on this path)
   and the streaming listener parity tests. The fix preserves the documented 
`removeListener`
   semantics (blocks until remaining events are processed) while removing the 
deadlock, so those
   existing tests continue to cover the behavior. Relying on CI to validate 
across the test matrix.
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Code
   


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