mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
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:
co-authored by
Claude Opus 5
parent
0846833b61
commit
f4df7c8045
+2
-2
@@ -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),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')"
|
||||
|
||||
Reference in New Issue
Block a user