From ad1291cc9cb4fc6560a1f82d73a496168a6770c6 Mon Sep 17 00:00:00 2001 From: Johannes Kulik Date: Mon, 4 Aug 2025 11:12:14 +0200 Subject: [PATCH] Fix parallel initial version negotiation If two parallel greenthreads use the same, uninitialized client, there's a race-condition when both enter `negotiate_version()`: there is a request made to Ironic, which hands over execution to the other greenthread. The first greenthread returning from the request changes the state of the client and the second one reads the updated state and thus thinks it's in an error-handling call instead of the initial negotiation - and errors out. We fix this by adding a lock around the initial call to `negotiate_version()`. Change-Id: I9ec03d2bc34017c7670fd6903e5353a8c91e9f17 Closes-Bug: #2119323 Signed-off-by: Johannes Kulik --- ironicclient/common/http.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 055af2377..580cfef6e 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -19,6 +19,7 @@ import json import logging import re import textwrap +import threading import time from urllib import parse as urlparse @@ -345,6 +346,7 @@ class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter): _('The Bare Metal API endpoint cannot be detected and was ' 'not provided explicitly')) self.endpoint_trimmed = _trim_endpoint_api_version(endpoint) + self._first_negotiation_lock = threading.Lock() def _parse_version_headers(self, resp): return self._generic_parse_version_headers(resp.headers.get) @@ -369,7 +371,9 @@ class SessionClient(VersionNegotiationMixin, adapter.LegacyJsonAdapter): # NOTE(TheJulia): self.os_ironic_api_version is reset in # the self.negotiate_version() call if negotiation occurs. if self.os_ironic_api_version and self._must_negotiate_version(): - self.negotiate_version(self.session, None) + with self._first_negotiation_lock: + if self._must_negotiate_version(): + self.negotiate_version(self.session, None) kwargs.setdefault('user_agent', USER_AGENT) kwargs.setdefault('auth', self.auth)