Files
Jungfraujoch/receiver/JFJochReceiverService.cpp
T
leonarski_fandClaude Opus 5 5f838bdb26 Broker: refuse a run the writer cannot write before arming the detector
A writer refusing to overwrite an existing file was reported badly, and the
earlier fix only covered half of it. The commit message of a6a715703 states the
assumption that broke: "SendStartMessage precedes every std::async in the
receiver constructor". That is true of JFJochReceiverFPGA, where the start
message goes out from the constructor body, and false of JFJochReceiverLite,
where it goes out from MeasurementThread - launched by std::async from the
constructor, which waits only on data_analysis_started. On the DECTRIS path the
writer's refusal therefore cannot reach services.Start(); it arrives from
services.Stop(), i.e. the second try-block of the state machine's
MeasurementThread, which never touched start_exception. /wait_till_done answered
200 for a run that never wrote anything.

Two changes, in front of the problem and behind it.

In front: a PREFLIGHT frame (TCP protocol version 3 -> 4), sent to every writer
before the receiver is built and before the detector is armed. It carries the
same CBOR start message a START does and asks one question: could this run be
written? The writer runs FileWriter::Preflight - the first three statements of
the FileWriter constructor, stopping short of the only one that creates
anything - and answers with an ACK. Shared code, so a pre-flight that passes
cannot be contradicted by the start behind it. A refusal is not fatal: nothing
was started, the connection stays usable, and above all no series is left armed
waiting for a trigger that will not come. Both ends already reject a frame of
another protocol version, so the bump costs no compatibility.

The check now covers the data files, not only the master. A stale
_data_000001.h5 used to sail through and fail when the file was renamed into
place at the end of the run, after the whole dataset had been collected. The
writer holding the master file checks for all of them - they share a directory
by construction, the master linking them by relative name - so the siblings do
not race each other statting files the others are creating.

Behind: /wait_till_done maps an Idle state to 200 or 500 by the severity of the
end-of-run message, so a collection that lost packets, or whose writer could not
write, is no longer indistinguishable from a good one to a script. A cancelled
collection is not affected - MeasurementThread checks the cancel before it looks
at the completeness of the data, and reports it with Info severity - and a
warning is still a 200. start_exception is untouched.

The PREFLIGHT payload is built from FillMessage alone, which is what keeps it
small: the per-pixel arrays are added by SendStartMessage and are not in it.
Measured on a JUNGFRAU 9M, 1.4 kB against 543 kB for the start message. It is
deliberately the whole of FillMessage rather than the fields the check reads
today - a hand-picked subset would answer for a different set of file names the
day the check learns to read one more field, and would do it silently.

Three defects found on the way:

TCPImagePuller republished a PREFLIGHT as a real start message, announcing to
downstream consumers a run that was not going to happen.

TCPStreamPusher::run_number was a plain uint64_t written by the control thread
and read by every ACK thread; Preflight adds a second writer to it.

StreamWriter::CollectImages reprocessed the last frame whenever WaitForImage
returned false - run is only tested at the top of the loop - so an abort wrote
the last image a second time. Pre-existing; the new branch made it visible.

Verified against a running broker on the local configuration: a colliding master
file and a colliding data file are both refused by /start, /wait_until_running
and /wait_till_done with the writer's own message, and the log shows the receiver
was never started for either; a cancelled collection still answers 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WmryXe8ASbNi632sUMfsa
2026-08-27 12:20:29 +02:00

