Acquisition errors return to Idle instead of Error state
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m8s
Build Packages / build:viewer-tgz:cuda (push) Successful in 7m38s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 12m56s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 12m57s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m35s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 13m40s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 13m59s
Build Packages / build:windows:nocuda (push) Successful in 16m4s
Build Packages / build:windows:cuda (push) Successful in 18m16s
Build Packages / build:rpm (rocky8) (push) Successful in 11m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 12m34s
Build Packages / XDS test (durin plugin) (push) Successful in 7m11s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m4s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m41s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m44s
Build Packages / build:rpm (rocky9) (push) Successful in 13m0s
Build Packages / DIALS test (push) Successful in 13m18s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m43s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m33s
Build Packages / Unit tests (push) Successful in 1h1m27s

Going to the Error state after any acquisition failure forced an expensive
re-initialisation (reconnect + full detector config + pedestals), even when the
detector was still perfectly configured and only a transient problem occurred.

Split failures into two classes:
 - ordinary acquisition failures (receiver/writer/analysis problems, missed
   packets, writer disconnect) return to Idle with an Error-severity message, so
   the run can be retried without re-initialising;
 - failures that leave the detector in an undefined state throw the new
   JFJochCriticalException and still go to Error, forcing re-initialisation.

The detector layer is the single source of truth for "detector faulted":
SLSDetectorWrapper::InternalStop and DectrisDetectorWrapper::CheckBusyOrError
raise JFJochCriticalException on the ERROR state, and JFJochServices::Stop raises
it when the detector does not return to idle at the end of a run. MeasurementThread
dispatches purely on exception type - no detector-status field checks.

Also:
 - JFJochServices::Start now stops the receiver it launched if the detector fails
   to start, so the service returns to idle cleanly and the run can be retried.
 - A synchronous Start rethrows the failure to the caller (HTTP layer) via a
   stored exception_ptr, so a failed start is no longer reported as HTTP 200.
 - JFJochServices::Stop no longer throws on a writer error; it is reported through
   receiver_output.writer_err and surfaced as an Idle + Error-severity message,
   which makes the previously-dead writer_err branch in MeasurementThread live.

