Skip to content

Commit 568315e

Browse files
lukaszachysandrobonazzola
authored andcommitted
squash: Fix race when session is initialized
Signed-off-by: Sandro Bonazzola <sbonazzo@redhat.com>
1 parent f25c560 commit 568315e

1 file changed

Lines changed: 119 additions & 74 deletions

File tree

did/plugins/jira.py

Lines changed: 119 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130

131131
import os
132132
import re
133+
import threading
133134
import time
134135
import urllib.parse
135136
from argparse import Namespace
@@ -418,6 +419,37 @@ def __init__(self, /,
418419
self.user: User
419420
super().__init__(option, name, parent, user, options=options)
420421

422+
def _get_user_aaid(self) -> str:
423+
"""
424+
Get the user's Atlassian Account ID (AAID) for Jira Cloud.
425+
"""
426+
query = urllib.parse.quote(self.user.email)
427+
search_url = f"{self.parent.url}/rest/api/3/user/search?query={query}"
428+
429+
log.debug("Fetching user AAID for %s from %s", self.user.email, search_url)
430+
431+
try:
432+
response = self.parent.session.get(
433+
search_url,
434+
timeout=self.parent.timeout
435+
)
436+
response.raise_for_status()
437+
users = response.json()
438+
439+
if not users:
440+
raise ReportError(
441+
f"No user found for email {self.user.email} in Jira Cloud"
442+
)
443+
444+
# Return the accountId of the first matching user
445+
return users[0]["accountId"]
446+
447+
except requests.exceptions.RequestException as error:
448+
log.error("Failed to fetch user AAID: %s", error)
449+
raise ReportError(
450+
f"Failed to fetch user AAID for {self.user.email}"
451+
) from error
452+
421453
def _get_user_identifier(self) -> str:
422454
"""
423455
Get the correct user identifier for JQL queries.
@@ -582,7 +614,11 @@ class JiraTransition(JiraStats):
582614

583615
def fetch(self) -> None:
584616
self.parent: JiraStatsGroup
585-
user_id = self._get_user_identifier()
617+
# For cloud we need AAID
618+
if self.parent.is_jira_cloud:
619+
user_id = self._get_user_aaid()
620+
else:
621+
user_id = self._get_user_identifier()
586622
log.info(
587623
"[%s] Searching for issues transitioned to '%s' by '%s'",
588624
self.option,
@@ -591,7 +627,7 @@ def fetch(self) -> None:
591627
query = (
592628
f"status changed to '{self.parent.transition_status}' "
593629
f"and status changed by '{user_id}' "
594-
f"after {self.options.since} before {self.options.until}"
630+
f"after '{self.options.since} 00:00' before {self.options.until}"
595631
)
596632
if self.parent.project:
597633
query = query + f" AND project in ({self.parent.project})"
@@ -746,6 +782,7 @@ def __init__(self,
746782
user: Optional[User] = None) -> None:
747783
StatsGroup.__init__(self, option, name, parent, user)
748784
self._session: Optional[requests.Session] = None
785+
self._session_lock = threading.Lock()
749786
# Make sure there is an url provided
750787
config = dict(Config().section(option))
751788
self.timeout: float = float(config.get("timeout", TIMEOUT))
@@ -860,17 +897,17 @@ def __init__(self,
860897
option=f"{option}-worklog", parent=self,
861898
name=f"Issues with worklogs in {option}"))
862899

863-
def _basic_auth_session(self) -> requests.Response:
864-
basic_auth = (self.auth_username, self.auth_password)
900+
def _basic_auth_session(self, _session) -> requests.Response:
901+
_session.auth = (self.auth_username, self.auth_password)
865902

866903
if self.is_jira_cloud:
867904
# For Jira Cloud, verify credentials by calling
868905
# the /myself endpoint
869906
log.debug("Connecting to Jira Cloud at %s for basic auth", self.url)
870907
test_url = f"{self.url}/rest/api/{self.api_version}/myself"
871908
try:
872-
response = self.session.get(
873-
test_url, auth=basic_auth, verify=self.ssl_verify,
909+
response = _session.get(
910+
test_url, verify=self.ssl_verify,
874911
timeout=self.timeout)
875912
except (requests.exceptions.ConnectionError,
876913
urllib3.exceptions.NewConnectionError,
@@ -882,14 +919,12 @@ def _basic_auth_session(self) -> requests.Response:
882919
"(not your password). Generate one at: "
883920
"https://id.atlassian.com/manage-profile/security/api-tokens"
884921
) from error
885-
# Store auth for all future requests
886-
self.session.auth = basic_auth
887922
else:
888923
# For Jira Server/Data Center, use session-based auth
889924
log.debug("Connecting to %s for basic auth", self.auth_url)
890925
try:
891-
response = self.session.get(
892-
self.auth_url, auth=basic_auth, verify=self.ssl_verify,
926+
response = _session.get(
927+
self.auth_url, verify=self.ssl_verify,
893928
timeout=self.timeout)
894929
except (requests.exceptions.ConnectionError,
895930
urllib3.exceptions.NewConnectionError,
@@ -900,13 +935,13 @@ def _basic_auth_session(self) -> requests.Response:
900935
) from error
901936
return response
902937

903-
def _token_auth_session(self) -> requests.Response:
938+
def _token_auth_session(self, _session) -> requests.Response:
904939
myself_url = f"{self.url}/rest/api/{self.api_version}/myself"
905940
log.debug("Connecting to %s", myself_url)
906-
self.session.headers["Authorization"] = f"Bearer {self.token}"
941+
_session.headers["Authorization"] = f"Bearer {self.token}"
907942
while True:
908943
try:
909-
response = self.session.get(
944+
response = _session.get(
910945
myself_url,
911946
verify=self.ssl_verify,
912947
timeout=self.timeout)
@@ -924,13 +959,11 @@ def _token_auth_session(self) -> requests.Response:
924959
break
925960
return response
926961

927-
def _gss_api_auth_session(self) -> requests.Response:
928-
if self._session is None:
929-
raise RuntimeError("Session has not been initialized")
962+
def _gss_api_auth_session(self, _session) -> requests.Response:
930963
log.debug("Connecting to %s for gssapi auth", self.auth_url)
931964
gssapi_auth = HTTPSPNEGOAuth(mutual_authentication=DISABLED)
932965
try:
933-
response: requests.Response = self._session.get(
966+
response: requests.Response = _session.get(
934967
self.auth_url, auth=gssapi_auth, verify=self.ssl_verify,
935968
timeout=self.timeout)
936969
except (requests.exceptions.ConnectionError,
@@ -943,72 +976,84 @@ def _gss_api_auth_session(self) -> requests.Response:
943976
return response
944977

945978
def renew_session(self) -> requests.Session:
946-
self._session = None
979+
with self._session_lock:
980+
self._session = None
947981
return self.session
948982

949983
@property
950984
def session(self) -> requests.Session:
951985
""" Initialize the session """
952986
# pylint: disable=too-many-branches
987+
# If session already exists, return it
953988
if self._session is not None:
954989
return self._session
955-
self._session = requests.Session()
956-
# Disable SSL warning when ssl_verify is False
957-
if not self.ssl_verify:
958-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
959-
while True:
960-
if self.auth_type == 'basic':
961-
response = self._basic_auth_session()
962-
elif self.auth_type == "token":
963-
response = self._token_auth_session()
964-
else:
965-
response = self._gss_api_auth_session()
966-
if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
967-
retry_after = 1
968-
if response.headers.get("X-RateLimit-Remaining") == "0":
969-
retry_after = max(int(response.headers["retry-after"]), 1)
970-
log.debug("Jira rate limit exceeded.")
971-
log.debug("Sleeping now for %s.",
972-
listed(retry_after, 'second'))
973-
time.sleep(retry_after)
974-
continue
975-
try:
976-
response.raise_for_status()
977-
except requests.exceptions.HTTPError as error:
978-
log.error(error)
979-
raise ReportError(
980-
"Jira authentication failed. Check credentials or kinit."
981-
) from error
982-
break
983-
if self.token_expiration:
990+
991+
# Acquire lock to initialize session
992+
with self._session_lock:
993+
# Double-check: another thread might have initialized it
994+
# while we were waiting for the lock
995+
if self._session is not None:
996+
return self._session
997+
998+
# Do not set it to self._sesson until it is fully ready
999+
_session = requests.Session()
1000+
# Disable SSL warning when ssl_verify is False
1001+
if not self.ssl_verify:
1002+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
9841003
while True:
1004+
if self.auth_type == 'basic':
1005+
response = self._basic_auth_session(_session)
1006+
elif self.auth_type == "token":
1007+
response = self._token_auth_session(_session)
1008+
else:
1009+
response = self._gss_api_auth_session(_session)
1010+
if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
1011+
retry_after = 1
1012+
if response.headers.get("X-RateLimit-Remaining") == "0":
1013+
retry_after = max(int(response.headers["retry-after"]), 1)
1014+
log.debug("Jira rate limit exceeded.")
1015+
log.debug("Sleeping now for %s.",
1016+
listed(retry_after, 'second'))
1017+
time.sleep(retry_after)
1018+
continue
9851019
try:
986-
response = self._session.get(
987-
f"{self.url}/rest/pat/latest/tokens",
988-
verify=self.ssl_verify,
989-
timeout=self.timeout)
990-
9911020
response.raise_for_status()
992-
token_found = None
993-
for token in response.json():
994-
if token["name"] == self.token_name:
995-
token_found = token
996-
break
997-
if token_found is None:
998-
raise ValueError(
999-
f"Can't check validity for the '{self.token_name}' "
1000-
f"token as it doesn't exist.")
1001-
expiring_at = datetime.strptime(
1002-
token_found["expiringAt"], r"%Y-%m-%dT%H:%M:%S.%f%z")
1003-
delta = (
1004-
expiring_at.astimezone() - datetime.now().astimezone())
1005-
if delta.days < self.token_expiration:
1006-
log.warning("Jira token '%s' expires in %s days.",
1007-
self.token_name, delta.days)
1008-
except (requests.exceptions.HTTPError,
1009-
KeyError, ValueError, requests.Timeout) as error:
1010-
log.warning(error)
1011-
time.sleep(1)
1012-
continue
1021+
except requests.exceptions.HTTPError as error:
1022+
log.error(error)
1023+
raise ReportError(
1024+
"Jira authentication failed. Check credentials or kinit."
1025+
) from error
10131026
break
1014-
return self._session
1027+
if self.token_expiration:
1028+
while True:
1029+
try:
1030+
response = _session.get(
1031+
f"{self.url}/rest/pat/latest/tokens",
1032+
verify=self.ssl_verify,
1033+
timeout=self.timeout)
1034+
1035+
response.raise_for_status()
1036+
token_found = None
1037+
for token in response.json():
1038+
if token["name"] == self.token_name:
1039+
token_found = token
1040+
break
1041+
if token_found is None:
1042+
raise ValueError(
1043+
f"Can't check validity for the '{self.token_name}' "
1044+
f"token as it doesn't exist.")
1045+
expiring_at = datetime.strptime(
1046+
token_found["expiringAt"], r"%Y-%m-%dT%H:%M:%S.%f%z")
1047+
delta = (
1048+
expiring_at.astimezone() - datetime.now().astimezone())
1049+
if delta.days < self.token_expiration:
1050+
log.warning("Jira token '%s' expires in %s days.",
1051+
self.token_name, delta.days)
1052+
except (requests.exceptions.HTTPError,
1053+
KeyError, ValueError, requests.Timeout) as error:
1054+
log.warning(error)
1055+
time.sleep(1)
1056+
continue
1057+
break
1058+
self._session = _session
1059+
return self._session

0 commit comments

Comments
 (0)