blob: ab761d911d144a525ece8fa0cb67351790ea46c1 [file]
#
# 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 faulthandler
import gc
import os
import select
import signal
import socket
import sys
import time
import traceback
import uuid
from errno import EAGAIN, EINTR
from signal import SIG_DFL, SIG_IGN, SIGCHLD, SIGHUP, SIGINT, SIGTERM
from socket import AF_INET, AF_INET6, SOCK_STREAM, SOMAXCONN
from types import FrameType
from typing import Any, Optional
from pyspark.errors import PySparkRuntimeError
from pyspark.serializers import UTF8Deserializer, read_int, write_int, write_with_length
from pyspark.util import enable_faulthandler
def compute_real_exit_code(exit_code: Any) -> int:
# SystemExit's code can be anything, but os._exit only accepts integer
if isinstance(exit_code, int):
return exit_code
else:
return 1
def worker(sock: socket.socket, authenticated: bool) -> int:
"""
Called by a worker process after the fork().
"""
signal.signal(SIGHUP, SIG_DFL)
signal.signal(SIGCHLD, SIG_DFL)
signal.signal(SIGTERM, SIG_DFL)
# restore the handler for SIGINT,
# it's useful for debugging (show the stacktrace before exit)
signal.signal(SIGINT, signal.default_int_handler)
# Read the socket using fdopen instead of socket.makefile() because the latter
# seems to be very slow; note that we need to dup() the file descriptor because
# otherwise writes also cause a seek that makes us miss data on the read side.
buffer_size = int(os.environ.get("SPARK_BUFFER_SIZE", 65536))
infile = os.fdopen(os.dup(sock.fileno()), "rb", buffer_size)
outfile = os.fdopen(os.dup(sock.fileno()), "wb", buffer_size)
if not authenticated:
client_secret = UTF8Deserializer().loads(infile)
if os.environ["PYTHON_WORKER_FACTORY_SECRET"] == client_secret:
write_with_length("ok".encode("utf-8"), outfile)
outfile.flush()
else:
write_with_length("err".encode("utf-8"), outfile)
outfile.flush()
sock.close()
return 1
exit_code = 0
# We don't know what could happen when we import the worker module. We have to
# guarantee that no thread is spawned before we fork, so we have to import the
# worker module after fork. For example, both pandas and pyarrow starts some
# threads when they are imported.
if len(sys.argv) > 1 and sys.argv[1].startswith("pyspark"):
import importlib
worker_module = importlib.import_module(sys.argv[1])
worker_main = worker_module.main
else:
from pyspark.worker import main as worker_main
try:
worker_main(infile, outfile)
except SystemExit as exc:
exit_code = compute_real_exit_code(exc.code)
finally:
try:
outfile.flush()
except Exception:
if os.environ.get("PYTHON_DAEMON_KILL_WORKER_ON_FLUSH_FAILURE", False):
faulthandler_log_path = os.environ.get("PYTHON_FAULTHANDLER_DIR", None)
if faulthandler_log_path:
faulthandler_log_path = os.path.join(faulthandler_log_path, str(os.getpid()))
with open(
faulthandler_log_path, "w", encoding="utf-8"
) as faulthandler_log_file:
faulthandler.dump_traceback(file=faulthandler_log_file)
raise
else:
print(
"PySpark daemon failed to flush the output to the worker process:\n"
+ traceback.format_exc(),
file=sys.stderr,
)
return exit_code
def manager() -> None:
# Create a new process group to corral our children
os.setpgid(0, 0)
is_unix_domain_sock = os.environ.get("PYTHON_UNIX_DOMAIN_ENABLED", "false").lower() == "true"
socket_path = None
# Create a listening socket on the loopback interface
if is_unix_domain_sock:
assert "PYTHON_WORKER_FACTORY_SOCK_DIR" in os.environ
socket_path = os.path.join(
os.environ["PYTHON_WORKER_FACTORY_SOCK_DIR"], f".{uuid.uuid4()}.sock"
)
listen_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
listen_sock.bind(socket_path)
listen_sock.listen(max(1024, SOMAXCONN))
listen_port = socket_path
elif os.environ.get("SPARK_PREFER_IPV6", "false").lower() == "true":
listen_sock = socket.socket(AF_INET6, SOCK_STREAM)
listen_sock.bind(("::1", 0, 0, 0))
listen_sock.listen(max(1024, SOMAXCONN))
listen_host, listen_port, _, _ = listen_sock.getsockname()
else:
listen_sock = socket.socket(AF_INET, SOCK_STREAM)
listen_sock.bind(("127.0.0.1", 0))
listen_sock.listen(max(1024, SOMAXCONN))
listen_host, listen_port = listen_sock.getsockname()
# re-open stdin/stdout in 'wb' mode
stdin_bin = os.fdopen(sys.stdin.fileno(), "rb", 4)
stdout_bin = os.fdopen(sys.stdout.fileno(), "wb", 4)
if is_unix_domain_sock:
write_with_length(listen_port.encode("utf-8"), stdout_bin)
else:
write_int(listen_port, stdout_bin)
stdout_bin.flush()
def shutdown(code: int) -> None:
if socket_path is not None and os.path.exists(socket_path):
os.remove(socket_path)
signal.signal(SIGTERM, SIG_DFL)
# Send SIGHUP to notify workers of shutdown
os.kill(0, SIGHUP)
sys.exit(code)
def handle_sigterm(signal_number: int, frame: Optional[FrameType]) -> None:
shutdown(1)
signal.signal(SIGTERM, handle_sigterm) # Gracefully exit on SIGTERM
signal.signal(SIGHUP, SIG_IGN) # Don't die on SIGHUP
signal.signal(SIGCHLD, SIG_IGN)
reuse = os.environ.get("SPARK_REUSE_WORKER")
# Initialization complete
try:
poller = None
if os.name == "posix":
# select.select has a known limit on the number of file descriptors
# it can handle. We use select.poll instead to avoid this limit.
poller = select.poll()
fd_reverse_map = {0: 0, listen_sock.fileno(): listen_sock}
poller.register(0, select.POLLIN)
poller.register(listen_sock, select.POLLIN)
while True:
if poller is not None:
ready_fds = []
# Unlike select, poll timeout is in millis.
for fd, event in poller.poll(1000):
if event & (select.POLLIN | select.POLLHUP):
# Data can be read (for POLLHUP peer hang up, so reads will return
# 0 bytes, in which case we want to break out - this is consistent
# with how select behaves).
ready_fds.append(fd_reverse_map[fd])
else:
# Could be POLLERR or POLLNVAL (select would raise in this case).
raise PySparkRuntimeError(f"Polling error - event {event} on fd {fd}")
else:
# If poll is not available, use select.
ready_fds = select.select([0, listen_sock], [], [], 1)[0]
if 0 in ready_fds:
try:
worker_pid = read_int(stdin_bin)
except EOFError:
# Spark told us to exit by closing stdin
shutdown(0)
try:
os.kill(worker_pid, signal.SIGKILL)
except OSError:
pass # process already died
if listen_sock in ready_fds:
try:
sock, _ = listen_sock.accept()
except OSError as e:
if e.errno == EINTR:
continue
raise
# Launch a worker process
try:
pid = os.fork()
except OSError as e:
if e.errno in (EAGAIN, EINTR):
time.sleep(1)
pid = os.fork() # error here will shutdown daemon
else:
outfile = sock.makefile(mode="wb")
write_int(e.errno, outfile) # Signal that the fork failed
outfile.flush()
outfile.close()
sock.close()
continue
if pid == 0:
# in child process
with enable_faulthandler():
if poller is not None:
poller.unregister(0)
poller.unregister(listen_sock)
listen_sock.close()
# It should close the standard input in the child process so that
# Python native function executions stay intact.
#
# Note that if we just close the standard input (file descriptor 0),
# the lowest file descriptor (file descriptor 0) will be allocated,
# later when other file descriptors should happen to open.
#
# Therefore, here we redirects it to '/dev/null' by duplicating
# another file descriptor for '/dev/null' to the standard input (0).
# See SPARK-26175.
devnull = open(os.devnull, "r", encoding="utf-8")
os.dup2(devnull.fileno(), 0)
devnull.close()
try:
# Acknowledge that the fork was successful
outfile = sock.makefile(mode="wb")
write_int(os.getpid(), outfile)
outfile.flush()
outfile.close()
authenticated = (
os.environ.get("PYTHON_UNIX_DOMAIN_ENABLED", "false").lower()
== "true"
)
while True:
code = worker(sock, authenticated)
if code == 0:
authenticated = True
if not reuse or code:
# wait for closing
try:
while sock.recv(1024):
pass
except Exception:
pass
break
gc.collect()
except BaseException:
traceback.print_exc()
os._exit(1)
else:
os._exit(0)
else:
sock.close()
finally:
if poller is not None:
poller.unregister(0)
poller.unregister(listen_sock)
shutdown(1)
if __name__ == "__main__":
manager()