Skip to content

Handling exceptions when applications do not report errors but time out in stateful transitions#868

Open
PawelPlesniak wants to merge 84 commits into
developfrom
PawelPlesniak/IncompleteStatefulCommandTransition
Open

Handling exceptions when applications do not report errors but time out in stateful transitions#868
PawelPlesniak wants to merge 84 commits into
developfrom
PawelPlesniak/IncompleteStatefulCommandTransition

Conversation

@PawelPlesniak

@PawelPlesniak PawelPlesniak commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes #803
Fixes #911

If a segment does not reach the target state, it is marked as in error, and the timeout is logged in the relevant server.
Also defines a set of configurations constructed to fail, and defines a set of unit tests to demonstrate this behaviour.
Error recovery with the supervisor will address what happens if an application completes this outside of the designated window. This is defined in #840

Type of change

  • New feature / enhancement
  • Optimization
  • Bug fix
  • Breaking change
  • Documentation

List of required branches from other repositories

Requires DUNE-DAQ/druncschema#87

Change log

Addressed issues

Now when an application does not complete its transition in the allocated timeout, the session is put into an error state.
Defines a new click command log that allows the user to send a message to the relevant controller or process manager to put into their log file with a selectable severity level.

Other new features

When running in batch mode, the user can start the run with --no-stop-error-batch-mode. Should an error state be encountered, the session is not terminated, and commands can continue to be executed. This is primarily intended for use with integration tests, such that we can inspect the ps and status tables.

Integration tests for failure modes

Defines a set of intentionally failing configurations in config/tests/failure-testing.data.xml, which contain configurations with a set of fake-daq-apps that fail at configurable points in the session lifecycle. These configurations are

  • ft-death-on-boot-nest-app - this kills a nested application (2 segments deep) on boot.
  • ft-death-on-boot-top-app - this kills the top application on boot.
  • ft-death-post-boot-nest-app - this kills a nested application (2 segments deep) after boot, before applications are marked as ready.
  • ft-death-post-boot-top-app - this kills the top application after boot, before applications are marked as ready.
  • ft-fsm-cmd-timeout-nest-app - this times out an FSM transition on a nested application.
  • ft-fsm-cmd-timeout-top-app - this times out an FSM transition on the top application.
  • ft-fsm-cmd-death-nest-app - this kills a nested fake daq app during a FSM transition.
  • ft-fsm-cmd-death-top-app - this kills the top fake daq app during a FSM transition.

The differences between the on-boot and post-boot are useful to see, as the post-boot configs allow the applications that die to register themselves on the connectivity service before dying, hence there is different behaviour.

The differences between the nest-app and top-app allow the user to visualize the effect of a nested hierarchy on the control tree, and how the releveant children applications register themselves to the segment parents. The user shoudl expect to see the majority of the apps in the status table on boot when using a nest config, and only the top two apps when using the top config.

Note that these configurations use the FSMconfiguration_noAction state machine, and will not behave fulliy in the standard way. An example includes not being able to reach the running state as there is no run number allocated, which will be reported as a generic gRPC error.

These tests have been integrated into the unit test framework, which can be executed as (from the local drunc root)

source scripts/setup_drunc_config_path.sh
pytest src/drunc/integtest/<integ_test_file_name>

It is recommended for the tester to use --integtest-verbosity 5 to see what is happening during the process, and if more details are wanted from the pytest side, the user can run the tests as pytest -s to show what would be printed.

Suggested manual testing checklist

Run each of the integration tests as defined above.

Developer checklist

Prior to marking this as "Ready for Review"

Tests ran on: np04-srv-029 from release NFD_DEV_260709_A9.

Unit tests - some tests can't be ran on the CI. This is documented. If this PR checks a feature that can't be tested with CI, this has been marked appropriately.

Integration tests - the daqsystemtest_integtest_bundle requires a lot of resources, and connections to the EHN1 infrastructure. Check the cross referenced list if you can't run these. The developer needs to run at least the .

  • Unit tests (pytest --marker) passed
    • With relevant marker
    • Without marker
  • Integration tests passed
    • Only daqsystemtest_integtest_bundle.sh -k minimal_system_quick_test.py
    • Full daqsystemtest_integtest_bundle.sh
  • Testing skipped as there are no core code changes in this PR, this only relates to documentation/CI workflows

Final checklist prior to marking this as "Ready for Review"

  • Code is clearly commented.
  • New unit tests have been added, or is documented in # ISSUE NUMBER
  • A suitable reviewer has been chosen from this list.

