Skip to content

Add retry, error handling for agamemnon and blueapi requests#1690

Open
rtuck99 wants to merge 14 commits into
mainfrom
1688_supervisor_exits_unable_to_connect_blueapi
Open

Add retry, error handling for agamemnon and blueapi requests#1690
rtuck99 wants to merge 14 commits into
mainfrom
1688_supervisor_exits_unable_to_connect_blueapi

Conversation

@rtuck99

@rtuck99 rtuck99 commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Fixes

Also partially addresses

On encountering a connection error when making a request to hyperion-blueapi, hyperion-supervisor should now attempt a further 2 more retries before giving up.

Other errors or failure after exhausting retries should cause hyperion-supervisor to end UDC and release the baton, raise an error alert but not exit.

Similarly receiving a timeout error, 50x HTTP error or a connection error when requesting the next instruction from agamemnon, should result in up to 2 more retries, any other error or exhaustion of the retries should result in hyperion-supervisor releasing the baton with and raise an error alert but remaining running awaiting the baton.

Link to dodal PR (if required): #N/A
(remember to update pyproject.toml with the dodal commit tag if you need it for tests to pass!)

Instructions to reviewer on how to test:

  1. Tests pass
  2. Behaviour is as described above

Checks for reviewer

  • Would the PR title make sense to a user on a set of release notes

@codecov

codecov Bot commented Apr 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.07%. Comparing base (bff8d34) to head (80cad0a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1690      +/-   ##
==========================================
+ Coverage   93.03%   93.07%   +0.04%     
==========================================
  Files         164      164              
  Lines        8927     8981      +54     
==========================================
+ Hits         8305     8359      +54     
  Misses        622      622              
Components Coverage Δ
i24 SSX 77.16% <ø> (ø)
hyperion 98.77% <100.00%> (+0.02%) ⬆️
other 98.17% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rtuck99 rtuck99 added the hyperion Issues for Hyperion, the Bluesky UDC stack label Apr 1, 2026
@rtuck99 rtuck99 marked this pull request as ready for review April 8, 2026 15:46
@rtuck99 rtuck99 requested a review from a team as a code owner April 8, 2026 15:46

def raise_error_alert(self, content: str, metadata: dict[Metadata, str]):
"""
Raise an alert that will be forwarded to beamline staff and EHC controllers for out-of-hours

@CoePaul CoePaul Jun 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence would be shorter in the form
"Alert beamline staff and EHC controllers ... support."

( We can see from the method name it achieves this by raising something )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to retain the raise an alert phrasing - it's something that might have more meaning in future - various alerting paradigms such as AlertManager have the concept of alerts as things that can be raised and continue alerting until a subsequent action is taken.

Just using "alert" as a verb is weaker and could just mean sending a message, as opposed to something more substantial.

In the long run we will probably plug in to such systems via the alerting backend.

Comment thread src/mx_bluesky/common/external_interaction/alerting/_service.py
Comment thread src/mx_bluesky/common/external_interaction/alerting/_service.py
by default.

"""
beamline = get_beamline_name("")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the dodal getter for beamline name - passing it an empty string
as the default is going to leave us without a beamline name, but suppresses errors

I assume that is intended - it seems odd that the getter doesn't just define an empty string
if everyone in practice is just going to step around the potential warning / error anyway

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given line 64 it's tempting to use "UNSPECIFIED BEAMLINE" or similar ... ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the function has changed since this was originally authored and I can now get rid of the parameter.

def _get_parameters_from_url(url: str) -> dict:
response = requests.get(url, headers={"Accept": "application/json"})
response.raise_for_status()
tries, delay = MAX_TRIES, RETRY_INITIAL_DELAY_S

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really - could these not have been done on separate lines -
or does ruff ( or similar ) keep putting them back onto one line?
one line - one action is always reader friendlier IMHO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python gives you tuple assignment, it's the python way. They are related values so it makes sense to me.

Comment thread src/mx_bluesky/hyperion/external_interaction/agamemnon.py
Comment thread src/mx_bluesky/hyperion/external_interaction/agamemnon.py
Comment thread src/mx_bluesky/hyperion/external_interaction/agamemnon.py Outdated
Comment thread src/mx_bluesky/hyperion/external_interaction/agamemnon.py
Comment thread src/mx_bluesky/hyperion/supervisor/_supervisor.py
Comment thread src/mx_bluesky/hyperion/supervisor/_supervisor.py Outdated
Comment thread src/mx_bluesky/hyperion/supervisor/_supervisor.py Outdated
Comment thread src/mx_bluesky/hyperion/supervisor/_supervisor.py
mock_requests.get.assert_has_calls(
[
call(
"http://agamemnon.diamond.ac.uk/getnextcollect/i03",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not clear to me from the test
why i03 is going to be the obvious default ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

external_interaction package has an autouse fixture in conftest.py that sets the beamline to i03.

In any case i03 is currently the only user of hyperion so I think it can be assumed that it is an appropriate beamline to use in tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think tests should ever know anything about a real URL - unless the
test is a system level test to see if it can genuinely see that ( but then You would never want such a test in CI )

Here it is a misleading realistic URL and creates the bad smell that the test might be dependent on the real IO3 beamline for a CI suite to pass - nobody wants to have read all the details of the code learn whether or not that is true

"... i00" ( instead of "... i03") would have made that independence plausible

Something like "http:/not_a_url/not_a_beamline" - even more so

Comment thread tests/unit_tests/hyperion/external_interaction/test_agamemnon.py
Comment thread tests/unit_tests/hyperion/external_interaction/test_agamemnon.py
Comment thread tests/unit_tests/hyperion/external_interaction/test_agamemnon.py
mock_alert_service: MagicMock,
):
response = Mock(spec=Response)
response.status_code = 400

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we really mean 40x - could we not have a parameterised test that racks up inputs 400 to 409 inclusive ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could skip a few but at least maybe cover 3 or 4 of 'em

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_parameters_from_url() uses requests library's response.raise_for_status() method to separate error- from non-error statuses.

TBH with HTTP client libraries it is pretty pointless to try to test their internal error handling functions, they do all sorts of stuff that probably isn't even fully documented, like following redirects on 30x codes, retrying requests on 401 Unauthorized (because some client libraries don't like to send credentials if they are not needed). All you can do is trust that it mostly works as advertised.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If You're determined to only ever test 400 - I don't quibble with that
I do quibble with doing only 400 inside a tin misleading labelled 40x
change the label to say 400

A bottle of sulphuric acid isn't usually labelled "some acid or other"

Comment thread tests/unit_tests/hyperion/external_interaction/test_agamemnon.py
Comment thread tests/unit_tests/hyperion/supervisor/test_supervisor.py
Comment thread tests/unit_tests/hyperion/supervisor/test_supervisor.py Outdated

@CoePaul CoePaul left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggested places for improvement

@rtuck99 rtuck99 requested a review from CoePaul June 25, 2026 10:12

@CoePaul CoePaul left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can read my replies - which I'm mostly letting slide

  • and anyway it looks like it's got a merge conflict to repair

You seem to take the view that a test being brittle
can rely on the specifics of the existing production code
(or the specifics of the use cases that the production code is coupled to)
to rescue the test from the consequences of its own brittleness

Whereas another school of thought simply says
test brittleness is unnecessary and should be defensively
( defense through offense ) annihilated unless there's an extremely
good excuse for it being unavoidable / strictly necessary

I'm basically approving once the merge conflict irons out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hyperion Issues for Hyperion, the Bluesky UDC stack

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants