platform: determine hostname / fqdn / hostid lazily, refs #9470

Determining the fqdn issues a DNS query. That happened at import time, so
EVERY borg process paid for it - which can take very long on hosts whose
own hostname does not resolve (~35s per process on github's macOS runners,
which made the macOS test jobs take hours: each remote test spawns ~10
borg serve / client processes).

Replace the module-level hostname / fqdn / hostid attributes with
get_hostname() / get_fqdn() / get_hostid() functions. A cached
_gethostname() keeps all derived values consistent for the process
lifetime; get_fqdn() is additionally cached because it issues the DNS
query. Processes that never use them no longer resolve anything at all,
on any platform. get_hostname() never needs DNS, and a set BORG_HOST_ID
avoids the fqdn lookup entirely.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Thomas Waldmann
2026-08-23 01:44:51 +02:00
co-authored by Claude Opus 5
parent 0846833b61
commit f4df7c8045
6 changed files with 40 additions and 22 deletions
+2 -2
View File
@@ -50,7 +50,7 @@ from .helpers import utcnow
from .lrucache import LRUCache
from .patterns import PathPrefixPattern, FnmatchPattern, IECommand
from .item import Item, ArchiveItem, ItemDiff
from .platform import acl_get, acl_set, set_flags, get_flags, swidth, hostname
from .platform import acl_get, acl_set, set_flags, get_flags, swidth, get_hostname
from .remote import cache_if_remote
from .repository import Repository, LIST_SCAN_LIMIT
@@ -636,7 +636,7 @@ Utilization of max. archive size: {csize_max:.0%}
'comment': comment or '',
'items': self.items_buffer.chunks,
'cmdline': sys.argv,
'hostname': os.environ.get('BORG_HOSTNAME') or hostname,
'hostname': os.environ.get('BORG_HOSTNAME') or get_hostname(),
'username': os.environ.get('BORG_USERNAME') or getuser(),
'time': start.strftime(ISO_FORMAT),
'time_end': end.strftime(ISO_FORMAT),
+3 -2
View File
@@ -205,13 +205,14 @@ def format_line(format, data):
def replace_placeholders(text, overrides={}):
"""Replace placeholders in text with their values."""
from ..platform import fqdn, hostname, getosusername
from ..platform import get_fqdn, get_hostname, getosusername
fqdn = get_fqdn()
current_time = datetime.now(timezone.utc)
data = {
'pid': os.getpid(),
'fqdn': fqdn,
'reverse-fqdn': '.'.join(reversed(fqdn.split('.'))),
'hostname': os.environ.get('BORG_HOSTNAME') or hostname,
'hostname': os.environ.get('BORG_HOSTNAME') or get_hostname(),
'now': DatetimeWrapper(current_time.astimezone(None)),
'utcnow': DatetimeWrapper(current_time),
'unixtime': int(current_time.timestamp()),
+1 -1
View File
@@ -11,7 +11,7 @@ from .base import acl_get, acl_set
from .base import set_flags, get_flags
from .base import SaveFile, SyncFile, sync_dir, fdatasync, safe_fadvise
from .base import swidth, API_VERSION
from .base import process_alive, get_process_id, local_pid_alive, fqdn, hostname, hostid
from .base import process_alive, get_process_id, local_pid_alive, get_hostname, get_fqdn, get_hostid
OS_API_VERSION = API_VERSION
+28 -14
View File
@@ -1,4 +1,5 @@
import errno
import functools
import os
import socket
import uuid
@@ -276,20 +277,33 @@ def getfqdn(name=''):
return name
# For performance reasons, only determine hostname / FQDN / host ID once.
# XXX This sometimes requires live internet access for issuing a DNS query in the background.
hostname = socket.gethostname()
fqdn = getfqdn(hostname)
# Some people put the FQDN into /etc/hostname (which is wrong; it should be the short hostname).
# Fix this (do the same as "hostname --short" CLI command does internally):
hostname = hostname.split('.')[0]
@functools.cache
def _gethostname():
"""socket.gethostname(), cached so all derived values are consistent for the process lifetime."""
return socket.gethostname()
# uuid.getnode() is problematic in some environments (e.g., OpenVZ, see #3968) where the virtual MAC address
# is all-zero. uuid.getnode falls back to returning a random value in that case, which is not what we want.
# Thus, we offer BORG_HOST_ID where a user can set an own, unique ID for each of his hosts.
hostid = os.environ.get('BORG_HOST_ID')
if not hostid:
hostid = f'{fqdn}@{uuid.getnode()}'
def get_hostname():
"""Return the (short) hostname of this machine."""
# Some people put the FQDN into /etc/hostname (which is wrong; it should be the short hostname).
# Fix this (do the same as "hostname --short" CLI command does internally):
return _gethostname().split('.')[0]
@functools.cache
def get_fqdn():
"""Return the fully qualified domain name of this machine."""
# Cached: this issues a DNS query, which can take very long on hosts whose own
# hostname does not resolve (e.g., ~35s per query on github's macOS CI runners, see #9470).
return getfqdn(_gethostname())
def get_hostid():
"""Return an identifier that is unique for this machine (host)."""
# uuid.getnode() is problematic in some environments (e.g., OpenVZ, see #3968) where the virtual MAC address
# is all-zero. uuid.getnode falls back to returning a random value in that case, which is not what we want.
# Thus, we offer BORG_HOST_ID where a user can set an own, unique ID for each of his hosts.
return os.environ.get('BORG_HOST_ID') or f'{get_fqdn()}@{uuid.getnode()}'
def get_process_id():
@@ -301,7 +315,7 @@ def get_process_id():
"""
thread_id = 0
pid = os.getpid()
return hostid, pid, thread_id
return get_hostid(), pid, thread_id
def process_alive(host, pid, thread):
+2 -1
View File
@@ -42,8 +42,9 @@ def process_alive(host, pid, thread):
always returns True, since there is no real way to check.
"""
from . import local_pid_alive
from . import hostid
from . import get_hostid
hostid = get_hostid()
assert isinstance(host, str)
assert isinstance(hostid, str)
assert isinstance(pid, int)
+4 -2
View File
@@ -243,7 +243,8 @@ class TestLocationWithoutEnv:
Location('ssh://user@host:/path')
def test_omit_archive(self):
from borg.platform import hostname
from borg.platform import get_hostname
hostname = get_hostname()
loc = Location('ssh://user@host:1234/repos/{hostname}::archive')
loc_without_archive = loc.omit_archive()
assert loc_without_archive.archive is None
@@ -262,7 +263,8 @@ class TestLocationWithEnv:
"Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
def test_ssh_placeholder(self, monkeypatch):
from borg.platform import hostname
from borg.platform import get_hostname
hostname = get_hostname()
monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/{hostname}')
assert repr(Location('::archive')) == \
f"Location(proto='ssh', user='user', host='host', port=1234, path='/{hostname}', archive='archive')"