364 lines
15 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochReceiverService.h"
#include "JFJochReceiverFPGA.h"
#include "JFJochReceiverLite.h"
#include "../preview/JFJochJPEG.h"
#include "../preview/JFJochTIFF.h"
JFJochReceiverService::JFJochReceiverService(AcquisitionDeviceGroup &in_aq_devices,
Logger &in_logger, ImagePusher &pusher,
size_t send_buffer_size_MiB)
: aq_devices(in_aq_devices),
logger(in_logger),
image_buffer(send_buffer_size_MiB * 1024 * 1024),
image_pusher(pusher),
spot_finding_settings(DiffractionExperiment::DefaultDataProcessingSettings()) {
}
JFJochReceiverService &JFJochReceiverService::NumThreads(int64_t input) {
if (input <= 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Thread number must be above zero");
nthreads = input;
return *this;
}
void JFJochReceiverService::FinalizeMeasurementChangeState() {
std::unique_lock ul(state_mutex);
state = ReceiverState::Idle;
measurement_done.notify_all();
}
void JFJochReceiverService::FinalizeMeasurement() {
try {
receiver->StopReceiver();
} catch (...) {
FinalizeMeasurementChangeState();
throw;
}
FinalizeMeasurementChangeState();
}
std::optional<JFJochReceiverStatus> JFJochReceiverService::GetStatus() {
return receiver_status.GetStatus();
}
void JFJochReceiverService::Preflight(const DiffractionExperiment &experiment) {
// Same condition JFJochReceiver uses to decide whether it pushes to a writer at all: with no
// images or no file prefix nothing is written, so there is nothing to check.
if ((experiment.GetImageNum() <= 0) || experiment.GetFilePrefix().empty())
return;
// FillMessage alone, which is what makes this frame small: the per-pixel arrays that dominate a
// real start message - the pixel mask, the azimuthal-integration map, the ROI map - are added
// by JFJochReceiver::SendStartMessage and are not here. Measured on a JUNGFRAU 9M: 1.4 kB
// against 543 kB for the start message that follows. It is deliberately the whole of
// FillMessage rather than the handful of fields the check reads today, so that the writer sees
// the same description of the run it will be given at start - a hand-picked subset would
// answer for a different set of file names the day the check learns to read one more field.
StartMessage message{};
experiment.FillMessage(message);
image_pusher.Preflight(message);
}
void JFJochReceiverService::Start(const DiffractionExperiment &experiment,
const PixelMask &pixel_mask,
const JFCalibration *calibration,
std::shared_ptr<ImagePuller> puller) {
std::unique_lock ul_state(state_mutex); // unique lock, as it will destroy and create receiver object
if (state != ReceiverState::Idle)
throw JFJochException(JFJochExceptionCategory::WrongDAQState, "Receiver not idle, cannot start");
try {
auto nthreads_local = nthreads;
if (experiment.IsCPUSummation())
nthreads_local = 4;
// First clean-up old measurement
receiver.reset();
preview_image.Configure(experiment, pixel_mask);
switch (experiment.GetDetectorType()) {
case DetectorType::EIGER:
case DetectorType::JUNGFRAU:
receiver = std::make_unique<JFJochReceiverFPGA>(experiment, pixel_mask,
calibration,
aq_devices, image_pusher,
logger,
nthreads_local,
spot_finding_settings,
receiver_status,
plots,
image_buffer,
zmq_preview_socket.get(),
zmq_metadata_socket.get(),
indexer_thread_pool.get());
break;
case DetectorType::DECTRIS:
if (puller)
image_puller = puller;
else {
image_puller = std::make_shared<ZMQImagePuller>(
experiment.GetDetectorSetup().GetDECTRISStream2Addr());
}
receiver = std::make_unique<JFJochReceiverLite>(experiment,
pixel_mask,
*image_puller,
image_pusher,
logger,
nthreads_local,
spot_finding_settings,
receiver_status,
plots,
image_buffer,
zmq_preview_socket.get(),
zmq_metadata_socket.get(),
indexer_thread_pool.get());
break;
}
measurement = std::async(std::launch::async, &JFJochReceiverService::FinalizeMeasurement, this);
state = ReceiverState::Running;
} catch (const std::exception &e) {
// The receiver never started, so drop the status its base constructor had already reset -
// otherwise /status and /statistics keep reporting a zero-progress run that never happened,
// until the next start overwrites it.
receiver_status.Clear();
receiver_status.SetProgress({});
logger.ErrorException(e);
throw;
}
}
void JFJochReceiverService::Cancel(bool silent) {
std::unique_lock ul(state_mutex);
if (state == ReceiverState::Running)
receiver->Cancel(silent);
}
JFJochReceiverOutput JFJochReceiverService::Stop() {
std::unique_lock ul(state_mutex);
measurement_done.wait(ul, [this] { return (state != ReceiverState::Running); });
if (state != ReceiverState::Idle)
throw JFJochException(JFJochExceptionCategory::WrongReceiverState, "Receiver in weird state");
try {
if (measurement.valid())
measurement.get();
} catch (JFJochException &e) {
logger.ErrorException(e);
throw;
}
if (!receiver) {
logger.Warning("Request to stop while receiver not running");
throw JFJochException(JFJochExceptionCategory::WrongReceiverState, "Receiver idle, cannot stop");
}
return receiver->GetFinalStatistics();
}
void JFJochReceiverService::SetSpotFindingSettings(const SpotFindingSettings &settings) {
try {
std::unique_lock ul(state_mutex);
DiffractionExperiment::CheckDataProcessingSettings(settings);
spot_finding_settings = settings;
if (state != ReceiverState::Idle)
receiver->SetSpotFindingSettings(settings);
} catch (std::exception &e) {
logger.ErrorException(e);
throw;
}
}
MultiLinePlot JFJochReceiverService::GetDataProcessingPlot(const PlotRequest &request) {
return plots.GetPlots(request);
}
void JFJochReceiverService::GetPlotRaw(std::vector<float> &v, PlotType type, const std::string &roi) {
plots.GetPlotRaw(v, type, roi);
}
std::vector<AcquisitionDeviceNetConfig> JFJochReceiverService::GetNetworkConfig() {
return aq_devices.GetNetworkConfig();
}
void JFJochReceiverService::LoadInternalGeneratorImage(const DiffractionExperiment &experiment,
const std::vector<uint16_t> &image,
uint64_t image_number) {
std::vector<uint16_t> raw_geom, eiger_geom;
const uint16_t *frame;
if (image.size() == RAW_MODULE_SIZE * experiment.GetModulesNum()) {
frame = image.data();
} else if (image.size() == experiment.GetPixelsNum()) {
raw_geom.resize(RAW_MODULE_SIZE * experiment.GetModulesNum());
ConvertedToRawGeometry(experiment, raw_geom.data(), image.data());
frame = raw_geom.data();
} else
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Size of input array with raw expected image is wrong");
for (int i = 0; i < experiment.GetDataStreamsNum(); i++) {
uint32_t module0 = experiment.GetFirstModuleOfDataStream(i);
switch (experiment.GetDetectorSetup().GetDetectorType()) {
case DetectorType::EIGER:
eiger_geom.resize(RAW_MODULE_SIZE);
for (int m = 0; m < experiment.GetModulesNum(i); m++) {
RawToEigerInput(eiger_geom.data(), frame + (module0 + m) * RAW_MODULE_SIZE);
aq_devices[i].SetInternalGeneratorFrame(eiger_geom.data(),
m + experiment.GetModulesNum(i) * image_number);
}
break;
case DetectorType::JUNGFRAU:
for (int m = 0; m < experiment.GetModulesNum(i); m++)
aq_devices[i].SetInternalGeneratorFrame(frame + (module0 + m) * RAW_MODULE_SIZE,
m + experiment.GetModulesNum(i) * image_number);
break;
default:
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Detector not supported");
}
}
}
void JFJochReceiverService::GetXFELEventCode(std::vector<uint64_t> &v) const {
plots.GetXFELEventCode(v);
}
void JFJochReceiverService::GetXFELPulseID(std::vector<uint64_t> &v) const {
plots.GetXFELPulseID(v);
}
std::vector<DeviceStatus> JFJochReceiverService::GetDeviceStatus() const {
return aq_devices.GetDeviceStatus();
}
std::optional<float> JFJochReceiverService::GetProgress() const {
return receiver_status.GetProgress();
}
JFJochReceiverService &JFJochReceiverService::PreviewSocket(const std::string &addr, const std::optional<int32_t> &watermark) {
if (!addr.empty()) {
logger.Info("ZeroMQ preview socket available at {}", addr);
zmq_preview_socket = std::make_unique<ZMQPreviewSocket>(addr, watermark);
}
return *this;
}
JFJochReceiverService &JFJochReceiverService::MetadataSocket(const std::string &addr) {
if (!addr.empty()) {
logger.Info("ZeroMQ metadata socket available at {}", addr);
zmq_metadata_socket = std::make_unique<ZMQMetadataSocket>(addr);
}
return *this;
}
std::string JFJochReceiverService::GetPreviewSocketAddress() const {
if (zmq_preview_socket)
return zmq_preview_socket->GetAddress();
return "";
}
std::string JFJochReceiverService::GetMetadataSocketAddress() const {
if (zmq_metadata_socket)
return zmq_metadata_socket->GetAddress();
return "";
}
JFJochReceiverService &JFJochReceiverService::PreviewSocketSettings(const ZMQPreviewSettings &input) {
if (zmq_preview_socket)
zmq_preview_socket->ImportSettings(input);
return *this;
}
JFJochReceiverService &JFJochReceiverService::MetadataSocketSettings(const ZMQMetadataSettings &input) {
if (zmq_metadata_socket)
zmq_metadata_socket->ImportSettings(input);
return *this;
}
ZMQPreviewSettings JFJochReceiverService::GetPreviewSocketSettings() const {
if (zmq_preview_socket)
return zmq_preview_socket->GetSettings();
return {};
}
ZMQMetadataSettings JFJochReceiverService::GetMetadataSocketSettings() const {
if (zmq_metadata_socket)
return zmq_metadata_socket->GetSettings();
return {};
}
void JFJochReceiverService::GetStartMessageFromBuffer(std::vector<uint8_t> &v) {
image_buffer.GetStartMessage(v);
}
bool JFJochReceiverService::GetImageFromBuffer(std::vector<uint8_t> &v, int64_t image_number) {
return image_buffer.GetImage(v, image_number);
}
std::string JFJochReceiverService::GetJPEGFromBuffer(const PreviewImageSettings &settings, int64_t image_number) {
std::vector<uint8_t> cbor_image;
if (!image_buffer.GetImage(cbor_image, image_number))
return {};
return preview_image.GenerateImage(settings, cbor_image);
}
std::string JFJochReceiverService::GetTIFFFromBuffer(int64_t image_number) {
std::vector<uint8_t> cbor_image;
if (!image_buffer.GetImage(cbor_image, image_number))
return {};
return PreviewImage::GenerateTIFF(cbor_image);
}
ImageBufferStatus JFJochReceiverService::GetImageBufferStatus() const {
return image_buffer.GetStatus();
}
void JFJochReceiverService::ClearImageBuffer() {
std::unique_lock ul(state_mutex);
// Clearing image buffer during data collection could be catastrophic, so better protect here, even if redundant
// with JFJochStateMachine
if (state == ReceiverState::Idle)
image_buffer.Finalize(std::chrono::milliseconds(2500));
else
throw JFJochException(JFJochExceptionCategory::WrongDAQState,
"Cannot clear image buffer during data collection");
}
JFJochReceiverService &JFJochReceiverService::Indexing(const IndexingSettings &input) {
std::unique_lock ul(state_mutex);
// Clearing image buffer during data collection could be catastrophic, so better protect here, even if redundant
// with JFJochStateMachine
if (state == ReceiverState::Idle) {
// Release the previous run's receiver first. It holds a raw pointer to the indexer pool and
// its own GPU resources; keeping it alive while we rebuild the pool means a fresh GPU indexer
// has to coexist with a stale receiver (e.g. after a failed acquisition start, where the
// receiver is stopped but not destroyed until the next Start), which can make the GPU indexer
// initialisation fail. Destroying the receiver before the pool also avoids the dangling
// pointer. Safe here: state is Idle, so no measurement is using it.
receiver.reset();
logger.Info("Resetting indexing thread pool");
indexer_thread_pool.reset();
if (input.GetAlgorithm() != IndexingAlgorithmEnum::None) {
logger.Info("Creating indexing thread pool...");
indexer_thread_pool = std::make_unique<IndexerThreadPool>(input);
logger.Info(" ... done");
}
return *this;
} else
throw JFJochException(JFJochExceptionCategory::WrongDAQState,
"Cannot change indexing settings during data collection");
}
ImagePusherStatus JFJochReceiverService::GetImagePusherStatus() const {
return image_pusher.GetStatus();
}