Reviewer checklist

  • This branch has been rebased with develop prior to testing.
  • Suggested manual tests show changes.
  • CI workflows fails documented (if present)
  • Integration tests passed
    • Only concern yourself if failures related to drunc are in the log files
    • If non-drunc failure appears:
      • Validate failure in fresh working area
      • Contact Pawel if unsure

Once the features are validated and both the unit and integration tests pass, the PRs is ready to be merged.

Prior to merging

Choose one of the following an complete all substeps
  • Changes only affect the Run Control, are in a single repository, and do not affect the end user.
    • Changes are documented in docstrings and code comments
    • Wiki has been updated if architectural or endpoint changes
  • Otherwise
    • Workflow changes demonstrated in the Change Log (if necessary)
    • Wiki has been updated (if necessary)
    • #daq-sw-librarians Slack channel notified (see below)

Once completed, the reviewer can merge the PR.

Notification message for a Slack channel

Note - this should be to #dunedaq-integration for general workflow that isn't during a release candidate period, and to #daq-release-prep otherwise.

For an single merge that changes the user workflow

The CCM WG has an isolated PR ready to merge that affects user workflows. The PR is:

_URL_

I will leave time for any comments, otherwise will merge these at the end of the work day _Insert your time zone_.

For co-ordinated merge

The CCM WG has a set of co-ordinated merges ready to merge. The PRs are:

_URL_

_URL_


I will leave time for any comments, otherwise will merge these at the end of the day.

@PawelPlesniak

Copy link
Copy Markdown
Collaborator Author
image In the case where a second application also fails to complete a transition in time, the same error gets thrown. This is likely caused by the nested structure, and the fact that there are multiple layers to this configuration. A robust solution to this problem will take longer to achieve, but I will continue working on it.

@PawelPlesniak PawelPlesniak changed the title Generating an environment for which the issue can be recreated Handling exceptions when applications do not report errors but time out in stateful transitions Mar 31, 2026
@PawelPlesniak
PawelPlesniak changed the base branch from prep-release/fddaq-v5.6.0 to develop June 4, 2026 16:05
@PawelPlesniak

PawelPlesniak commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author
  • Log files do not contain wierd characters from redirecting rich ASCII output to a file with colors
  • Top app config failure not working?

@PawelPlesniak

PawelPlesniak commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author
  • Stream handler has no logging time zone, file name, line number, etc?
  • Duplicate logs of apps failing
  • Remove old logs used for debugging
  • Add druncschema dependency to this PR log
  • Integrate the failure mode testing into the unit tests

@PawelPlesniak

Copy link
Copy Markdown
Collaborator Author

Integration tests passed on np04-srv-029

======================== 18 passed in 293.70s (0:04:53) ========================

+++++++++++++++++++++++++++++++++++++++++++++++++
++++++++++++++++++++ SUMMARY ++++++++++++++++++++
+++++++++++++++++++++++++++++++++++++++++++++++++

Thu Jul 16 03:15:52 PM CEST 2026
Log file is: /tmp/pytest-of-pplesnia/dunedaq_integtest_bundle_20260716143520.log

⮕ Running daqsystemtest/3ru_1df_multirun_test.py ⬅
======================== 6 passed ✅ in 244.30s (0:04:04) =========================
⮕ Running daqsystemtest/3ru_3df_multirun_test.py ⬅
======================== 6 passed ✅ in 243.98s (0:04:03) =========================
⮕ Running daqsystemtest/disabled_tpg_test.py ⬅
============================== 3 passed ✅ in 59.25s ==============================
⮕ Running daqsystemtest/example_system_test.py ⬅
======================== 12 passed ✅ in 234.24s (0:03:54) ========================
⮕ Running daqsystemtest/fake_data_producer_test.py ⬅
======================== 6 passed ✅ in 235.73s (0:03:55) =========================
⮕ Running daqsystemtest/long_window_readout_test.py ⬅
============================== 1 skipped 🟡 in 1.52s ==============================
⮕ Running daqsystemtest/minimal_system_quick_test.py ⬅
============================== 4 passed ✅ in 53.46s ==============================
⮕ Running daqsystemtest/readout_type_scan_test.py ⬅
======================== 33 passed ✅ in 576.37s (0:09:36) ========================
⮕ Running daqsystemtest/sample_ehn1_multihost_test.py ⬅
======================== 4 skipped 🟡 in 84.05s (0:01:24) =========================
⮕ Running daqsystemtest/small_footprint_quick_test.py ⬅
============================== 3 passed ✅ in 51.67s ==============================
⮕ Running daqsystemtest/tpg_state_collection_test.py ⬅
======================== 5 passed ✅ in 107.56s (0:01:47) =========================
⮕ Running daqsystemtest/tpreplay_test.py ⬅
======================== 6 passed ✅ in 118.26s (0:01:58) =========================
⮕ Running daqsystemtest/tpstream_writing_test.py ⬅
======================== 4 passed ✅ in 110.07s (0:01:50) =========================
⮕ Running daqsystemtest/trigger_bitwords_test.py ⬅
======================== 18 passed ✅ in 293.70s (0:04:53) ========================

