Skip to content

Commit e6acbe5

Browse files
committed
DPL: optionally fork data processors without exec
With DPL_FORK_WITHOUT_EXEC=1 in the environment, the driver only forks the data processors it defined itself, i.e. those not imported from an upstream workflow, including the internal ones, and the child runs the device straight away, from the topology, libraries and services the driver has already set up. Data processors imported from an upstream workflow are still exec'd, since only their executable can define them. By default nothing changes: the exec path is as before, and the new code is only reached when noExec is set. The forked child is made to look like one which did exec: - only the driver's end of the pipes of the devices forked so far, and of its own, are closed, rather than every descriptor: what the driver opened, e.g. libuv's signal lock pipe, recreated at fork, is still in use in the child. The pipes of the next devices do not exist yet. - SIGUSR1/SIGUSR2 are ignored as on the exec path, and the handlers the driver installed for SIGINT, SIGTERM and SIGCHLD are reset, as exec would do. - it gets a loop of its own, the driver's listening socket is closed, and it reads to the end the workflow the driver streams on its stdin, so the driver neither blocks on nor gets SIGPIPE from that pipe. - stdio is flushed before forking, so nothing is printed twice. - on macOS its os_log handles are replaced by OS_LOG_DISABLED: after a fork without exec os_signpost_enabled() reports every one of them as enabled, so every signpost argument would be evaluated. --signposts still works, since it does not go through os_log. This was seen as a crash in test_SimpleWildcard: the data_relayer signpost in saveInSlot dereferences a null input channel for the second wildcard route. That is an upstream bug, the same crash happens in exec mode with --signposts data_relayer. - getIdString() returns the device id, so that the workflow termination hook knows it runs in a device, as test_RootTreeWriterWorkflow needs. - it only redoes the steps the driver did not perform before forking: MERGE_CONFIGS, IMPORT_CURRENT_WORKFLOW and MATERIALISE_WORKFLOW (from 6903032bdeac).
1 parent 6f69769 commit e6acbe5

1 file changed

Lines changed: 103 additions & 11 deletions

File tree

‎Framework/Core/src/runDataProcessing.cxx‎

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,14 @@ void doBoostException(boost::exception& e, const char*);
188188
void doDPLException(o2::framework::RuntimeErrorRef& ref, char const*);
189189
void doUnknownException(std::string const& s, char const*);
190190

