Skip to content

Raise Exceptions on Hardware I/O to Set Record Alarms#389

Open
shihab-dls wants to merge 9 commits into
mainfrom
investigate_error_prop
Open

Raise Exceptions on Hardware I/O to Set Record Alarms#389
shihab-dls wants to merge 9 commits into
mainfrom
investigate_error_prop

Conversation

@shihab-dls

@shihab-dls shihab-dls commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Closes #303

This depends on a new release of softioc.

This PR sets and resets alarm states on failed and successful PV puts for CA and PVA.

This addresses the underlying bug in ophyd-async, where a failed command does not raise an error. However, only PVA allows these errors to immediately propagate up to the signal call, otherwise requiring a check on the PV status after the put on CA. Therefore, fastcs-eiger/odin must be converted to use PVA to close the ophyd-async bug.

Summary by CodeRabbit

  • Bug Fixes

    • Failed attribute writes and command executions now propagate errors instead of being silently ignored.
    • EPICS Channel Access and PVAccess records now report major alarms when operations fail and clear alarms after successful operations.
    • Error messages for failed PV operations now provide more consistent details.
    • Improved handling of generated PV value keys for case-sensitive controller items.
  • Tests

    • Added coverage for error propagation and alarm states across EPICS transports.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Attribute write failures now propagate, and EPICS CA/PVA write and command handlers report failures through operation errors and major alarm states. Tests cover callback propagation, SoftIOC command alarms, and PVA put failures.

Changes

EPICS error handling

Layer / File(s) Summary
Failure contract and test fixtures
pyproject.toml, src/fastcs/attributes/attr_w.py, tests/example_softioc.py, tests/test_attributes.py
AttrW.put() re-raises callback failures; test fixtures provide an asynchronously failing command, and related assertions verify propagated exceptions.
EPICS CA alarm handling
src/fastcs/transports/epics/ca/ioc.py, tests/transports/epics/ca/test_softioc_system.py
CA attribute writes and commands clear alarms on success and set MAJOR_ALARM on failure; SoftIOC tests verify command alarm severity.
EPICS PVA value and alarm representation
src/fastcs/transports/epics/pva/pvi.py, src/fastcs/transports/epics/pva/_pv_handlers.py
PVA raw-value keys use lowercased sub-controller names, while alarm posting handles enum, table, waveform, and structured values.
EPICS PVA operation errors
src/fastcs/transports/epics/pva/_pv_handlers.py, tests/transports/epics/pva/test_p4p.py
Failed writes and blocking commands pass formatted errors to op.done(error=...), post major alarms, and are covered by PVA tests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

EPICS CA failure flow

sequenceDiagram
  participant Client
  participant EPICS_CA
  participant AttrW
  participant EPICS_Record
  Client->>EPICS_CA: write PV or command PV
  EPICS_CA->>AttrW: put attribute or run method
  AttrW-->>EPICS_CA: exception
  EPICS_CA->>EPICS_Record: set current value and MAJOR_ALARM
Loading

EPICS PVA failure flow

sequenceDiagram
  participant Client
  participant WritePvHandler
  participant AttrW
  participant PVA
  Client->>WritePvHandler: put value
  WritePvHandler->>AttrW: await put
  AttrW-->>WritePvHandler: exception
  WritePvHandler->>PVA: post major alarm
  WritePvHandler-->>Client: op.done(error)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement error propagation for failed write and command puts with op.done(error=...) handling, matching issue #303.
Out of Scope Changes check ✅ Passed Most edits support the alarm/error-handling work, and the softioc dependency update is explicitly called out as required.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: propagating hardware I/O exceptions so record alarms are set.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch investigate_error_prop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@shihab-dls
shihab-dls force-pushed the investigate_error_prop branch from 25e95c9 to b0466d2 Compare July 15, 2026 14:06
@shihab-dls shihab-dls changed the title wip: add alarms Raise Exceptions on Hardware I/O to Set Record Alarms Jul 15, 2026
@shihab-dls
shihab-dls force-pushed the investigate_error_prop branch from b0466d2 to b2b5e9b Compare July 15, 2026 14:08
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.10526% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.29%. Comparing base (8c54354) to head (f5661f6).