Initialisation, calibration/pedestal, SelectDetector and Deactivate keep the
Error-state behaviour, as those genuinely leave the system in an undefined state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 14:10:48 +02:00
co-authored by Claude Opus 4.8
parent d6389e12da
commit 0a28da7c2e
6 changed files with 85 additions and 25 deletions
+32 -18
View File
@@ -15,18 +15,35 @@ void JFJochServices::Start(const DiffractionExperiment& experiment,
cannot_stop_detector = false;
if (receiver != nullptr) {
logger.Info(" ... receiver start");
if (image_puller)
image_puller->ResumeAndClear();
receiver->Start(experiment, pixel_mask, calibration, image_puller);
{
std::shared_lock ul(detector_mutex);
if (detector && !experiment.IsUsingInternalPacketGen()) {
logger.Info(" ... detector start");
detector->Start(experiment);
}
if (receiver == nullptr) {
logger.Info(" Done!");
return;
}
logger.Info(" ... receiver start");
if (image_puller)
image_puller->ResumeAndClear();
receiver->Start(experiment, pixel_mask, calibration, image_puller);
// From here the receiver is running asynchronously. If starting the detector fails, stop the
// receiver again so the service returns to idle and the run can be retried without
// re-initialising. The detector failure is then propagated (a critical detector error stays
// critical, so the broker still goes to the Error state).
try {
std::shared_lock ul(detector_mutex);
if (detector && !experiment.IsUsingInternalPacketGen()) {
logger.Info(" ... detector start");
detector->Start(experiment);
}
} catch (const std::exception &e) {
logger.Error(" ... detector failed to start ({}) - stopping receiver", e.what());
receiver->Cancel(false);
try {
receiver->Stop();
} catch (const std::exception &stop_error) {
logger.Warning("Receiver stop after failed start reported: {}", stop_error.what());
}
throw;
}
logger.Info(" Done!");
@@ -123,12 +140,9 @@ JFJochServicesOutput JFJochServices::Stop() {
}
}
// A writer that broke mid-run (e.g. a lost connection) leaves a truncated file.
// Surface that as a failed acquisition instead of silently reporting success.
if (!ret.receiver_output.writer_err.empty())
throw JFJochException(JFJochExceptionCategory::FileWriteError,
"Writer error, written data may be incomplete: "
+ ret.receiver_output.writer_err);
// A writer that broke mid-run (e.g. a lost connection) leaves a truncated file. This is
// reported to the caller via receiver_output.writer_err (the state machine surfaces it as
// an error message) - it is not a detector fault, so it does not need re-initialisation.
logger.Info(" ... finished with success");
} catch (const JFJochException &e) {
@@ -146,7 +160,7 @@ JFJochServicesOutput JFJochServices::Stop() {
throw JFJochException(*exception);
if (detector_error)
throw JFJochException(JFJochExceptionCategory::Detector, "Error in detector operation");
throw JFJochCriticalException("Error in detector operation");
return ret;
}
+36 -4
View File
@@ -7,6 +7,7 @@
#include "../preview/JFJochTIFF.h"
#include "../common/CUDAWrapper.h"
#include "../common/GitInfo.h"
#include "../common/JFJochException.h"
JFJochStateMachine::JFJochStateMachine(const DiffractionExperiment& in_experiment,
JFJochServices &in_services,
@@ -361,10 +362,19 @@ void JFJochStateMachine::Start(const DatasetSettings &settings, bool async) {
experiment.IncrementRunNumber();
start_exception = nullptr;
SetState(JFJochState::Busy, "Preparing measurement", BrokerStatus::MessageSeverity::Info);
measurement = std::async(std::launch::async, &JFJochStateMachine::MeasurementThread, this);
if (!async)
if (!async) {
c.wait(ul, [&]() { return state != JFJochState::Busy; });
// A synchronous start propagates the failure to the caller. The state has already been set
// by MeasurementThread (Idle for an ordinary failure, Error for a critical detector fault).
if (start_exception) {
auto e = start_exception;
start_exception = nullptr;
std::rethrow_exception(e);
}
}
}
BrokerStatus JFJochStateMachine::WaitTillNotBusy(std::chrono::milliseconds timeout) {
@@ -392,12 +402,26 @@ void JFJochStateMachine::MeasurementThread() {
SetState(JFJochState::Measuring, "Measuring ...", BrokerStatus::MessageSeverity::Info);
}
c.notify_all();
} catch (std::exception &e) {
} catch (const JFJochCriticalException &e) {
// Detector left in an undefined state - force re-initialisation via the Error state.
logger.Error("Critical error starting measurement: {}", e.what());
{
std::unique_lock ul(m);
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
start_exception = std::current_exception();
}
c.notify_all();
return;
} catch (const std::exception &e) {
// Ordinary acquisition failure - the detector is still configured/calibrated, so return to
// Idle and let the user retry without re-initialising. services.Start has already stopped the
// receiver it launched.
logger.Error("Error starting measurement: {}", e.what());
{
std::unique_lock ul(m);
SetState(JFJochState::Idle, e.what(), BrokerStatus::MessageSeverity::Error);
start_exception = std::current_exception();
}
services.Cancel();
c.notify_all();
return;
}
@@ -442,9 +466,17 @@ void JFJochStateMachine::MeasurementThread() {
"Data collection without problems",
BrokerStatus::MessageSeverity::Success);
}
} catch (const std::exception &e) {
} catch (const JFJochCriticalException &e) {
// Detector faulted during the run - it needs re-initialisation, so go to the Error state.
logger.Error("Critical error finishing measurement: {}", e.what());
std::unique_lock ul(m);
SetState(JFJochState::Error, e.what(), BrokerStatus::MessageSeverity::Error);
} catch (const std::exception &e) {
// Receiver/writer problem - the data may be incomplete, but the detector is still usable, so
// return to Idle rather than forcing re-initialisation.
logger.Error("Error finishing measurement: {}", e.what());
std::unique_lock ul(m);
SetState(JFJochState::Idle, e.what(), BrokerStatus::MessageSeverity::Error);
}
c.notify_all();
}
+3
View File
@@ -7,6 +7,7 @@
#include <mutex>
#include <future>
#include <optional>
#include <exception>
#include "../common/DiffractionExperiment.h"
#include "../jungfrau/JFCalibration.h"
@@ -99,6 +100,8 @@ class JFJochStateMachine {
PixelMask pixel_mask;
int64_t current_detector_setup; // Lock only on change
std::optional<ScanResult> scan_result;
// Set by MeasurementThread when a synchronous Start fails, so Start() can rethrow to the caller
std::exception_ptr start_exception;
mutable std::mutex calibration_statistics_mutex;
std::vector<JFCalibrationModuleStatistics> calibration_statistics;
+10
View File
@@ -155,6 +155,16 @@ public:
: JFJochException(JFJochExceptionCategory::WrongDAQState, description) {}
};
// Thrown when an operation leaves a subsystem (typically the detector) in an undefined state that a
// simple retry cannot recover from - the broker must re-initialise. The state machine catches this
// separately and goes to the Error state, whereas ordinary acquisition failures return to Idle.
class JFJochCriticalException : public JFJochException {
public:
explicit JFJochCriticalException(const std::string &description,
JFJochExceptionCategory in_category = JFJochExceptionCategory::Detector)
: JFJochException(in_category, description) {}
};
class PCIeDeviceException : public JFJochException {
public:
explicit PCIeDeviceException(const std::string &description)
+1 -2
View File
@@ -48,8 +48,7 @@ void DectrisDetectorWrapper::CheckBusy() {
void DectrisDetectorWrapper::CheckBusyOrError() {
switch(GetState()) {
case DetectorState::ERROR:
throw JFJochException(JFJochExceptionCategory::Detector,
"Detector in error state");
throw JFJochCriticalException("Detector in error state");
case DetectorState::BUSY:
case DetectorState::WAITING:
throw JFJochException(JFJochExceptionCategory::Detector,
+3 -1
View File
@@ -217,6 +217,8 @@ void SLSDetectorWrapper::Start(const DiffractionExperiment& experiment) {
det.setNumberOfTriggers(experiment.GetNumTriggers());
det.startDetector();
} catch (const JFJochException &) {
throw; // already a categorised JFJoch exception (incl. critical detector error) - keep its type
} catch (std::exception &e) {
logger.ErrorException(e);
throw JFJochException(JFJochExceptionCategory::Detector, e.what());
@@ -229,7 +231,7 @@ void SLSDetectorWrapper::InternalStop() {
// Assume it is executed in try-catch!
auto state = GetState();
if (state == DetectorState::ERROR)
throw JFJochException(JFJochExceptionCategory::Detector, "Detector in error state");
throw JFJochCriticalException("Detector in error state");
else if ((state == DetectorState::BUSY) || (state == DetectorState::WAITING)) {
try {
det.stopDetector();