Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
723ad80
Add UDS support
dometto Jul 22, 2026
791466a
Also run dashboard on unix domain socket
dometto Jul 23, 2026
455252d
Pass linter
dometto Jul 28, 2026
819c378
Move integration test to test_comms.py
dometto Jul 28, 2026
14b6670
uds:// -> unix://
dometto Jul 28, 2026
5bb2eea
Add some docs
dometto Jul 28, 2026
ebdd90d
Skip tests and don't register UDS backend on Windows
dometto Jul 28, 2026
de2a7ec
Don't check for unix socket type on Windows
dometto Jul 28, 2026
959bd1f
Move get_uds_path() to distributed.utils
dometto Jul 28, 2026
5c8451b
Dashboard: allow passing in 'unix://' as dashboard address for random…
dometto Jul 28, 2026
bd52b6e
Set dashboard unix socket permissions to 600
dometto Jul 28, 2026
34f1ba2
Fix get_uds_path
dometto Jul 28, 2026
0db0f6a
Add unix to --protocol help
dometto Jul 28, 2026
805dc71
fix uds path
dometto Jul 28, 2026
c2ce79b
Remove unnecessarily added comments
dometto Jul 28, 2026
66fb708
Add test and support for starting scheduler on a random unix socket
dometto Jul 28, 2026
d875699
refactor format_dashboard_link logic: allow setting unix socket path …
dometto Jul 29, 2026
e5fc50d
Add test for get_uds_path()
dometto Jul 29, 2026
d2e191a
Restore wrongly deleted line
dometto Jul 29, 2026
42a2561
Remove dashboard socket before binding.
dometto Jul 29, 2026
0bf2f6e
Fix test_get_uds_path on ubuntu.
dometto Jul 30, 2026
a5431bc
Fix resolver tests
dometto Jul 30, 2026
558101d
Fix get_uds_path method signature
dometto Jul 30, 2026
04a14d6
Fix UDS backend: ensure we return a filesystem path on host lookup
dometto Jul 30, 2026
588d58f
Skip all UDS tests on Windows
dometto Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion distributed/cli/dask_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
help="Preferred network interface like 'eth0' or 'ib0'",
)
@click.option(
"--protocol", type=str, default=None, help="Protocol like tcp, tls, or ucx"
"--protocol", type=str, default=None, help="Protocol like tcp, tls, unix, or ucx"
)
@click.option(
"--tls-ca-file",
Expand Down
2 changes: 1 addition & 1 deletion distributed/cli/dask_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
"--interface", type=str, default=None, help="Network interface like 'eth0' or 'ib0'"
)
@click.option(
"--protocol", type=str, default=None, help="Protocol like tcp, tls, or ucx"
"--protocol", type=str, default=None, help="Protocol like tcp, tls, unix, or ucx"
)
@click.option("--nthreads", type=int, default=0, help="Number of threads per process.")
@click.option(
Expand Down
41 changes: 41 additions & 0 deletions distributed/cli/tests/test_dask_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -353,6 +354,46 @@ def test_dashboard_port_zero(loop):
assert get_dashboard_port(c) > 0


@pytest.mark.skipif(WINDOWS, reason="POSIX only")
def test_dashboard_unix_socket(loop):
pytest.importorskip("bokeh")
port = open_port()

def _connect_unix_socket(path):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
s.connect(path)
except Exception:
return False
else:
return True
finally:
s.close()

with popen(
[
sys.executable,
"-m",
"dask",
"scheduler",
"--host",
f"127.0.0.1:{port}",
"--dashboard-address",
"unix://",
],
):
with Client(f"tcp://127.0.0.1:{port}", loop=loop) as c:
proto, rest = c.dashboard_link.split("://")
assert proto == "http+unix"
assert rest.endswith(".sock/status")
assert ":" not in rest # no port

path = rest.split("/status")[0]
assert os.path.isabs(path)
assert os.path.exists(path)
assert _connect_unix_socket(path)


PRELOAD_TEXT = """
_scheduler_info = {}

Expand Down
12 changes: 3 additions & 9 deletions distributed/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1337,16 +1337,10 @@ def dashboard_link(self):
scheduler, info = self._get_scheduler_info(n_workers=0)
if scheduler is None:
return None
else:
protocol, rest = scheduler.address.split("://")

port = info["services"]["dashboard"]
if protocol == "inproc":
host = "localhost"
else:
host = rest.split(":")[0]

return format_dashboard_link(host, port)
return format_dashboard_link(
scheduler.address, info["services"]["dashboard"]
)

def _get_scheduler_info(self, n_workers):
from distributed.scheduler import Scheduler
Expand Down
5 changes: 4 additions & 1 deletion distributed/comm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
from distributed.comm.core import Comm, CommClosedError, connect, listen
from distributed.comm.registry import backends
from distributed.comm.utils import get_tcp_server_address, get_tcp_server_addresses
from distributed.compatibility import WINDOWS


def _register_transports():
from distributed.comm import inproc, tcp, ws
from distributed.comm import inproc, tcp, uds, ws

backends["tcp"] = tcp.TCPBackend()
backends["tls"] = tcp.TLSBackend()
if not WINDOWS:
backends["unix"] = uds.UDSBackend()

try:
# If `distributed-ucxx` is installed, it takes over the protocol="ucx" support
Expand Down
2 changes: 2 additions & 0 deletions distributed/comm/addressing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def parse_address(addr: str, strict: bool = False) -> tuple[str, str]:

>>> parse_address('tcp://127.0.0.1')
('tcp', '127.0.0.1')
>>> parse_address('unix:///tmp/socket')
('unix', '/tmp/socket')

If strict is set to true the address must have a scheme.
"""
Expand Down
7 changes: 6 additions & 1 deletion distributed/comm/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
from dask.utils import parse_timedelta

from distributed.comm import registry
from distributed.comm.addressing import get_address_host, parse_address, resolve_address
from distributed.comm.addressing import (
get_address_host,
parse_address,
resolve_address,
)
from distributed.metrics import time
from distributed.protocol.compression import get_compression_settings
from distributed.protocol.pickle import HIGHEST_PROTOCOL
Expand Down Expand Up @@ -321,6 +325,7 @@ async def connect(
scheme, loc = parse_address(addr)
backend = registry.get_backend(scheme)
connector = backend.get_connector()

comm = None

start = time()
Expand Down
9 changes: 7 additions & 2 deletions distributed/comm/tcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
get_tcp_server_address,
to_frames,
)
from distributed.compatibility import WINDOWS
from distributed.protocol.utils import host_array, pack_frames_prelude, unpack_frames
from distributed.system import MEMORY_LIMIT
from distributed.utils import ensure_ip, ensure_memoryview, get_ip, nbytes
Expand All @@ -59,7 +60,7 @@ def set_tcp_timeout(comm):
"""
Set kernel-level TCP timeout on the stream.
"""
if comm.closed():
if comm.closed() or (not WINDOWS and comm.socket.family is socket.AF_UNIX):

@dometto dometto Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we have to check for unix socket stuff in tcp.py -- that should all belong to the subclass.

To refactor this we could change set_tcp_timeout to a function in the TCP class, create a new UDS subclass of TCP in uds.py, and override set_tcp_timeout there.

An example of moving set_tcp_timeout to the TCP class is here: dometto@5e60065

Let me know if you'd like me to implement that.

return

timeout = dask.config.get("distributed.comm.timeouts.tcp")
Expand Down Expand Up @@ -124,7 +125,10 @@ def get_stream_address(comm):
if comm.closed():
raise CommClosedError()

return unparse_host_port(*comm.socket.getsockname()[:2])
if not WINDOWS and comm.socket.family is socket.AF_UNIX:

@dometto dometto Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we have to check for unix socket stuff in tcp.py -- that should all belong to the subclass.

Refactoring this is slightly more involved than for set_tcp_tmeout, as this function is called from inside _handle_stream.

To work around this, we would have to implement _handle_stream entirely in UDSListener, instead of making it subclass TCPListener. That is perhaps cleaner anyway.

An example of this approach is again here: dometto@5e60065

Let me know if you'd like me to implement that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically dometto@5e60065 removes all subclassing from uds.py except for the UDS Comm, which subclasses TCP -- in order not to have to duplicate the code for reading and writing streams.

To make the UDS code entirely independent from TCP another option would be to create a new mixin class for IOStream reading and writing, and include that in both UDS and TCP. Not sure if that's worth it.

return comm.socket.getsockname() or comm.socket.getpeername()
else:
return unparse_host_port(*comm.socket.getsockname()[:2])


def convert_stream_closed_error(obj, exc):
Expand Down Expand Up @@ -567,6 +571,7 @@ async def connect(self, address, deserialize=True, **connection_args):
raise FatalCommClosedError() from err

local_address = self.prefix + get_stream_address(stream)

comm = self.comm_class(
stream, local_address, self.prefix + address, deserialize
)
Expand Down
65 changes: 64 additions & 1 deletion distributed/comm/tests/test_comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from distributed.comm.registry import backends, get_backend
from distributed.comm.tcp import get_stream_address
from distributed.compatibility import asyncio_run
from distributed.compatibility import WINDOWS, asyncio_run
from distributed.config import get_loop_factory
from distributed.metrics import time
from distributed.protocol import Serialized, deserialize, serialize, to_serialize
Expand Down Expand Up @@ -55,6 +55,17 @@ def tcp(monkeypatch, request):
return tcp


@pytest.fixture(params=["tornado"])
def uds(monkeypatch, request):
"""Set the TCP backend to either tornado or asyncio"""
if request.param == "tornado":
import distributed.comm.uds as uds
else:
raise NotImplementedError()
monkeypatch.setitem(backends, "uds", uds.UDSBackend())
return uds


ca_file = get_cert("tls-ca-cert.pem")

# The Subject field of our test certs
Expand Down Expand Up @@ -171,6 +182,8 @@ def test_get_address_host(tcp):

assert f("tcp://127.0.0.1:123") == "127.0.0.1"
assert f("inproc://%s/%d/123" % (get_ip(), os.getpid())) == get_ip()
if not WINDOWS:
assert f("unix:///tmp/dask.sock") == "/tmp/dask.sock"


def test_resolve_address(tcp):
Expand All @@ -192,6 +205,9 @@ def test_resolve_address(tcp):
assert f("tcp://localhost:456") == "tcp://127.0.0.1:456"
assert f("tls://localhost:456") == "tls://127.0.0.1:456"

if not WINDOWS:
assert f("unix:///tmp/dask.sock") == "unix:///tmp/dask.sock"


def test_get_local_address_for(tcp):
f = get_local_address_for
Expand Down Expand Up @@ -277,6 +293,53 @@ async def client_communicate(key, delay=0):
assert set(l) == {1234} | set(range(N))


@pytest.mark.skipif(WINDOWS, reason="No unix sockets on Windows")
@gen_test()
async def test_uds_specific(uds):
"""
Test concrete UDS API.
"""

async def handle_comm(comm):
assert comm.peer_address == (f"unix://{host}:0")
assert comm.extra_info == {}
msg = await comm.read()
msg["op"] = "pong"
await comm.write(msg)
await comm.close()

listener = await uds.UDSListener("localhost", handle_comm)
host, port = listener.get_host_port()

assert host.endswith(".sock")
assert port == 0 # we fake port 0 when using UDS

l = []

async def client_communicate(key, delay=0):
comm = await connect(listener.contact_address)
assert comm.peer_address == f"unix://{host}:0"
assert comm.extra_info == {}
await comm.write({"op": "ping", "data": key})
if delay:
await asyncio.sleep(delay)
msg = await comm.read()
assert msg == {"op": "pong", "data": key}
l.append(key)
await comm.close()

await client_communicate(key=1234)

# Many clients at once
N = 100
futures = [client_communicate(key=i, delay=0.05) for i in range(N)]
await asyncio.gather(*futures)
assert set(l) == {1234} | set(range(N))

listener.stop()
assert not os.path.exists(host) # assert socket deleted


@pytest.mark.parametrize("sni", [None, "localhost"])
@gen_test()
async def test_tls_specific(tcp, sni):
Expand Down
23 changes: 23 additions & 0 deletions distributed/comm/tests/test_uds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest

from distributed.compatibility import WINDOWS

from distributed.comm.addressing import parse_address, unparse_address
from distributed.comm.registry import backends, get_backend
from distributed.comm.uds import UDSBackend


@pytest.mark.skipif(WINDOWS, reason="No unix sockets on Windows")
def test_registered():
assert "unix" in backends
backend = get_backend("unix")
assert isinstance(backend, UDSBackend)


@pytest.mark.skipif(WINDOWS, reason="No unix sockets on Windows")
def test_parse_uds_address():
addr = "unix:///tmp/dask-test.sock"
scheme, loc = parse_address(addr)
assert scheme == "unix"
assert loc == "/tmp/dask-test.sock"
assert unparse_address(scheme, loc) == addr
Loading
Loading