Files with missing lines Patch % Lines
src/fastcs/transports/epics/ca/ioc.py 84.61% 2 Missing ⚠️
src/fastcs/transports/epics/pva/_pv_handlers.py 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #389      +/-   ##
==========================================
+ Coverage   91.25%   91.29%   +0.04%     
==========================================
  Files          72       72              
  Lines        2892     2919      +27     
==========================================
+ Hits         2639     2665      +26     
- Misses        253      254       +1     

☔ 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.

@shihab-dls
shihab-dls force-pushed the investigate_error_prop branch from e2fb84c to 48791cc Compare July 15, 2026 15:51
@shihab-dls
shihab-dls marked this pull request as ready for review July 15, 2026 16:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/transports/epics/pva/test_p4p.py (1)

215-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using an async def test function.

Since other tests in this file are async (e.g., test_command_method), you can define this test as async def to avoid manually interacting with the event loop. This is more idiomatic and prevents potential RuntimeError issues with get_event_loop() in Python 3.12+.

♻️ Proposed refactor to make the test natively async
-def test_alarms_set_if_put_fails(caplog):
+async def test_alarms_set_if_put_fails(caplog):
     `@dataclass`
     class SimpleAttributeIORef(AttributeIORef):
         pass
 
     class SimpleAttributeIO(AttributeIO[DType_T, SimpleAttributeIORef]):
         async def send(self, attr: AttrW[DType_T, SimpleAttributeIORef], value):
             raise ValueError("Failed")
 
     class SomeController(Controller):
         a = AttrW(Int(), io_ref=SimpleAttributeIORef())
 
     controller = SomeController(ios=[SimpleAttributeIO()])
     pv_prefix = str(uuid4())
     fastcs = make_fastcs(pv_prefix, controller)
 
     async def put_pvs():
         await asyncio.sleep(0.1)
         ctxt = Context("pva")
         await ctxt.put(f"{pv_prefix}:A", 1)
 
-    serve = asyncio.ensure_future(fastcs.serve(interactive=False))
-    try:
-        asyncio.get_event_loop().run_until_complete(
-            asyncio.wait_for(asyncio.gather(serve, put_pvs()), timeout=1)
-        )
-    except RuntimeError as exc:
-        assert str(exc) == "Exception raised during put operation: ValueError('Failed')"
-    serve.cancel()
+    serve = asyncio.create_task(fastcs.serve(interactive=False))
+    try:
+        await asyncio.wait_for(asyncio.gather(serve, put_pvs()), timeout=1)
+    except RuntimeError as exc:
+        assert str(exc) == "Exception raised during put operation: ValueError('Failed')"
+    finally:
+        serve.cancel()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/transports/epics/pva/test_p4p.py` around lines 215 - 245, Convert
test_alarms_set_if_put_fails to an async test and await the concurrent serve and
put_pvs tasks directly, following the pattern used by test_command_method.
Remove the manual get_event_loop().run_until_complete and wait_for usage while
preserving the existing timeout, exception assertion, and serve cancellation
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyproject.toml`:
- Around line 37-40: Update the epicsca dependency list in pyproject.toml to
replace the softioc Git master reference with a released softioc version
specifier, preserving the existing pvi dependency and ensuring installs use the
stable published package.

In `@src/fastcs/transports/epics/ca/ioc.py`:
- Around line 231-239: Update both write PV and command PV callbacks in
src/fastcs/transports/epics/ca/ioc.py at lines 231-239 and 291-299: bind caught
exceptions explicitly, log each with the existing logger and relevant PV context
before setting the MAJOR_ALARM record state, and avoid blind exception catches.

---

Nitpick comments:
In `@tests/transports/epics/pva/test_p4p.py`:
- Around line 215-245: Convert test_alarms_set_if_put_fails to an async test and
await the concurrent serve and put_pvs tasks directly, following the pattern
used by test_command_method. Remove the manual
get_event_loop().run_until_complete and wait_for usage while preserving the
existing timeout, exception assertion, and serve cancellation behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3502c74e-caf1-455e-af90-0199f1affa9b

📥 Commits

