Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
Channel.cxx
Go to the documentation of this file.
1/*
2 * Project: RooFit
3 * Authors:
4 * Jonas Rembser, CERN 2026
5 *
6 * Copyright (c) 2026, CERN
7 *
8 * Redistribution and use in source and binary forms,
9 * with or without modification, are permitted according to the terms
10 * listed in LICENSE (http://roofit.sourceforge.net/license.txt)
11 */
12
15
16#include <algorithm>
17#include <cerrno>
18#include <cstring>
19
20#include <fcntl.h>
21#include <poll.h>
22#include <sys/socket.h>
23#include <unistd.h>
24
25namespace RooFit {
26namespace MultiProcess {
27
28namespace {
29
30constexpr std::uint64_t moreBit = std::uint64_t(1) << 63;
31constexpr std::uint64_t sizeMask = moreBit - 1;
32
33/// Per-process registry of all live channels, so that any blocking wait can
34/// flush the pending output of every channel (also the ones not being read
35/// from) and no send can be starved. The processes are single-threaded, so a
36/// plain static is fine here. The vector is intentionally leaked: Channels
37/// held by the static JobManager instance are destroyed during static
38/// destruction, which can happen after a function-local static vector would
39/// have been destroyed.
40std::vector<Channel *> &liveChannels()
41{
42 static auto *channels = new std::vector<Channel *>;
43 return *channels;
44}
45
46void registerChannel(Channel *channel)
47{
48 liveChannels().push_back(channel);
49}
50
51void unregisterChannel(Channel *channel)
52{
53 auto &channels = liveChannels();
54 channels.erase(std::remove(channels.begin(), channels.end(), channel), channels.end());
55}
56
57ssize_t send_some(int fd, const void *buf, std::size_t n)
58{
59#ifdef MSG_NOSIGNAL
60 return ::send(fd, buf, n, MSG_NOSIGNAL);
61#else
62 return ::send(fd, buf, n, 0);
63#endif
64}
65
66} // namespace
67
68Channel::Channel(int fd) : fd_(fd)
69{
70 int flags = fcntl(fd_, F_GETFL, 0);
71 if (flags == -1 || fcntl(fd_, F_SETFL, flags | O_NONBLOCK) == -1) {
72 throw std::runtime_error(std::string("MultiProcess::Channel: could not set O_NONBLOCK: ") + strerror(errno));
73 }
74#ifdef SO_NOSIGPIPE
75 // on platforms without MSG_NOSIGNAL (macOS), prevent SIGPIPE on writes to a closed peer
76 int optval = 1;
78#endif
79 registerChannel(this);
80}
81
83{
84 if (valid()) {
86 }
87 close_fd();
88}
89
91 : fd_(other.fd_),
92 out_buf_(std::move(other.out_buf_)),
93 out_pos_(other.out_pos_),
94 in_header_(other.in_header_),
95 in_header_bytes_(other.in_header_bytes_),
96 in_have_header_(other.in_have_header_),
97 in_msg_(std::move(other.in_msg_)),
98 in_msg_bytes_(other.in_msg_bytes_)
99{
100 other.fd_ = -1;
101 if (valid()) {
103 registerChannel(this);
104 }
105}
106
108{
109 if (this != &other) {
110 if (valid()) {
111 unregisterChannel(this);
112 }
113 close_fd();
114 fd_ = other.fd_;
115 out_buf_ = std::move(other.out_buf_);
116 out_pos_ = other.out_pos_;
117 in_header_ = other.in_header_;
118 in_header_bytes_ = other.in_header_bytes_;
119 in_have_header_ = other.in_have_header_;
120 in_msg_ = std::move(other.in_msg_);
121 in_msg_bytes_ = other.in_msg_bytes_;
122 other.fd_ = -1;
123 if (valid()) {
125 registerChannel(this);
126 }
127 }
128 return *this;
129}
130
132{
133 if (fd_ >= 0) {
134 ::close(fd_);
135 fd_ = -1;
136 }
137}
138
140{
141 // A closed connection during shutdown just means the other process was
142 // terminated a moment before this one noticed; in that case exit the event
143 // loops through the regular SIGTERM path. The SIGTERM may still be in
144 // flight, so give it a moment to arrive.
147 if (wake_fd >= 0) {
149 ::poll(&pfd, 1, 500);
150 }
151 }
153 throw ppoll_error_t(EINTR, "MultiProcess::Channel: connection closed while terminating");
154 }
155 throw std::runtime_error("MultiProcess::Channel: connection closed by peer process (did it die unexpectedly?)");
156}
157
158void Channel::send_frame(const void *data, std::size_t size, bool more)
159{
160 std::uint64_t header = (std::uint64_t(size) & sizeMask) | (more ? moreBit : 0);
161 // Append to the pending-output buffer and then write out as much as the
162 // socket accepts. Appending first keeps this simple and correct also when
163 // there already is pending output; the extra copy is negligible for the
164 // message sizes used here. For multipart messages, the flush is deferred
165 // to the final frame, so a k-frame message costs one send() system call
166 // instead of k. Deferring is safe: any blocking wait() in this process
167 // also flushes the pending output of all channels.
168 const char *headerBytes = reinterpret_cast<const char *>(&header);
169 out_buf_.insert(out_buf_.end(), headerBytes, headerBytes + sizeof(header));
170 const char *dataBytes = static_cast<const char *>(data);
171 out_buf_.insert(out_buf_.end(), dataBytes, dataBytes + size);
172 if (!more) {
173 try_flush();
174 }
175}
176
178{
179 while (out_pos_ < out_buf_.size()) {
180 ssize_t n = send_some(fd_, out_buf_.data() + out_pos_, out_buf_.size() - out_pos_);
181 if (n >= 0) {
182 out_pos_ += n;
183 } else if (errno == EINTR) {
184 continue;
185 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
186 return false;
187 } else if (errno == EPIPE || errno == ECONNRESET) {
189 } else {
190 throw std::runtime_error(std::string("MultiProcess::Channel: send failed: ") + strerror(errno));
191 }
192 }
193 out_buf_.clear();
194 out_pos_ = 0;
195 return true;
196}
197
199{
200 if (!in_have_header_) {
201 char *headerBytes = reinterpret_cast<char *>(&in_header_);
202 while (in_header_bytes_ < sizeof(in_header_)) {
204 if (n > 0) {
206 } else if (n == 0) {
208 } else if (errno == EINTR) {
209 continue;
210 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
211 return false;
212 } else if (errno == ECONNRESET) {
214 } else {
215 throw std::runtime_error(std::string("MultiProcess::Channel: receive failed: ") + strerror(errno));
216 }
217 }
218 in_have_header_ = true;
220 in_msg_bytes_ = 0;
221 }
222
223 // Read exactly the payload of the current frame, so that any following
224 // frames stay in the kernel buffer and poll() remains accurate.
225 char *payload = in_msg_.data<char>();
226 while (in_msg_bytes_ < in_msg_.size()) {
227 ssize_t n = ::read(fd_, payload + in_msg_bytes_, in_msg_.size() - in_msg_bytes_);
228 if (n > 0) {
229 in_msg_bytes_ += n;
230 } else if (n == 0) {
232 } else if (errno == EINTR) {
233 continue;
234 } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
235 return false;
236 } else if (errno == ECONNRESET) {
238 } else {
239 throw std::runtime_error(std::string("MultiProcess::Channel: receive failed: ") + strerror(errno));
240 }
241 }
242
243 msg = std::move(in_msg_);
244 if (more) {
245 *more = (in_header_ & moreBit) != 0;
246 }
247 in_have_header_ = false;
248 in_header_ = 0;
250 in_msg_ = Message{};
251 in_msg_bytes_ = 0;
252 return true;
253}
254
256{
257 Message msg;
258 while (!try_recv_frame(msg, more)) {
259 wait({this}, -1);
260 }
261 return msg;
262}
263
264std::vector<std::size_t> Channel::wait(const std::vector<const Channel *> &read_channels, int timeout_ms)
265{
266 while (true) {
267 std::vector<pollfd> pollfds;
268 pollfds.reserve(read_channels.size() + liveChannels().size() + 1);
269
271 if (wake_fd >= 0) {
272 pollfds.push_back({wake_fd, POLLIN, 0});
273 }
274 const std::size_t first_read_item = pollfds.size();
275 for (const Channel *channel : read_channels) {
276 pollfds.push_back({channel->fd(), POLLIN, 0});
277 }
278 // also watch all channels that still have pending output, so their
279 // sends make progress while we wait and no two processes can deadlock
280 // each other with full socket buffers
281 std::vector<Channel *> flush_channels;
282 for (Channel *channel : liveChannels()) {
283 if (channel->has_pending_output()) {
284 flush_channels.push_back(channel);
285 pollfds.push_back({channel->fd(), POLLOUT, 0});
286 }
287 }
288
289 int rc = ::poll(pollfds.data(), pollfds.size(), timeout_ms);
290 if (rc < 0) {
291 if (errno == EINTR) {
292 // Retry on benign signal interruptions (profilers, debuggers,
293 // SIGCHLD, ...). This is essential for protocol integrity: a
294 // multi-frame message is received with one blocking receive per
295 // frame, and surfacing a benign EINTR mid-sequence to the event
296 // loops would make them restart the loop and desynchronize the
297 // wire protocol. Only termination requests leave this function
298 // exceptionally. There is no lost-wakeup race with SIGTERM: the
299 // handler also writes to the self-pipe, which the next poll
300 // reports as readable.
302 throw ppoll_error_t(EINTR, "poll interrupted by SIGTERM");
303 }
304 continue;
305 }
306 throw std::runtime_error(std::string("MultiProcess::Channel::wait: poll failed: ") + strerror(errno));
307 }
308
309 // a byte on the self-pipe means a SIGTERM arrived (possibly before we
310 // entered poll, which is exactly the race the self-pipe closes)
311 if (wake_fd >= 0 && (pollfds[0].revents & POLLIN)) {
312 throw ppoll_error_t(EINTR, "poll interrupted by SIGTERM");
313 }
314
315 for (std::size_t fx = 0; fx < flush_channels.size(); ++fx) {
316 std::size_t item = first_read_item + read_channels.size() + fx;
317 if (pollfds[item].revents & (POLLOUT | POLLERR | POLLHUP)) {
318 flush_channels[fx]->try_flush();
319 }
320 }
321
322 std::vector<std::size_t> readable;
323 for (std::size_t ix = 0; ix < read_channels.size(); ++ix) {
325 readable.push_back(ix);
326 }
327 }
328 if (!readable.empty() || timeout_ms >= 0) {
329 return readable;
330 }
331 // infinite timeout, but we only woke up to flush output: wait again
332 }
333}
334
335} // namespace MultiProcess
336} // namespace RooFit
size_t size(const MatrixT &matrix)
retrieve the size of a square matrix
std::string Message(const std::string &msg, const std::string &location)
Definition Scanner.cxx:177
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void data
const_iterator begin() const
const_iterator end() const
One endpoint of a full-duplex interprocess message pipe.
Definition Channel.h:55
void send_frame(const void *data, std::size_t size, bool more)
Queue one frame for sending and write out as much as the socket accepts.
Definition Channel.cxx:158
static void throw_connection_closed()
Handle end-of-stream / closed-connection conditions; never returns.
Definition Channel.cxx:139
bool try_recv_frame(Message &msg, bool *more)
Non-blocking receive attempt.
Definition Channel.cxx:198
static std::vector< std::size_t > wait(const std::vector< const Channel * > &read_channels, int timeout_ms)
Wait until at least one of read_channels has input available, flushing the pending output of all live...
Definition Channel.cxx:264
Channel & operator=(const Channel &)=delete
bool try_flush()
Write out pending output; returns true when all of it has been written.
Definition Channel.cxx:177
Message recv_frame(bool *more=nullptr)
Blocking receive of one complete frame, interruptible by SIGTERM (throws ppoll_error_t,...
Definition Channel.cxx:255
std::vector< char > out_buf_
Definition Channel.h:101
A contiguous byte buffer used as the unit of interprocess communication.
Definition Message.h:30
std::size_t size() const
Size of the message in bytes.
Definition Message.h:65
static int sigterm_wake_fd()
Read end of the self-pipe that the SIGTERM handler writes to (or -1 on the master process,...
Thrown when a blocking wait on a Channel is interrupted, e.g.
Definition Channel.h:31
const Int_t n
Definition legend1.C:16
The namespace RooFit contains mostly switches that change the behaviour of functions of PDFs (or othe...
Definition CodegenImpl.h:73