191+
// The id of a device which was forked without exec, since its argv is the driver's.
192+
static std::string gForkedDeviceId;
193+
191194
char* getIdString(int argc, char** argv)
192195
{
196+
if (!gForkedDeviceId.empty()) {
197+
return gForkedDeviceId.data();
198+
}
193199
for (int argi = 0; argi < argc; argi++) {
194200
if (strcmp(argv[argi], "--id") == 0 && argi + 1 < argc) {
195201
return argv[argi + 1];
@@ -710,8 +716,9 @@ void handle_crash(int sig)
710716
}
711717

712718
/// This will start a new device by forking and executing a
713-
/// new child
714-
void spawnDevice(uv_loop_t* loop,
719+
/// new child, or only forking it if @a noExec.
720+
/// @return true in a child which did not exec, and has to run the device.
721+
bool spawnDevice(uv_loop_t* loop,
715722
DeviceRef ref,
716723
std::vector<DeviceSpec> const& specs,
717724
DriverInfo& driverInfo,
@@ -723,7 +730,8 @@ void spawnDevice(uv_loop_t* loop,
723730
boost::program_options::variables_map& varmap,
724731
std::vector<DeviceStdioContext>& childFds,
725732
unsigned parentCPU,
726-
unsigned parentNode)
733+
unsigned parentNode,
734+
bool noExec)
727735
{
728736
// FIXME: this might not work when more than one DPL driver on the same
729737
// machine. Hopefully we do not care.
@@ -736,6 +744,10 @@ void spawnDevice(uv_loop_t* loop,
736744
service.preFork(serviceRegistry, DeviceConfig{varmap});
737745
}
738746
}
747+
if (noExec) {
748+
// Otherwise what is buffered is printed by both processes.
749+
fflush(nullptr);
750+
}
739751
// If we have a framework id, it means we have already been respawned
740752
// and that we are in a child. If not, we need to fork and re-exec, adding
741753
// the framework-id as one of the options.
@@ -765,7 +777,21 @@ void spawnDevice(uv_loop_t* loop,
765777
// idea in the first place, because rlim_cur could be huge
766778
// FIXME: I should understand which one is really to be closed and use
767779
// CLOEXEC on it.
768-
int rlim_cur = std::min((int)rlim.rlim_cur, 10000);
780+
// Without exec the child keeps using what the driver opened (e.g. libuv's
781+
// internals), so we only close the driver's end of the pipes and reset the
782+
// handlers the driver installed, as exec would do.
783+
int rlim_cur = noExec ? 0 : std::min((int)rlim.rlim_cur, 10000);
784+
for (int i = 0; noExec && i <= ref.index; ++i) {
785+
if (i == ref.index || deviceInfos[i].active) {
786+
close(childFds[i].childstdin[1]);
787+
close(childFds[i].childstdout[0]);
788+
}
789+
}
790+
if (noExec) {
791+
signal(SIGINT, SIG_DFL);
792+
signal(SIGTERM, SIG_DFL);
793+
signal(SIGCHLD, SIG_DFL);
794+
}
769795
for (int i = 0; i < rlim_cur; ++i) {
770796
if (childFds[ref.index].childstdin[0] == i) {
771797
continue;
@@ -787,6 +813,9 @@ void spawnDevice(uv_loop_t* loop,
787813
for (auto& env : execution.environ) {
788814
putenv(strdup(DeviceSpecHelpers::reworkTimeslicePlaceholder(env, spec).data()));
789815
}
816+
if (noExec) {
817+
return true;
818+
}
790819
int err = execvp(execution.args[0], execution.args.data());
791820
if (err) {
792821
perror("Unable to start child process");
@@ -876,6 +905,7 @@ void spawnDevice(uv_loop_t* loop,
876905

877906
// Let's add also metrics information for the given device
878907
gDeviceMetricsInfos.emplace_back(DeviceMetricsInfo{});
908+
return false;
879909
}
880910

881911
void processChildrenOutput(uv_loop_t* loop,
@@ -1598,6 +1628,13 @@ int runStateMachine(DataProcessorSpecs const& workflow,
15981628
}
15991629
});
16001630

1631+
// Which steps were done before forking a child which does not exec.
1632+
struct {
1633+
bool childDidMergeConfig = false;
1634+
bool childDidImportWorkflow = false;
1635+
bool childDidMaterialiseWorkflow = false;
1636+
} childState;
1637+
16011638
while (true) {
16021639
// If control forced some transition on us, we push it to the queue.
16031640
if (driverControl.forcedTransitions.empty() == false) {
@@ -1674,6 +1711,7 @@ int runStateMachine(DataProcessorSpecs const& workflow,
16741711
#endif
16751712
break;
16761713
case DriverState::IMPORT_CURRENT_WORKFLOW:
1714+
childState.childDidImportWorkflow = true;
16771715
// This state is needed to fill the metadata structure
16781716
// which contains how to run the current workflow
16791717
dataProcessorInfos = previousDataProcessorInfos;
@@ -1702,6 +1740,7 @@ int runStateMachine(DataProcessorSpecs const& workflow,
17021740
}
17031741
break;
17041742
case DriverState::MATERIALISE_WORKFLOW:
1743+
childState.childDidMaterialiseWorkflow = true;
17051744
try {
17061745
auto workflowState = WorkflowHelpers::verifyWorkflow(workflow);
17071746
if (driverConfig.batch == true && varmap["dds"].as<std::string>().empty() && !varmap["dump-workflow"].as<bool>() && workflowState == WorkflowParsingState::Empty) {
@@ -2100,6 +2139,7 @@ int runStateMachine(DataProcessorSpecs const& workflow,
21002139
}
21012140
break;
21022141
case DriverState::MERGE_CONFIGS: {
2142+
childState.childDidMergeConfig = true;
21032143
try {
21042144
controls.resize(runningWorkflow.devices.size());
21052145
/// Set the default value for tracingFlags of each control
@@ -2204,15 +2244,67 @@ int runStateMachine(DataProcessorSpecs const& workflow,
22042244
runningWorkflow.devices[di], controls[di], deviceExecutions[di], infos, allStates);
22052245
} else {
22062246
DeviceRef ref{di};
2207-
spawnDevice(loop,
2208-
ref,
2209-
runningWorkflow.devices, driverInfo,
2210-
controls, deviceExecutions, infos,
2211-
allStates,
2212-
serviceRegistry, varmap,
2213-
childFds, parentCPU, parentNode);
2247+
// With DPL_FORK_WITHOUT_EXEC set, the data processors defined in this process
2248+
// are only forked. Those imported from upstream workflows still need their executable.
2249+
static bool forkWithoutExec = getenv("DPL_FORK_WITHOUT_EXEC") && strcmp(getenv("DPL_FORK_WITHOUT_EXEC"), "0");
2250+
bool imported = std::any_of(previousDataProcessorInfos.begin(), previousDataProcessorInfos.end(), [&id = runningWorkflow.devices[di].id](auto const& info) { return info.name == id; });
2251+
bool noExec = forkWithoutExec && !imported;
2252+
if (spawnDevice(loop,
2253+
ref,
2254+
runningWorkflow.devices, driverInfo,
2255+
controls, deviceExecutions, infos,
2256+
allStates,
2257+
serviceRegistry, varmap,
2258+
childFds, parentCPU, parentNode, noExec)) {
2259+
// We are a child which did not exec: from now on it behaves
2260+
// like one which did, with a loop of its own and without the
2261+
// driver's listening socket.
2262+
int serverFd = -1;
2263+
if (uv_fileno((uv_handle_t*)&serverContext.serverHandle, &serverFd) == 0) {
2264+
close(serverFd);
2265+
}
2266+
#ifdef __APPLE__
2267+
// After a fork without exec, os_signpost_enabled() reports every os_log as
2268+
// enabled, which a fresh process does not. --signposts still works, since
2269+
// it does not go through os_log.
2270+
o2_walk_logs([](char const*, void* l, void*) { ((_o2_log_t*)l)->os_log = OS_LOG_DISABLED; return true; }, nullptr);
2271+
#endif
2272+
loop = uv_loop_new();
2273+
serverContext.loop = loop;
2274+
serverContext.isDriver = false;
2275+
frameworkId = runningWorkflow.devices[di].id;
2276+
gForkedDeviceId = frameworkId;
2277+
driverInfo.defaultDriverClient = "stdout://";
2278+
driverInfo.timeout = 0;
2279+
driverInfo.argc = deviceExecutions[di].args.size() - 1;
2280+
driverInfo.argv = deviceExecutions[di].args.data();
2281+
driverInfo.states.clear();
2282+
// Like one which did exec, consume the workflow the driver streams on stdin,
2283+
// so that the driver does not block on, or get SIGPIPE from, the pipe.
2284+
char buffer[4096];
2285+
ssize_t bytes = 0;
2286+
while ((bytes = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0 || (bytes < 0 && errno == EINTR)) {
2287+
}
2288+
// Only redo the steps which the driver did not already perform before forking.
2289+
driverControl.forcedTransitions = {
2290+
DriverState::DO_CHILD,
2291+
DriverState::BIND_GUI_PORT};
2292+
if (!childState.childDidMergeConfig) {
2293+
driverControl.forcedTransitions.push_back(DriverState::MERGE_CONFIGS);
2294+
}
2295+
if (!childState.childDidImportWorkflow) {
2296+
driverControl.forcedTransitions.push_back(DriverState::IMPORT_CURRENT_WORKFLOW);
2297+
}
2298+
if (!childState.childDidMaterialiseWorkflow) {
2299+
driverControl.forcedTransitions.push_back(DriverState::MATERIALISE_WORKFLOW);
2300+
}
2301+
break;
2302+
}
22142303
}
22152304
}
2305+
if (!frameworkId.empty()) {
2306+
break;
2307+
}
22162308
handleSignals();
22172309
handleChildrenStdio(&serverContext, forwardedStdin.str(), childFds, pollHandles);
22182310
for (auto& callback : postScheduleCallbacks) {

0 commit comments

Comments
 (0)