Reviewing files that changed from the base of the PR and between 8c54354 and ed6398c.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • pyproject.toml
  • src/fastcs/attributes/attr_w.py
  • src/fastcs/transports/epics/ca/ioc.py
  • src/fastcs/transports/epics/pva/_pv_handlers.py
  • tests/example_softioc.py
  • tests/test_attributes.py
  • tests/transports/epics/ca/test_softioc_system.py
  • tests/transports/epics/pva/test_p4p.py

Comment thread pyproject.toml
Comment on lines +37 to +40
epicsca = [
"pvi~=0.12.0",
"softioc @ git+https://github.com/DiamondLightSource/pythonSoftIOC.git@master",
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,120p' pyproject.toml

Repository: DiamondLightSource/fastcs

Length of output: 3639


🏁 Script executed:

rg -n "epicsca|softioc|pythonSoftIOC|master" .

Repository: DiamondLightSource/fastcs

Length of output: 5315


🌐 Web query:

pythonSoftIOC PyPI release softioc stable version

💡 Result:

As of July 15, 2026, the latest stable release of the pythonSoftIOC library (available on PyPI as softioc) is version 4.7.0, which was released on January 14, 2026 [1][2]. pythonSoftIOC is a library that allows an EPICS (Experimental Physics and Industrial Control System) IOC (Input/Output Controller) with Python device support to be run directly from within a Python interpreter [3][4]. It enables the programmatic creation of EPICS records and allows arbitrary Python code to update those records and respond to channel access requests [3][5]. Key details include: - PyPI Project Name: softioc [3] - Latest Stable Version: 4.7.0 [1][2] - Primary Repository: DiamondLightSource/pythonSoftIOC (maintained by Diamond Light Source) [6][5] - Documentation: Available on the official GitHub Pages site [7][8] You can install the library using pip: pip install softioc For further details regarding changes in specific versions, you can consult the project's official changelog [1][2].

Citations:


Pin softioc to a released version before merging. fastcs[epicsca] currently depends on softioc @ git+https://github.com/DiamondLightSource/pythonSoftIOC.git@master, which makes installs non-deterministic for anyone using the extra. softioc has a stable release available, so this should use a version specifier instead of a branch ref.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 37 - 40, Update the epicsca dependency list in
pyproject.toml to replace the softioc Git master reference with a released
softioc version specifier, preserving the existing pvi dependency and ensuring
installs use the stable published package.

Comment thread src/fastcs/transports/epics/ca/ioc.py Outdated
Comment on lines +231 to +239
try:
await attribute.put(cast_from_epics_type(attribute.datatype, value))
except Exception:
record.set(
record.get(),
process=False,
severity=alarm.MAJOR_ALARM,
alarm=alarm.MAJOR_ALARM,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Log caught exceptions to prevent silent failures.

Both the write PV and command PV update callbacks catch Exception to set alarm states but do not log the exception. This causes the error details to be completely swallowed, making it difficult to debug why an EPICS major alarm was triggered.

  • src/fastcs/transports/epics/ca/ioc.py#L231-L239: catch the exception explicitly (except Exception as e:) and log it before setting the record state (e.g., logger.opt(exception=e).error("Exception during PV put", pv=pv)).
  • src/fastcs/transports/epics/ca/ioc.py#L291-L299: catch the exception explicitly (except Exception as e:) and log it before setting the record state (e.g., logger.opt(exception=e).error("Exception during command PV put", pv=pv)).

As per static analysis hints, do not catch a blind exception.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 233-233: Do not catch blind exception: Exception

(BLE001)

📍 Affects 1 file
  • src/fastcs/transports/epics/ca/ioc.py#L231-L239 (this comment)
  • src/fastcs/transports/epics/ca/ioc.py#L291-L299
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fastcs/transports/epics/ca/ioc.py` around lines 231 - 239, Update both
write PV and command PV callbacks in src/fastcs/transports/epics/ca/ioc.py at
lines 231-239 and 291-299: bind caught exceptions explicitly, log each with the
existing logger and relevant PV context before setting the MAJOR_ALARM record
state, and avoid blind exception catches.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fastcs/transports/epics/pva/_pv_handlers.py (1)

51-64: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not clear alarms before the hardware put succeeds.

The new default p4p_alarm_states() is posted before await self._attr_w.put(cast_value), so an existing major alarm is temporarily cleared while I/O is still pending. A stalled or cancelled operation can leave the PV falsely healthy; post the value/reset alarm only in the success path, or preserve the current alarm until the operation completes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fastcs/transports/epics/pva/_pv_handlers.py` around lines 51 - 64, Move
the normal value/alarm post in the handler around the put operation so it occurs
only after await self._attr_w.put(cast_value) succeeds. Preserve the existing
alarm state while the put is pending or fails, and keep the exception path’s
error alarm posting and op.done(error=...) behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fastcs/transports/epics/ca/ioc.py`:
- Around line 234-235: Update the write PV on_update callback at
src/fastcs/transports/epics/ca/ioc.py lines 234-235 to bind the caught exception
and log its details before setting the MAJOR alarm. Apply the same explicit
exception binding and logging in the command PV wrapped_method callback at lines
290-291, preserving the existing alarm behavior.

---

Outside diff comments:
In `@src/fastcs/transports/epics/pva/_pv_handlers.py`:
- Around line 51-64: Move the normal value/alarm post in the handler around the
put operation so it occurs only after await self._attr_w.put(cast_value)
succeeds. Preserve the existing alarm state while the put is pending or fails,
and keep the exception path’s error alarm posting and op.done(error=...)
behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3f2f9465-f2a4-4206-a3bb-21b55a1f826e

📥 Commits

Reviewing files that changed from the base of the PR and between ed6398c and 494eed7.

📒 Files selected for processing (4)
  • src/fastcs/transports/epics/ca/ioc.py
  • src/fastcs/transports/epics/pva/_pv_handlers.py
  • src/fastcs/transports/epics/pva/pvi.py
  • src/fastcs/transports/epics/pva/types.py

Comment on lines +234 to +235
except Exception:
_set_alarm(record, alarm.MAJOR_ALARM)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Log caught exceptions to prevent silent failures.

Both the write PV and command PV update callbacks catch Exception to set alarm states but do not log the exception. This causes the error details to be completely swallowed, making it difficult to debug why an EPICS major alarm was triggered.

  • src/fastcs/transports/epics/ca/ioc.py#L234-L235: catch the exception explicitly (except Exception as e:) and log it in the on_update callback.
  • src/fastcs/transports/epics/ca/ioc.py#L290-L291: catch the exception explicitly (except Exception as e:) and log it in the wrapped_method callback.
🐛 Proposed fixes

For on_update (Write PV):

-        except Exception:
+        except Exception as e:
+            logger.opt(exception=e).error("Exception during PV put", pv=pv)
             _set_alarm(record, alarm.MAJOR_ALARM)

For wrapped_method (Command PV):

-        except Exception:
+        except Exception as e:
+            logger.opt(exception=e).error("Exception during command PV put", pv=pv)
             _set_alarm(record, alarm.MAJOR_ALARM)
🧰 Tools
🪛 Ruff (0.15.21)

[warning] 234-234: Do not catch blind exception: Exception

(BLE001)

📍 Affects 1 file
  • src/fastcs/transports/epics/ca/ioc.py#L234-L235 (this comment)
  • src/fastcs/transports/epics/ca/ioc.py#L290-L291
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fastcs/transports/epics/ca/ioc.py` around lines 234 - 235, Update the
write PV on_update callback at src/fastcs/transports/epics/ca/ioc.py lines
234-235 to bind the caught exception and log its details before setting the
MAJOR alarm. Apply the same explicit exception binding and logging in the
command PV wrapped_method callback at lines 290-291, preserving the existing
alarm behavior.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fastcs/transports/epics/pva/_pv_handlers.py`:
- Around line 192-199: Update _set_alarm to avoid mutating alarm_states["alarm"]
in place by copying the nested alarm dictionary. Only remove "status" from that
copy for Table or Waveform datatypes, then call pv.post with one positional,
properly nested dictionary containing the value and alarm structure; preserve
the existing alarm_states structure for other datatypes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e97e013-1f54-4c51-b55d-88e09ab22a43

📥 Commits

Reviewing files that changed from the base of the PR and between 494eed7 and a0be859.

📒 Files selected for processing (1)
  • src/fastcs/transports/epics/pva/_pv_handlers.py

Comment thread src/fastcs/transports/epics/pva/_pv_handlers.py Outdated
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.

Implement op.done(error="msg") for failed put operations on command and write PVs

1 participant