11import json
22import os
33import re
4+ import time
45from collections .abc import Sequence
56from enum import StrEnum
67from os import path
910import requests
1011from dodal .utils import get_beamline_name
1112from pydantic import BaseModel
13+ from requests import ConnectionError , HTTPError , Response , Timeout
1214
15+ from mx_bluesky .common .external_interaction .alerting import get_alerting_service
1316from mx_bluesky .common .parameters .components import (
1417 WithVisit ,
1518)
2831from mx_bluesky .hyperion .external_interaction .config_server import (
2932 get_hyperion_feature_settings ,
3033)
34+ from mx_bluesky .hyperion .plan_runner import PlanError
3135
3236T = TypeVar ("T" , bound = WithVisit )
3337MULTIPIN_PREFIX = "multipin"
3438MULTIPIN_FORMAT_DESC = "Expected multipin format is multipin_{number_of_wells}x{well_size}+{distance_between_tip_and_first_well}"
3539MULTIPIN_REGEX = rf"^{ MULTIPIN_PREFIX } _(\d+)x(\d+(?:\.\d+)?)\+(\d+(?:\.\d+)?)$"
3640MX_GENERAL_ROOT_REGEX = r"^/dls/(?P<beamline>[^/]+)/data/[^/]*/(?P<visit>[^/]+)(?:/|$)"
3741
42+ MAX_TRIES = 3
43+ RETRY_INITIAL_DELAY_S = 2
44+
3845
3946class _InstructionType (StrEnum ):
4047 WAIT = "wait"
@@ -46,7 +53,10 @@ def create_parameters_from_agamemnon() -> Sequence[BaseModel]:
4653 mx-bluesky instructions.
4754 Returns:
4855 The generated sequence of mx-bluesky parameters, or empty list if
49- no instructions."""
56+ no instructions.
57+ Raises:
58+ PlanError: if the instructions could not be fetched from Agamemnon
59+ """
5060 beamline_name = get_beamline_name ("i03" )
5161 agamemnon_instruction = _get_next_instruction (beamline_name )
5262 if agamemnon_instruction :
@@ -75,11 +85,46 @@ def _instruction_and_data(agamemnon_instruction: dict) -> tuple[str, Any]:
7585
7686
7787def _get_parameters_from_url (url : str ) -> dict :
78- response = requests .get (url , headers = {"Accept" : "application/json" })
79- response .raise_for_status ()
88+ tries , delay = MAX_TRIES , RETRY_INITIAL_DELAY_S
89+ while tries > 0 :
90+ tries -= 1
91+ try :
92+ response = requests .get (url , headers = {"Accept" : "application/json" })
93+ try :
94+ response .raise_for_status ()
95+ break
96+ except HTTPError as e :
97+ if _is_server_error (response ):
98+ LOGGER .warning (
99+ f"Agamemnon returned server error status { response .status_code } , retries left { tries } : { str (e )} "
100+ )
101+ else :
102+ msg = f"Agamemnon returned unexpected HTTP response status code { response .status_code } "
103+ get_alerting_service ().raise_error_alert (msg , {})
104+ raise PlanError (msg ) from e
105+ except ConnectionError as e :
106+ LOGGER .warning (
107+ f"Connection error attempting to connect to agamemnon, retries left { tries } " ,
108+ exc_info = e ,
109+ )
110+ except Timeout :
111+ LOGGER .warning (
112+ f"Timed out attempting to connect to agamemnon, retries left { tries } "
113+ )
114+ if tries :
115+ time .sleep (delay ) # noqa
116+ delay *= 2
117+ else :
118+ msg = f"Unable to fetch instruction from agamemnon after { MAX_TRIES } attempts, ending UDC."
119+ get_alerting_service ().raise_error_alert (msg , {})
120+ raise PlanError (msg )
80121 return json .loads (response .content )
81122
82123
124+ def _is_server_error (response : Response ):
125+ return 500 <= response .status_code < 600
126+
127+
83128def _get_pin_type_from_agamemnon_collect_parameters (
84129 collect_parameters : dict ,
85130) -> PinTypeParam :
0 commit comments