@PawelPlesniak
PawelPlesniak marked this pull request as ready for review July 16, 2026 13:19
@PawelPlesniak
PawelPlesniak requested a review from emmuhamm July 16, 2026 13:19
@PawelPlesniak

Copy link
Copy Markdown
Collaborator Author

Duplicate code is present in develop between send_msg and log_on_server, resolving this to log_on_server, see #888 for more details

@PawelPlesniak

Copy link
Copy Markdown
Collaborator Author

@emmuhamm should be good to go

@emmuhamm

Copy link
Copy Markdown
Member

Note that for the review process, this PR and the dependency is up to date with develop as of today's (23 june) nightly.

A few changes that were implemented are porting over some of the new functionality that now exists in develop (particularly about how all lines should have stripped_ansi now) and minor changes to get some of the other tests working. Including increasing the widths of the table. as well as changing the column names.

I'll proceed to give comments, but this may come in bits and pieces

@emmuhamm emmuhamm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review 1/?

Ran out of bandwidth for now but this should be most of my comments for the testing side of things.

Pretty minor comments for this PR, some could be defferred for a bigger issue. Importantly, all the tests run just fine

Comment on lines +30 to +33
pytestmark = pytest.mark.skipif(
drunc_missing,
reason="drunc is not present in DUNEDAQ_DB_PATH, skipping drunc integration tests",
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This error message should also say how to fix this. (this applied to all the files)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a good point to raise. However, the way in which the configuration file is packaged for retrieval by the integration tests defined within python-only DD repos is still under discussion. Please leave this unresolved so this can be addressed ASAP.

Comment on lines +25 to +27
drunc_missing = not any(
"drunc" == segment for path in db_path_env.split(":") for segment in path.split("/")
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For posterity: why is this necessary?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the way to check if drunc is included as part of the DUNEDAQ_DB_PATH env var, so that the path to the configuration file can actually be matched (and the session can be run). This need to be explicitly stated as equality, as initial implementations matched this for druncschema, which did caused failure of the integration tests (the session wouldn't run as it wouldn't find the config, and would just print a stack of errors)

Comment thread src/drunc/integtest/test_failure_mode_fsm_cmd_death_top_app.py
Comment on lines +115 to +117
except FileNotFoundError:
print(f"Error: {file} not found.")
return False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we want to return false here or throw? Feels like in a testing environment if we cant find the file it should throw

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This should be left as a boolean return, mainly for cleaner output. The way that the test is currently structured is on a query base, where we check whether the line exists, and if it does not, then we return. Both can achieve the same functionaltiy, but I think this is cleaner. Should this fail in the future, when running with pytest -s (-s allows seeing the print statements in the tests), the error will be a one line solution, rather than a stacktrace.

Comment on lines 294 to 304
_PS_COLUMNS = ["session", "friendly_name", "user", "host", "uuid", "alive", "exit_code"]
_STATUS_COLUMNS = [
"name",
"info",
"state",
"substate",
"in_error",
"included",
"endpoint",
]
_EXEC_REPORT_COLUMNS = ["name", "command_execution", "fsm_transition"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

After the big developments of this tests, im starrting to think we should improve this part. Specifically, the columns themselves are called in the tests 'eg check if [substate] exists'

We should definitely strive to improve this, but this is beyond the scope of this PR.

If you agree, can convert this to an issue?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sorry the question here is unclear to me, what would you propose changing with these variables?

Comment on lines +402 to +415
#! Replace this with a generic one
def get_rows_by_friendly_name_from_ps_table(
ps_table: list[dict[str, str]], friendly_name: str
) -> list[dict[str, str]]:
"""Return all rows whose `friendly_name` matches exactly after stripping."""
return [row for row in ps_table if row["friendly_name"].strip() == friendly_name]


def get_rows_by_name_from_status_table(
status_table: list[dict[str, str]], name: str
) -> list[dict[str, str]]:
"""Return all rows whose `Name` matches exactly after stripping."""
return [row for row in status_table if row["name"].strip() == name]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The two functions are nearly identical. Suggest

def get_rows_by_name_from_table(table: list[dict[str,str]], name:str, row_name : str) -> list:
    return [row for row in table if row[row_name].strip() == name]

Would this be easy to implement? If not, happy to fold this into the issue i mentioned above about the columns

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The issue is that the ps table lists the process name as friendly_name, and the status table lists the process name as name. I think we could do this, but this should be a separate PR, including changing the actual ps table process name to just name and not friendly_name

Comment thread src/drunc/integtest/test_failure_mode_death_on_boot_nest_app.py Outdated
@PawelPlesniak PawelPlesniak added this to the fddaq-v5.7.0 milestone Jul 23, 2026
@PawelPlesniak

Copy link
Copy Markdown
Collaborator Author

Thanks @emmuhamm I will look tomorrow
Note to self - requested from @MRiganSUSX , list the application that did not complete in time, and tell the user to look at the logs of that specific one

Co-authored-by: Emir Muhammad <49058518+emmuhamm@users.noreply.github.com>

@PawelPlesniak PawelPlesniak left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, for some reason parts of these comments were just comment replies, others were a second review.

Comment on lines 294 to 304
_PS_COLUMNS = ["session", "friendly_name", "user", "host", "uuid", "alive", "exit_code"]
_STATUS_COLUMNS = [
"name",
"info",
"state",
"substate",
"in_error",
"included",
"endpoint",
]
_EXEC_REPORT_COLUMNS = ["name", "command_execution", "fsm_transition"]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sorry the question here is unclear to me, what would you propose changing with these variables?

Comment on lines +402 to +415
#! Replace this with a generic one
def get_rows_by_friendly_name_from_ps_table(
ps_table: list[dict[str, str]], friendly_name: str
) -> list[dict[str, str]]:
"""Return all rows whose `friendly_name` matches exactly after stripping."""
return [row for row in ps_table if row["friendly_name"].strip() == friendly_name]


def get_rows_by_name_from_status_table(
status_table: list[dict[str, str]], name: str
) -> list[dict[str, str]]:
"""Return all rows whose `Name` matches exactly after stripping."""
return [row for row in status_table if row["name"].strip() == name]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The issue is that the ps table lists the process name as friendly_name, and the status table lists the process name as name. I think we could do this, but this should be a separate PR, including changing the actual ps table process name to just name and not friendly_name

Comment on lines +115 to +117
except FileNotFoundError:
print(f"Error: {file} not found.")
return False

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This should be left as a boolean return, mainly for cleaner output. The way that the test is currently structured is on a query base, where we check whether the line exists, and if it does not, then we return. Both can achieve the same functionaltiy, but I think this is cleaner. Should this fail in the future, when running with pytest -s (-s allows seeing the print statements in the tests), the error will be a one line solution, rather than a stacktrace.

PawelPlesniak added 3 commits July 24, 2026 17:13
…ithub.com:DUNE-DAQ/drunc into PawelPlesniak/IncompleteStatefulCommandTransition

@emmuhamm emmuhamm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hello hello here's another round of reviews. Gone through most of the log code and mostly appears fine. Only got a few comments on that bit

log = get_logger("unified_shell.log_on_server")
log.debug("Logging message to server(s)...")

if target_server in ["", "process_manager"]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

log --target-server df-controller "hello3"
I would expect this to send a hello3 to the 'df-controller' server. But this doesnt work.

Maybe a smarter system that can distribute it?

(Maybe beyond the scope of this PR but useful to think about)

Comment on lines +613 to +620
def log_on_server(
self,
text: str,
severity: str = "INFO",
target: str = "",
execute_along_path: bool = False,
execute_on_all_subsequent_children_in_path: bool = True,
) -> LogOnServerResponse:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Docstring

flag=ResponseFlag.EXECUTED_SUCCESSFULLY,
)

def log_on_server(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Docstring

execute_on_all_subsequent_children_in_path: bool,
extended: bool,
) -> None:
obj.log.warning(f"Getting status for target '{target}'...")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this a warning?

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

Labels

None yet

Projects

None yet

4 participants