This is an automated email from the ASF dual-hosted git repository.
HyukjinKwon pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new cc76e900cabb [SPARK-56519][PYTHON] Isolate communication part from
python udf worker
cc76e900cabb is described below
commit cc76e900cabb2682edebc5f7c7d0f02e2feaad1b
Author: Tian Gao <[email protected]>
AuthorDate: Fri May 8 08:45:47 2026 +0900
[SPARK-56519][PYTHON] Isolate communication part from python udf worker
### What changes were proposed in this pull request?
We read everything we need for creating Python UDF worker from JVM at the
very beginning, so we don't need to read anything while we process. This
isolates the communication part from the processing logic.
A message interface is designed to hold all the information from JVM. For
now it does not change the protocol at all. It is designed to support multiple
protocols. The goal is to have a layer that contains all the information needed
in a language agnostic form (`int`, `str`, `bytes`, `None`, `list`, `dict`). In
the future, we can support a whole different protocol while keep supporting the
existing socket protocol - the new protocol just needs to fill in the blanks.
We still have some random workers that rely on `setup_spark_files` or
`setup_broadcasts`. So in this PR we made all these worker utils support both
the old and new input. If the argument is an IO, it uses the original code
path. If the function is fed the concrete data, it just uses it to initialize
stuff.
### Why are the changes needed?
The current protocol is super fragile because we mix the communication and
processing. No one knows where exactly the socket is going to be read. It's
also heavily based on the actual eval type which is horrible. We want to
isolate the communication part out and do read just once - read everything we
need then process. It will make the protocol much more robust and easier to
debug.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
`test_udf.py` and `test_udtf.py` passed locally. Still need to wait for CI
result.
### Was this patch authored or co-authored using generative AI tooling?
No.
Closes #55380 from gaogaotiantian/isolate-init-data.
Authored-by: Tian Gao <[email protected]>
Signed-off-by: Hyukjin Kwon <[email protected]>
(cherry picked from commit 0fc754675ed96b025b0a7f4953600b627925bdd3)
Signed-off-by: Hyukjin Kwon <[email protected]>
---
python/pyspark/taskcontext.py | 6 +-
python/pyspark/worker.py | 126 +++++++-----------
python/pyspark/worker_message.py | 272 +++++++++++++++++++++++++++++++++++++++
python/pyspark/worker_util.py | 145 +++++++++++++--------
4 files changed, 414 insertions(+), 135 deletions(-)
diff --git a/python/pyspark/taskcontext.py b/python/pyspark/taskcontext.py
index 8347e77cb94e..7ac645323243 100644
--- a/python/pyspark/taskcontext.py
+++ b/python/pyspark/taskcontext.py
@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-from typing import ClassVar, Type, TypeVar, Dict, List, Optional, Union, cast
+from typing import Any, ClassVar, Type, TypeVar, Dict, List, Optional, Union,
cast
from pyspark.util import local_connect_and_auth
from pyspark.serializers import read_int, write_int, write_with_length,
UTF8Deserializer
@@ -130,7 +130,7 @@ class TaskContext:
_cpus: Optional[int] = None
_resources: Optional[Dict[str, "ResourceInformation"]] = None
- def __new__(cls: Type["TaskContext"], **kwargs: Dict) -> "TaskContext":
+ def __new__(cls: Type["TaskContext"], **kwargs: Any) -> "TaskContext":
"""
Even if users construct :class:`TaskContext` instead of using get,
give them the singleton.
"""
@@ -142,7 +142,7 @@ class TaskContext:
def __init__(
self,
- **kwargs: Dict,
+ **kwargs: Any,
) -> None:
# Set attributes only if they are passed in and not None
# The kwargs are auto-mapped to the private attributes of TaskContext
diff --git a/python/pyspark/worker.py b/python/pyspark/worker.py
index a47e940f031f..b9f791ada1e6 100644
--- a/python/pyspark/worker.py
+++ b/python/pyspark/worker.py
@@ -57,8 +57,6 @@ from pyspark.taskcontext import BarrierTaskContext,
TaskContext
from pyspark.util import PythonEvalType
from pyspark.serializers import (
write_int,
- read_long,
- read_bool,
write_long,
read_int,
SpecialLengths,
@@ -109,6 +107,7 @@ from pyspark.util import (
)
from pyspark import _NoValue, shuffle
from pyspark.errors import PySparkRuntimeError, PySparkTypeError,
PySparkValueError
+from pyspark.worker_message import WorkerInitInfo
from pyspark.worker_util import (
check_python_version,
get_sock_file_to_executor,
@@ -118,7 +117,6 @@ from pyspark.worker_util import (
setup_broadcasts,
setup_memory_limits,
setup_spark_files,
- utf8_deserializer,
Conf,
)
from pyspark.logger.worker_io import capture_outputs
@@ -1017,29 +1015,15 @@ def wrap_memory_profiler(f, eval_type, result_id):
return profiling_func
-def read_single_udf(pickleSer, infile, eval_type, runner_conf, udf_index):
- num_arg = read_int(infile)
-
- args_offsets = []
- kwargs_offsets = {}
- for _ in range(num_arg):
- offset = read_int(infile)
- if read_bool(infile):
- name = utf8_deserializer.loads(infile)
- kwargs_offsets[name] = offset
- else:
- args_offsets.append(offset)
-
+def read_single_udf(pickleSer, udf_info, eval_type, runner_conf, udf_index):
chained_func = None
- for i in range(read_int(infile)):
- f, return_type = read_command(pickleSer, infile)
+ for udf in udf_info.udfs:
+ f, return_type = read_command(pickleSer, udf)
if chained_func is None:
chained_func = f
else:
chained_func = chain(chained_func, f)
- result_id = read_long(infile)
-
# If chained_func is from pyspark.sql.worker, it is to read/write data
source.
# In this case, we check the data_source_profiler config.
if getattr(chained_func, "__module__",
"").startswith("pyspark.sql.worker."):
@@ -1047,9 +1031,9 @@ def read_single_udf(pickleSer, infile, eval_type,
runner_conf, udf_index):
else:
profiler = runner_conf.udf_profiler
if profiler == "perf":
- profiling_func = wrap_perf_profiler(chained_func, eval_type, result_id)
+ profiling_func = wrap_perf_profiler(chained_func, eval_type,
udf_info.result_id)
elif profiler == "memory":
- profiling_func = wrap_memory_profiler(chained_func, eval_type,
result_id)
+ profiling_func = wrap_memory_profiler(chained_func, eval_type,
udf_info.result_id)
else:
profiling_func = chained_func
@@ -1063,6 +1047,8 @@ def read_single_udf(pickleSer, infile, eval_type,
runner_conf, udf_index):
# when they are processed in a for loop, raise them as RuntimeError's
instead
func = fail_on_stopiteration(profiling_func)
+ args_offsets, kwargs_offsets = udf_info.args, udf_info.kwargs
+
# the last returnType will be the return type of UDF
if eval_type == PythonEvalType.SQL_SCALAR_PANDAS_UDF:
return wrap_scalar_pandas_udf(func, args_offsets, kwargs_offsets,
return_type, runner_conf)
@@ -1141,7 +1127,7 @@ def read_single_udf(pickleSer, infile, eval_type,
runner_conf, udf_index):
# It expects the UDTF to be in a specific format and performs various checks to
# ensure the UDTF is valid. This function also prepares a mapper function for
applying
# the UDTF logic to input rows.
-def read_udtf(pickleSer, infile, eval_type, runner_conf, eval_conf):
+def read_udtf(pickleSer, udtf_info, eval_type, runner_conf, eval_conf):
if eval_type == PythonEvalType.SQL_ARROW_TABLE_UDF:
if runner_conf.use_legacy_pandas_udtf_conversion:
# NOTE: if timezone is set here, that implies
respectSessionTimeZone is True
@@ -1162,50 +1148,35 @@ def read_udtf(pickleSer, infile, eval_type,
runner_conf, eval_conf):
# Each row is a group so do not batch but send one by one.
ser = BatchedSerializer(CPickleSerializer(), 1)
- # See 'PythonUDTFRunner.PythonUDFWriterThread.writeCommand'
- num_arg = read_int(infile)
- args_offsets = []
- kwargs_offsets = {}
- for _ in range(num_arg):
- offset = read_int(infile)
- if read_bool(infile):
- name = utf8_deserializer.loads(infile)
- kwargs_offsets[name] = offset
- else:
- args_offsets.append(offset)
- num_partition_child_indexes = read_int(infile)
- partition_child_indexes = [read_int(infile) for i in
range(num_partition_child_indexes)]
- has_pickled_analyze_result = read_bool(infile)
- if has_pickled_analyze_result:
- pickled_analyze_result = pickleSer._read_with_length(infile)
+ if udtf_info.pickled_analyze_result is not None:
+ pickled_analyze_result =
pickleSer.loads(udtf_info.pickled_analyze_result)
else:
pickled_analyze_result = None
# Initially we assume that the UDTF __init__ method accepts the pickled
AnalyzeResult,
# although we may set this to false later if we find otherwise.
- handler = read_command(pickleSer, infile)
+ handler = read_command(pickleSer, udtf_info.handler)
if not isinstance(handler, type):
raise PySparkRuntimeError(
f"Invalid UDTF handler type. Expected a class (type 'type'), but "
f"got an instance of {type(handler).__name__}."
)
- return_type = _parse_datatype_json_string(utf8_deserializer.loads(infile))
+ return_type = _parse_datatype_json_string(udtf_info.return_type)
if not isinstance(return_type, StructType):
raise PySparkRuntimeError(
f"The return type of a UDTF must be a struct type, but got
{type(return_type)}."
)
- udtf_name = utf8_deserializer.loads(infile)
# Update the handler that creates a new UDTF instance to first try calling
the UDTF constructor
# with one argument containing the previous AnalyzeResult. If that fails,
then try a constructor
# with no arguments. In this way each UDTF class instance can decide if it
wants to inspect the
# AnalyzeResult.
udtf_init_args = inspect.getfullargspec(handler)
- if has_pickled_analyze_result:
+ if pickled_analyze_result is not None:
if len(udtf_init_args.args) > 2:
raise PySparkRuntimeError(
errorClass="UDTF_CONSTRUCTOR_INVALID_IMPLEMENTS_ANALYZE_METHOD",
- messageParameters={"name": udtf_name},
+ messageParameters={"name": udtf_info.name},
)
elif len(udtf_init_args.args) == 2:
prev_handler = handler
@@ -1218,7 +1189,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
elif len(udtf_init_args.args) > 1:
raise PySparkRuntimeError(
errorClass="UDTF_CONSTRUCTOR_INVALID_NO_ANALYZE_METHOD",
- messageParameters={"name": udtf_name},
+ messageParameters={"name": udtf_info.name},
)
class UDTFWithPartitions:
@@ -1256,7 +1227,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
self._create_udtf: Callable = create_udtf
self._udtf = create_udtf()
self._prev_arguments: list = list()
- self._partition_child_indexes: list = partition_child_indexes
+ self._partition_child_indexes: list =
udtf_info.partition_child_indexes
self._eval_raised_skip_rest_of_input_table: bool = False
def eval(self, *args, **kwargs) -> Iterator:
@@ -1650,13 +1621,13 @@ def read_udtf(pickleSer, infile, eval_type,
runner_conf, eval_conf):
# Instantiate the UDTF class.
try:
- if len(partition_child_indexes) > 0:
+ if len(udtf_info.partition_child_indexes) > 0:
# Determine if this is an Arrow UDTF
is_arrow_udtf = eval_type == PythonEvalType.SQL_ARROW_UDTF
if is_arrow_udtf:
- udtf = ArrowUDTFWithPartition(handler, partition_child_indexes)
+ udtf = ArrowUDTFWithPartition(handler,
udtf_info.partition_child_indexes)
else:
- udtf = UDTFWithPartitions(handler, partition_child_indexes)
+ udtf = UDTFWithPartitions(handler,
udtf_info.partition_child_indexes)
else:
udtf = handler()
except Exception as e:
@@ -1676,11 +1647,11 @@ def read_udtf(pickleSer, infile, eval_type,
runner_conf, eval_conf):
# Check that the arguments provided to the UDTF call match the expected
parameters defined
# in the 'eval' method signature.
try:
- inspect.signature(udtf.eval).bind(*args_offsets, **kwargs_offsets)
+ inspect.signature(udtf.eval).bind(*udtf_info.args, **udtf_info.kwargs)
except TypeError as e:
raise PySparkRuntimeError(
errorClass="UDTF_EVAL_METHOD_ARGUMENTS_DO_NOT_MATCH_SIGNATURE",
- messageParameters={"name": udtf_name, "reason": str(e)},
+ messageParameters={"name": udtf_info.name, "reason": str(e)},
) from None
def build_null_checker(return_type: StructType) ->
Optional[Callable[[Any], None]]:
@@ -1884,7 +1855,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
return evaluate
eval_func_kwargs_support, args_kwargs_offsets = wrap_kwargs_support(
- getattr(udtf, "eval"), args_offsets, kwargs_offsets
+ getattr(udtf, "eval"), udtf_info.args, udtf_info.kwargs
)
eval = wrap_arrow_udtf(eval_func_kwargs_support, return_type)
@@ -2046,7 +2017,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
return evaluate
eval_func_kwargs_support, args_kwargs_offsets = wrap_kwargs_support(
- getattr(udtf, "eval"), args_offsets, kwargs_offsets
+ getattr(udtf, "eval"), udtf_info.args, udtf_info.kwargs
)
eval = wrap_arrow_udtf(eval_func_kwargs_support, return_type)
@@ -2172,7 +2143,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
return evaluate
eval_func_kwargs_support, args_kwargs_offsets = wrap_kwargs_support(
- getattr(udtf, "eval"), args_offsets, kwargs_offsets
+ getattr(udtf, "eval"), udtf_info.args, udtf_info.kwargs
)
eval = wrap_pyarrow_udtf(eval_func_kwargs_support, return_type)
@@ -2274,7 +2245,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
return evaluate
eval_func_kwargs_support, args_kwargs_offsets = wrap_kwargs_support(
- getattr(udtf, "eval"), args_offsets, kwargs_offsets
+ getattr(udtf, "eval"), udtf_info.args, udtf_info.kwargs
)
eval = wrap_udtf(eval_func_kwargs_support, return_type)
@@ -2302,7 +2273,7 @@ def read_udtf(pickleSer, infile, eval_type, runner_conf,
eval_conf):
return mapper, None, ser, ser
-def read_udfs(pickleSer, infile, eval_type, runner_conf, eval_conf):
+def read_udfs(pickleSer, udf_info_list, eval_type, runner_conf, eval_conf):
if eval_type in (
PythonEvalType.SQL_ARROW_BATCHED_UDF,
PythonEvalType.SQL_SCALAR_PANDAS_UDF,
@@ -2436,13 +2407,13 @@ def read_udfs(pickleSer, infile, eval_type,
runner_conf, eval_conf):
batch_size = int(os.environ.get("PYTHON_UDF_BATCH_SIZE", "100"))
ser = BatchedSerializer(CPickleSerializer(), batch_size)
- # Read all UDFs
- num_udfs = read_int(infile)
udfs = [
- read_single_udf(pickleSer, infile, eval_type, runner_conf, udf_index=i)
- for i in range(num_udfs)
+ read_single_udf(pickleSer, udf_info, eval_type, runner_conf,
udf_index=udf_index)
+ for udf_index, udf_info in enumerate(udf_info_list)
]
+ num_udfs = len(udfs)
+
def extract_key_value_indexes(grouped_arg_offsets):
"""
Helper function to extract the key and value indexes from arg_offsets
for the grouped and
@@ -3555,52 +3526,49 @@ def read_udfs(pickleSer, infile, eval_type,
runner_conf, eval_conf):
def main(infile, outfile):
try:
boot_time = time.time()
- split_index = read_int(infile)
- if split_index == -1: # for unit tests
- sys.exit(-1)
+ init_info = WorkerInitInfo.from_stream(infile)
start_faulthandler_periodic_traceback()
- check_python_version(infile)
+ check_python_version(init_info.python_version)
memory_limit_mb = int(os.environ.get("PYSPARK_EXECUTOR_MEMORY_MB",
"-1"))
setup_memory_limits(memory_limit_mb)
- task_context_json = json.loads(utf8_deserializer.loads(infile))
- if task_context_json["isBarrier"]:
- taskContext = BarrierTaskContext.from_json(task_context_json)
- else:
- taskContext = TaskContext.from_json(task_context_json)
- TaskContext._setTaskContext(taskContext)
+ TaskContext._setTaskContext(init_info.task_context.to_task_context())
shuffle.MemoryBytesSpilled = 0
shuffle.DiskBytesSpilled = 0
- setup_spark_files(infile)
- setup_broadcasts(infile)
+ setup_spark_files(init_info.spark_files_dir, init_info.python_includes)
+ setup_broadcasts(
+ init_info.broadcast.variables,
+ init_info.broadcast.conn_info,
+ init_info.broadcast.auth_secret,
+ )
_accumulatorRegistry.clear()
- eval_type = read_int(infile)
- runner_conf = RunnerConf(infile)
- eval_conf = EvalConf(infile)
+ eval_type = init_info.eval_type
+ runner_conf = RunnerConf(init_info.runner_conf)
+ eval_conf = EvalConf(init_info.eval_conf)
if eval_type == PythonEvalType.NON_UDF:
- func, profiler, deserializer, serializer = read_command(pickleSer,
infile)
+ func, profiler, deserializer, serializer = read_command(pickleSer,
init_info.udf_info)
elif eval_type in (
PythonEvalType.SQL_TABLE_UDF,
PythonEvalType.SQL_ARROW_TABLE_UDF,
PythonEvalType.SQL_ARROW_UDTF,
):
func, profiler, deserializer, serializer = read_udtf(
- pickleSer, infile, eval_type, runner_conf, eval_conf
+ pickleSer, init_info.udf_info, eval_type, runner_conf,
eval_conf
)
else:
func, profiler, deserializer, serializer = read_udfs(
- pickleSer, infile, eval_type, runner_conf, eval_conf
+ pickleSer, init_info.udf_info, eval_type, runner_conf,
eval_conf
)
init_time = time.time()
def process():
iterator = deserializer.load_stream(infile)
- out_iter = func(split_index, iterator)
+ out_iter = func(init_info.split_index, iterator)
try:
serializer.dump_stream(out_iter, outfile)
finally:
diff --git a/python/pyspark/worker_message.py b/python/pyspark/worker_message.py
new file mode 100644
index 000000000000..b1519cb08442
--- /dev/null
+++ b/python/pyspark/worker_message.py
@@ -0,0 +1,272 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import dataclasses
+import json
+import sys
+from typing import Optional, Union, IO
+
+from pyspark.errors import PySparkValueError
+from pyspark.serializers import read_bool, read_int, read_long, SpecialLengths
+from pyspark.taskcontext import BarrierTaskContext, ResourceInformation,
TaskContext
+from pyspark.util import PythonEvalType
+from pyspark.worker_util import utf8_deserializer
+
+
[email protected]
+class TaskContextInfo:
+ @dataclasses.dataclass
+ class ResourceInfo:
+ name: str
+ addresses: list[str]
+
+ is_barrier: bool
+ conn_info: Optional[Union[str, int]]
+ secret: Optional[str]
+ stage_id: int
+ partition_id: int
+ attempt_number: int
+ task_attempt_id: int
+ cpus: int
+ resources: dict[str, ResourceInfo]
+ local_properties: dict[str, str]
+
+ @classmethod
+ def from_stream(cls, stream: IO) -> "TaskContextInfo":
+ task_context_json = json.loads(utf8_deserializer.loads(stream))
+ return cls(
+ is_barrier=task_context_json["isBarrier"],
+ conn_info=task_context_json["connInfo"],
+ secret=task_context_json["secret"],
+ stage_id=task_context_json["stageId"],
+ partition_id=task_context_json["partitionId"],
+ attempt_number=task_context_json["attemptNumber"],
+ task_attempt_id=task_context_json["taskAttemptId"],
+ cpus=task_context_json["cpus"],
+ resources={
+ k: cls.ResourceInfo(name=v["name"], addresses=v["addresses"])
+ for k, v in task_context_json["resources"].items()
+ },
+ local_properties=task_context_json["localProperties"],
+ )
+
+ def to_task_context(self) -> TaskContext:
+ if self.is_barrier:
+ return BarrierTaskContext(
+ conn_info=self.conn_info,
+ secret=self.secret,
+ stageId=self.stage_id,
+ partitionId=self.partition_id,
+ attemptNumber=self.attempt_number,
+ taskAttemptId=self.task_attempt_id,
+ cpus=self.cpus,
+ resources={
+ k: ResourceInformation(v.name, v.addresses) for k, v in
self.resources.items()
+ },
+ localProperties=self.local_properties,
+ )
+ else:
+ return TaskContext(
+ stageId=self.stage_id,
+ partitionId=self.partition_id,
+ attemptNumber=self.attempt_number,
+ taskAttemptId=self.task_attempt_id,
+ cpus=self.cpus,
+ resources={
+ k: ResourceInformation(v.name, v.addresses) for k, v in
self.resources.items()
+ },
+ localProperties=self.local_properties,
+ )
+
+
[email protected]
+class BroadcastInfo:
+ conn_info: Optional[Union[str, int]]
+ auth_secret: Optional[str]
+ variables: list[tuple[int, Optional[str]]]
+
+ @classmethod
+ def from_stream(cls, stream: IO) -> "BroadcastInfo":
+ needs_broadcast_decryption_server = read_bool(stream)
+ num_broadcast_variables = read_int(stream)
+ conn_info = None
+ auth_secret = None
+ if needs_broadcast_decryption_server:
+ conn_info = read_int(stream)
+ if conn_info == -1:
+ conn_info = utf8_deserializer.loads(stream)
+ else:
+ auth_secret = utf8_deserializer.loads(stream)
+
+ variables = []
+ for _ in range(num_broadcast_variables):
+ bid = read_long(stream)
+ path = None
+ if bid >= 0 and not needs_broadcast_decryption_server:
+ path = utf8_deserializer.loads(stream)
+ variables.append((bid, path))
+
+ return cls(conn_info=conn_info, auth_secret=auth_secret,
variables=variables)
+
+
[email protected]
+class UDFInfo:
+ udfs: list[bytes]
+ args: list[int]
+ kwargs: dict[str, int]
+ result_id: int
+
+ @classmethod
+ def from_stream(cls, stream: IO) -> "UDFInfo":
+ num_args = read_int(stream)
+ udfs = []
+ args = []
+ kwargs = {}
+
+ for _ in range(num_args):
+ offset = read_int(stream)
+ if read_bool(stream):
+ name = utf8_deserializer.loads(stream)
+ kwargs[name] = offset
+ else:
+ args.append(offset)
+
+ for i in range(read_int(stream)):
+ length = read_int(stream)
+ if length == SpecialLengths.END_OF_DATA_SECTION:
+ raise EOFError
+ elif length == SpecialLengths.NULL:
+ raise PySparkValueError("Unexpected NULL value for UDF")
+ else:
+ data = stream.read(length)
+ if len(data) < length:
+ raise EOFError
+ udfs.append(data)
+
+ result_id = read_long(stream)
+
+ return cls(udfs=udfs, args=args, kwargs=kwargs, result_id=result_id)
+
+
[email protected]
+class UDTFInfo:
+ args: list[int]
+ kwargs: dict[str, int]
+ partition_child_indexes: list[int]
+ pickled_analyze_result: Optional[bytes]
+ handler: bytes
+ return_type: str
+ name: str
+
+ @classmethod
+ def from_stream(cls, stream: IO) -> "UDTFInfo":
+ # See 'PythonUDTFRunner.PythonUDFWriterThread.writeCommand'
+ args = []
+ kwargs = {}
+ for _ in range(read_int(stream)):
+ offset = read_int(stream)
+ if read_bool(stream):
+ name = utf8_deserializer.loads(stream)
+ kwargs[name] = offset
+ else:
+ args.append(offset)
+ partition_child_indexes = [read_int(stream) for _ in
range(read_int(stream))]
+ if read_bool(stream):
+ pickled_analyze_result = stream.read(read_int(stream))
+ else:
+ pickled_analyze_result = None
+ handler = stream.read(read_int(stream))
+ return_type = utf8_deserializer.loads(stream)
+ name = utf8_deserializer.loads(stream)
+
+ return cls(
+ args=args,
+ kwargs=kwargs,
+ partition_child_indexes=partition_child_indexes,
+ pickled_analyze_result=pickled_analyze_result,
+ handler=handler,
+ return_type=return_type,
+ name=name,
+ )
+
+
[email protected]
+class WorkerInitInfo:
+ split_index: int
+ python_version: str
+ spark_files_dir: str
+ task_context: TaskContextInfo
+ python_includes: list[str]
+ broadcast: BroadcastInfo
+ eval_type: int
+ runner_conf: dict[str, str]
+ eval_conf: dict[str, str]
+ udf_info: Union[bytes, UDTFInfo, list[UDFInfo]]
+
+ @classmethod
+ def from_stream(cls, stream: IO) -> "WorkerInitInfo":
+ split_index = read_int(stream)
+ if split_index == -1:
+ sys.exit(-1)
+ python_version = utf8_deserializer.loads(stream)
+ task_context = TaskContextInfo.from_stream(stream)
+
+ spark_files_dir = utf8_deserializer.loads(stream)
+ python_includes = []
+ for _ in range(read_int(stream)):
+ python_includes.append(utf8_deserializer.loads(stream))
+
+ broadcast = BroadcastInfo.from_stream(stream)
+ eval_type = read_int(stream)
+ runner_conf = {}
+ for _ in range(read_int(stream)):
+ k = utf8_deserializer.loads(stream)
+ v = utf8_deserializer.loads(stream)
+ runner_conf[k] = v
+ eval_conf = {}
+ for _ in range(read_int(stream)):
+ k = utf8_deserializer.loads(stream)
+ v = utf8_deserializer.loads(stream)
+ eval_conf[k] = v
+
+ udf_info: Union[bytes, UDTFInfo, list[UDFInfo]]
+
+ if eval_type == PythonEvalType.NON_UDF:
+ udf_info = stream.read(read_int(stream))
+ elif eval_type in (
+ PythonEvalType.SQL_TABLE_UDF,
+ PythonEvalType.SQL_ARROW_TABLE_UDF,
+ PythonEvalType.SQL_ARROW_UDTF,
+ ):
+ udf_info = UDTFInfo.from_stream(stream)
+ else:
+ udf_info = []
+ for _ in range(read_int(stream)):
+ udf_info.append(UDFInfo.from_stream(stream))
+
+ return cls(
+ split_index=split_index,
+ python_version=python_version,
+ spark_files_dir=spark_files_dir,
+ task_context=task_context,
+ python_includes=python_includes,
+ broadcast=broadcast,
+ eval_type=eval_type,
+ runner_conf=runner_conf,
+ eval_conf=eval_conf,
+ udf_info=udf_info,
+ )
diff --git a/python/pyspark/worker_util.py b/python/pyspark/worker_util.py
index 8c52bde6ea6c..08edf0c5decb 100644
--- a/python/pyspark/worker_util.py
+++ b/python/pyspark/worker_util.py
@@ -24,7 +24,7 @@ import importlib
from inspect import currentframe, getframeinfo
import os
import sys
-from typing import Any, Generator, IO, Optional
+from typing import Any, Generator, IO, Optional, Union, overload
import warnings
if "SPARK_TESTING" in os.environ:
@@ -44,7 +44,6 @@ from pyspark.util import is_remote_only
from pyspark.errors import PySparkRuntimeError
from pyspark.util import local_connect_and_auth
from pyspark.serializers import (
- read_bool,
read_int,
read_long,
write_int,
@@ -66,21 +65,27 @@ def add_path(path: str) -> bool:
return False
-def read_command(serializer: FramedSerializer, file: IO) -> Any:
+def read_command(serializer: FramedSerializer, file: Union[IO, bytes]) -> Any:
if not is_remote_only():
from pyspark.core.broadcast import Broadcast
- command = serializer._read_with_length(file)
+ if isinstance(file, bytes):
+ command = serializer.loads(file)
+ else:
+ command = serializer._read_with_length(file)
if not is_remote_only() and isinstance(command, Broadcast):
command = serializer.loads(command.value)
return command
-def check_python_version(infile: IO) -> None:
+def check_python_version(infile_or_version: Union[IO, str]) -> None:
"""
Check the Python version between the running process and the one used to
serialize the command.
"""
- version = utf8_deserializer.loads(infile)
+ if isinstance(infile_or_version, str):
+ version = infile_or_version
+ else:
+ version = utf8_deserializer.loads(infile_or_version)
worker_version = "%d.%d" % sys.version_info[:2]
if version != worker_version:
raise PySparkRuntimeError(
@@ -130,12 +135,20 @@ def setup_memory_limits(memory_limit_mb: int) -> None:
)
-def setup_spark_files(infile: IO) -> None:
+@overload
+def setup_spark_files(infile_or_spark_files_dir: IO) -> None: ...
+@overload
+def setup_spark_files(infile_or_spark_files_dir: str, python_includes:
list[str]) -> None: ...
+def setup_spark_files(
+ infile_or_spark_files_dir: Union[IO, str], python_includes:
Optional[list[str]] = None
+) -> None:
"""
Set up Spark files, archives, and pyfiles.
"""
- # fetch name of workdir
- spark_files_dir = utf8_deserializer.loads(infile)
+ if isinstance(infile_or_spark_files_dir, str):
+ spark_files_dir = infile_or_spark_files_dir
+ else:
+ spark_files_dir = utf8_deserializer.loads(infile_or_spark_files_dir)
if not is_remote_only():
from pyspark.core.files import SparkFiles
@@ -145,51 +158,74 @@ def setup_spark_files(infile: IO) -> None:
# fetch names of includes (*.zip and *.egg files) and construct PYTHONPATH
path_changed = add_path(spark_files_dir) # *.py files that were added
will be copied here
- num_python_includes = read_int(infile)
- for _ in range(num_python_includes):
- filename = utf8_deserializer.loads(infile)
+ if not isinstance(infile_or_spark_files_dir, str):
+ python_includes = [
+ utf8_deserializer.loads(infile_or_spark_files_dir)
+ for _ in range(read_int(infile_or_spark_files_dir))
+ ]
+ assert python_includes is not None
+
+ for filename in python_includes:
path_changed = add_path(os.path.join(spark_files_dir, filename)) or
path_changed
if path_changed:
importlib.invalidate_caches()
-def setup_broadcasts(infile: IO) -> None:
+@overload
+def setup_broadcasts(infile_or_variables: IO) -> None: ...
+@overload
+def setup_broadcasts(
+ infile_or_variables: list[tuple[int, Union[str, None]]], conn_info: str,
auth_secret: None
+) -> None: ...
+@overload
+def setup_broadcasts(
+ infile_or_variables: list[tuple[int, Union[str, None]]], conn_info: int,
auth_secret: str
+) -> None: ...
+@overload
+def setup_broadcasts(
+ infile_or_variables: list[tuple[int, Union[str, None]]], conn_info: None,
auth_secret: None
+) -> None: ...
+def setup_broadcasts(
+ infile_or_variables: Union[IO, list[tuple[int, Union[str, None]]]],
+ conn_info: Optional[Union[str, int]] = None,
+ auth_secret: Optional[str] = None,
+) -> None:
"""
Set up broadcasted variables.
"""
if not is_remote_only():
from pyspark.core.broadcast import Broadcast, _broadcastRegistry
- # fetch names and values of broadcast variables
- needs_broadcast_decryption_server = read_bool(infile)
- num_broadcast_variables = read_int(infile)
+ if isinstance(infile_or_variables, list):
+ variables = infile_or_variables
+ else:
+ from pyspark.worker_message import BroadcastInfo
+
+ broadcast_info = BroadcastInfo.from_stream(infile_or_variables)
+ conn_info = broadcast_info.conn_info
+ auth_secret = broadcast_info.auth_secret
+ variables = broadcast_info.variables
+
+ needs_broadcast_decryption_server = conn_info is not None or auth_secret
is not None
+
if needs_broadcast_decryption_server:
- # read the decrypted data from a server in the jvm
- conn_info = read_int(infile)
- auth_secret = None
- if conn_info == -1:
- conn_info = utf8_deserializer.loads(infile)
- else:
- auth_secret = utf8_deserializer.loads(infile)
broadcast_sock_file, _ = local_connect_and_auth(conn_info, auth_secret)
+ else:
+ broadcast_sock_file = None
- for _ in range(num_broadcast_variables):
- bid = read_long(infile)
+ for bid, path in variables:
if bid >= 0:
- if needs_broadcast_decryption_server:
+ if path is None:
read_bid = read_long(broadcast_sock_file)
assert read_bid == bid
_broadcastRegistry[bid] =
Broadcast(sock_file=broadcast_sock_file)
else:
- path = utf8_deserializer.loads(infile)
_broadcastRegistry[bid] = Broadcast(path=path)
-
else:
- bid = -bid - 1
- _broadcastRegistry.pop(bid)
+ _broadcastRegistry.pop(-bid - 1)
- if needs_broadcast_decryption_server:
+ if broadcast_sock_file is not None:
broadcast_sock_file.write(b"1")
broadcast_sock_file.close()
@@ -223,29 +259,32 @@ def send_accumulator_updates(outfile: IO) -> None:
class Conf:
- def __init__(self, infile: Optional[IO] = None) -> None:
+ def __init__(self, infile_or_dict: Optional[Union[dict[str, str], IO]] =
None) -> None:
self._conf: dict[str, Any] = {}
- if infile is not None:
- self.load(infile)
-
- def load(self, infile: IO) -> None:
- num_conf = read_int(infile)
- # We do a sanity check here to reduce the possibility to stuck
indefinitely
- # due to an invalid messsage. If the numer of configurations is
obviously
- # wrong, we just raise an error directly.
- # We hand-pick the configurations to send to the worker so the number
should
- # be very small (less than 100).
- if num_conf < 0 or num_conf > 10000:
- raise PySparkRuntimeError(
- errorClass="PROTOCOL_ERROR",
- messageParameters={
- "failure": f"Invalid number of configurations: {num_conf}",
- },
- )
- for _ in range(num_conf):
- k = utf8_deserializer.loads(infile)
- v = utf8_deserializer.loads(infile)
- self._conf[k] = v
+ if infile_or_dict is not None:
+ self.load(infile_or_dict)
+
+ def load(self, infile_or_dict: Union[dict[str, str], IO]) -> None:
+ if isinstance(infile_or_dict, dict):
+ self._conf = infile_or_dict
+ else:
+ num_conf = read_int(infile_or_dict)
+ # We do a sanity check here to reduce the possibility to stuck
indefinitely
+ # due to an invalid messsage. If the numer of configurations is
obviously
+ # wrong, we just raise an error directly.
+ # We hand-pick the configurations to send to the worker so the
number should
+ # be very small (less than 100).
+ if num_conf < 0 or num_conf > 10000:
+ raise PySparkRuntimeError(
+ errorClass="PROTOCOL_ERROR",
+ messageParameters={
+ "failure": f"Invalid number of configurations:
{num_conf}",
+ },
+ )
+ for _ in range(num_conf):
+ k = utf8_deserializer.loads(infile_or_dict)
+ v = utf8_deserializer.loads(infile_or_dict)
+ self._conf[k] = v
def get(self, key: str, default: Any = "", *, lower_str: bool = True) ->
Any:
val = self._conf.get(key, default)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]