Skip to content

Commit 633bc18

Browse files
committed
Retry transient failures during upstream sync
1 parent 21115f1 commit 633bc18

2 files changed

Lines changed: 118 additions & 21 deletions

File tree

application/cmd/cre_main.py

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,51 @@
3333
app = None
3434

3535

36+
def fetch_upstream_json(
37+
path: str,
38+
timeout: Optional[float] = None,
39+
max_attempts: Optional[int] = None,
40+
backoff_seconds: Optional[float] = None,
41+
) -> Dict[str, Any]:
42+
base_url = os.environ.get("CRE_UPSTREAM_API_URL", "https://opencre.org/rest/v1")
43+
timeout = timeout or float(os.environ.get("CRE_UPSTREAM_TIMEOUT_SECONDS", "30"))
44+
max_attempts = max_attempts or int(os.environ.get("CRE_UPSTREAM_MAX_ATTEMPTS", "4"))
45+
backoff_seconds = backoff_seconds or float(
46+
os.environ.get("CRE_UPSTREAM_RETRY_BACKOFF_SECONDS", "2")
47+
)
48+
url = f"{base_url}{path}"
49+
last_error: Optional[Exception] = None
50+
51+
for attempt in range(1, max_attempts + 1):
52+
try:
53+
response = requests.get(url, timeout=timeout)
54+
if response.status_code == 200:
55+
return response.json()
56+
57+
status_error = RuntimeError(
58+
f"cannot connect to upstream status code {response.status_code}"
59+
)
60+
# Retry only on transient upstream failures.
61+
if response.status_code < 500 and response.status_code != 429:
62+
raise status_error
63+
last_error = status_error
64+
except requests.exceptions.RequestException as exc:
65+
last_error = exc
66+
67+
if attempt < max_attempts:
68+
logger.warning(
69+
"upstream fetch failed for %s on attempt %s/%s, retrying",
70+
url,
71+
attempt,
72+
max_attempts,
73+
)
74+
time.sleep(backoff_seconds * attempt)
75+
76+
if last_error:
77+
raise RuntimeError(f"upstream fetch failed for {url}") from last_error
78+
raise RuntimeError(f"upstream fetch failed for {url}")
79+
80+
3681
def register_node(node: defs.Node, collection: db.Node_collection) -> db.Node:
3782
"""
3883
for each link find if either the root node or the link have a CRE,
@@ -238,6 +283,8 @@ def register_standard(
238283
):
239284
if os.environ.get("CRE_NO_GEN_EMBEDDINGS"):
240285
generate_embeddings = False
286+
if os.environ.get("CRE_NO_CALCULATE_GAP_ANALYSIS"):
287+
calculate_gap_analysis = False
241288

242289
if not standard_entries:
243290
logger.warning("register_standard() called with no standard_entries")
@@ -246,11 +293,11 @@ def register_standard(
246293
if collection is None:
247294
collection = db_connect(path=db_connection_str)
248295

249-
conn = redis.connect()
296+
conn = redis.connect() if calculate_gap_analysis else None
250297
ph = prompt_client.PromptHandler(database=collection)
251298
importing_name = standard_entries[0].name
252299
standard_hash = gap_analysis.make_resources_key([importing_name])
253-
if calculate_gap_analysis and conn.get(standard_hash):
300+
if calculate_gap_analysis and conn and conn.get(standard_hash):
254301
logger.info(
255302
f"Standard importing job with info-hash {standard_hash} has already returned, skipping"
256303
)
@@ -273,7 +320,7 @@ def register_standard(
273320
if generate_embeddings and importing_name:
274321
ph.generate_embeddings_for(importing_name)
275322

276-
if calculate_gap_analysis and not os.environ.get("CRE_NO_CALCULATE_GAP_ANALYSIS"):
323+
if calculate_gap_analysis:
277324
# calculate gap analysis
278325
populate_neo4j_db(db_connection_str)
279326
jobs = []
@@ -466,15 +513,7 @@ def download_graph_from_upstream(cache: str) -> None:
466513
collection = db_connect(path=cache).with_graph()
467514

468515
def download_cre_from_upstream(creid: str):
469-
cre_response = requests.get(
470-
os.environ.get("CRE_UPSTREAM_API_URL", "https://opencre.org/rest/v1")
471-
+ f"/id/{creid}"
472-
)
473-
if cre_response.status_code != 200:
474-
raise RuntimeError(
475-
f"cannot connect to upstream status code {cre_response.status_code}"
476-
)
477-
data = cre_response.json()
516+
data = fetch_upstream_json(f"/id/{creid}")
478517
credict = data["data"]
479518
cre = defs.Document.from_dict(credict)
480519
if cre.id in imported_cres:
@@ -486,15 +525,7 @@ def download_cre_from_upstream(creid: str):
486525
if link.document.doctype == defs.Credoctypes.CRE:
487526
download_cre_from_upstream(link.document.id)
488527

489-
root_cres_response = requests.get(
490-
os.environ.get("CRE_UPSTREAM_API_URL", "https://opencre.org/rest/v1")
491-
+ "/root_cres"
492-
)
493-
if root_cres_response.status_code != 200:
494-
raise RuntimeError(
495-
f"cannot connect to upstream status code {root_cres_response.status_code}"
496-
)
497-
data = root_cres_response.json()
528+
data = fetch_upstream_json("/root_cres")
498529
for root_cre in data["data"]:
499530
cre = defs.Document.from_dict(root_cre)
500531
register_cre(cre, collection)
@@ -625,6 +656,54 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover
625656
BaseParser().register_resource(
626657
secure_headers.SecureHeaders, db_connection_str=args.cache_file
627658
)
659+
if args.owasp_top10_2025_in:
660+
from application.utils.external_project_parsers.parsers import owasp_top10_2025
661+
662+
BaseParser().register_resource(
663+
owasp_top10_2025.OwaspTop10_2025, db_connection_str=args.cache_file
664+
)
665+
if args.owasp_api_top10_2023_in:
666+
from application.utils.external_project_parsers.parsers import (
667+
owasp_api_top10_2023,
668+
)
669+
670+
BaseParser().register_resource(
671+
owasp_api_top10_2023.OwaspApiTop10_2023,
672+
db_connection_str=args.cache_file,
673+
)
674+
if args.owasp_kubernetes_top10_2022_in:
675+
from application.utils.external_project_parsers.parsers import (
676+
owasp_kubernetes_top10_2022,
677+
)
678+
679+
BaseParser().register_resource(
680+
owasp_kubernetes_top10_2022.OwaspKubernetesTop10_2022,
681+
db_connection_str=args.cache_file,
682+
)
683+
if args.owasp_kubernetes_top10_2025_in:
684+
from application.utils.external_project_parsers.parsers import (
685+
owasp_kubernetes_top10_2025,
686+
)
687+
688+
BaseParser().register_resource(
689+
owasp_kubernetes_top10_2025.OwaspKubernetesTop10_2025,
690+
db_connection_str=args.cache_file,
691+
)
692+
if args.owasp_llm_top10_2025_in:
693+
from application.utils.external_project_parsers.parsers import (
694+
owasp_llm_top10_2025,
695+
)
696+
697+
BaseParser().register_resource(
698+
owasp_llm_top10_2025.OwaspLlmTop10_2025,
699+
db_connection_str=args.cache_file,
700+
)
701+
if args.owasp_aisvs_in:
702+
from application.utils.external_project_parsers.parsers import owasp_aisvs
703+
704+
BaseParser().register_resource(
705+
owasp_aisvs.OwaspAisvs, db_connection_str=args.cache_file
706+
)
628707
if args.pci_dss_4_in:
629708
from application.utils.external_project_parsers.parsers import pci_dss
630709

application/tests/cre_main_test.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import Any, Dict, List
77
from unittest import mock
88
from unittest.mock import Mock, patch
9+
import requests
910
from rq import Queue
1011
from application.utils import redis
1112
from application.prompt_client import prompt_client as prompt_client
@@ -313,6 +314,23 @@ def test_register_cre(self) -> None:
313314
],
314315
)
315316

317+
@patch("application.cmd.cre_main.time.sleep")
318+
@patch("application.cmd.cre_main.requests.get")
319+
def test_fetch_upstream_json_retries_transient_failures(
320+
self, mock_get, mock_sleep
321+
) -> None:
322+
transient_error = requests.exceptions.ConnectionError("reset by peer")
323+
success_response = Mock()
324+
success_response.status_code = 200
325+
success_response.json.return_value = {"data": []}
326+
mock_get.side_effect = [transient_error, success_response]
327+
328+
data = main.fetch_upstream_json("/root_cres")
329+
330+
self.assertEqual(data, {"data": []})
331+
self.assertEqual(mock_get.call_count, 2)
332+
mock_sleep.assert_called_once()
333+
316334
def test_parse_file(self) -> None:
317335
file: List[Dict[str, Any]] = [
318336
{

0 commit comments

Comments
 (0)