Skip to content

Add better error handling#682

Open
dlcaballero16 wants to merge 11 commits into
nextfrom
fix-livedata-error-reporting
Open

Add better error handling#682
dlcaballero16 wants to merge 11 commits into
nextfrom
fix-livedata-error-reporting

Conversation

@dlcaballero16

@dlcaballero16 dlcaballero16 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Description of work

Originally this story was to fix an issue with reducing livedata, but I was not able to recreate the defect. These changes should hopefully prevent the error from happening, and at the very least improve the error message that gets reported by SNAPRed. Also, a CIS script was added that will go through the calibration folder and add cycle info as needed. Cycle info is required for reduction, but if no cycle data is present, the reduction result will be marked as diagnostic.

Explanation of work

In LocalDataService.py, attempting to read live metadata is now run in a try/except block and an error message about missing PV info is reported.
In StateValidationException.py, the actual exception string is added to the error message in the case that the exception type is not a FileNotFoundError or a PermissionError.
A check was added in the ReductionService so that if no cycle data is present, then a warning box pops up if the user wants to continue. If they chose to continue, then the resulting reduction gets marked as diagnostic.

To test

Dev testing

Make sure unit tests pass and also see CIS testing.

CIS testing

Open mantid workbench and load the new update_SNAPInstPrm_cycle.py script. There are 3 user settings in the script defined near the top:

  1. DRY_RUN - leave this set to true unless you actually want the script to update the files!
  2. OVERWRITE_EXISTING - leave this to false unless you want to overwrite previous cycle data
  3. CYCLE_METADATA - this is where you define the cycle data and how the SNAPInstPrm.json's get updated. It is a dictionary of dictionaries where each dictionary is keyed on the first run of the cycle, and the value is the rest of the info required to fill out a cycle object.

You should be able to run the script as is and see output showing how the SNAPInstPrm for each version folder found will be updated. Try adding more cycles to the CYCLE_METADATA and make sure the script still works.

To test the cycle continue warning, attempt to run reduction with no cycle data present. You should see a continue warning pop up saying you are missing cycle info. If you click continue and finish reduction, your reduction output workspaces should be marked as diagnostic. Now, try reduction again with valid cycle info, and you should not see a pop up about cycle info and now see that the workspace will not be marked as diagnostic (given that running reduction does not already lead to diagnostic output).

Link to EWM item

EWM#16396

Verification

  • the author has read the EWM story and acceptance critera
  • the reviewer has read the EWM story and acceptance criteria
  • the reviewer certifies the acceptance criteria below reflect the criteria in EWM

Acceptance Criteria

This list is for ease of reference, and does not replace reading the EWM story as part of the review. Verify this list matches the EWM story before reviewing.

  • acceptance criterion 1
  • acceptance criterion 2

@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.50%. Comparing base (be1553c) to head (a7b0fdc).
⚠️ Report is 1 commits behind head on next.

Additional details and impacted files
@@            Coverage Diff             @@
##             next     #682      +/-   ##
==========================================
+ Coverage   96.47%   96.50%   +0.03%     
==========================================
  Files          79       79              
  Lines        7258     7270      +12     
==========================================
+ Hits         7002     7016      +14     
+ Misses        256      254       -2     
Flag Coverage Δ
integration 48.77% <22.22%> (-0.07%) ⬇️
unittests 96.23% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ekapadi ekapadi 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.

So far, there are only some minor issues, with putting stuff in DataFactoryService that should actually be routed from LocalDataService. I also would like you to take a deeper look at the exception flow in these two cases: (Indexer -> InstrumentParameters -> ...) and (<live-data fallback> -> PV-file -> <run metadata>, and etc.). I still am not entirely satisfied that we've completely fixed the misleading messages issue.

For "testing", it would be a huge help for you to please explain how a developer (not a CIS) should test this? I'm probably not going to run the migration script, so I'm at a bit of a loss. I think for this, just explain what I need to do to set up my directories so that they will trigger or not-trigger these changes?

return self.lookupService.readInstrumentConfig(runId)

def getCycleID(self, runNumber: str) -> str:
def cycleInfoExists(self, runNumber: str) -> bool:

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.

SOP: these need to call LocalDataService methods. They shouldn't be ever fully implemented in DataFactoryService. Sometimes the backend might need to access these methods directly, and in that case you now have no way to do that from LocalDataService. Look at the other surrounding methods, and you'll see what the pattern is.

if not success:
metadata = self.readLiveMetadata()
# No local PVFile for `runNumber`, but a live-data connection exists: fall back to the live run.
# If the fallback itself fails, make clear that (1) there's no local data for the requested run,

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 took me a while to figure out, but in the end, I agree with what you've done. Gosh all of this is complicated! :)

self.message = (
"Instrument State for given Run Number is invalid!\n\n"
+ f"{exceptionStr}\n\n"
+ "(See logs for details.)"

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 "see logs for details" statement needs to go away. Can we just put all of the information here? I'm especially fond of the case "see logs for details", and then there's no additional information in the logs -- this is the usual case! :(

@@ -250,6 +261,8 @@ def reduction(self, request: ReductionRequest):
)
isDiagnostic = workspaceMetadata.diffcalState != DiffcalStateMetadata.EXISTS

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.

Can't all of this just be one short-circuit logical statement? I'm not sure that's really better, but try it and see?

# Each instrument-parameter version lives in its own folder:
# <instrument.parameters.home>/v_XXXX/SNAPInstPrm.json
# This walks those folders and, for each file, derives the cycle from the file's own
# `indexEntry.appliesTo` run range:

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.

Please justify why we didn't just extend / fix the original instrument-parameters migration script. This is after all just an instrument-parameters migration?!

self.instance.lookupService.readInstrumentParameters.assert_called_once_with("200")

def test_getCycleID_no_cycle(self):
# Missing cycle info is not an error: a fallback cycle ID is returned so reduction

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.

Same issue as mentioned at DataFactoryService. You are testing what should be LocalDataService methods. For DataFactoryService these would be captured via the generic testing procedure, which automatically triggers the sub-call. (Take a lookk and you'll see what I mean, although it's a bit abstract, and I don't think we use it consistently.)

# A `FileNotFoundError` that isn't a "No PVFile exists" error should be re-raised as-is.
runNumber = "12345"
instance = LocalDataService()
instance._readPVFile = mock.Mock(side_effect=FileNotFoundError("some other file is missing"))

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.

Wouldn't it be incorrect to raise a FileNotFoundError when you're unable to construct the instrument-parameters due to an Indexer error? This is an example of the exception-flow issue that I mentioned previously (in my slack message)!

with pytest.raises(
StateValidationException, match=r"Instrument State for given Run Number is invalid! \(See logs for details\.\)"
):
with pytest.raises(StateValidationException, match=r"Instrument State for given Run Number is invalid!") as excinfo:

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.

YES! Get rid of the stupid "See logs for details". Could we get rid of it everywhere else as well? :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants