Skip to content

Commit b39b822

Browse files
clumensnrwahl2
authored andcommitted
Fix: various: Correctly detect completion of systemd start/stop actions
When systemd receives a StartUnit() or StopUnit() method call, it returns almost immediately, as soon as a start/stop job is enqueued. A successful return code does NOT indicate that the start/stop has finished. Previously, we worked around this in action_complete() with a hack that scheduled a follow-up monitor after a successful start/stop method call, which polled the service after 2 seconds to see whether it was actually running. However, this was not a robust solution. Timing issues could result in Pacemaker having an incorrect view of the resource's status or prematurely declaring the action as failed. Now, we follow the best practice as documented in the systemd D-Bus API doc (see StartUnit()): https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html#Methods After kicking off a systemd start/stop action, we make note of the job's D-Bus object path. Then we register a D-Bus message filter that looks for a JobRemoved signal whose bus path matches. This signal indicates that the job has completed and includes its result. When we find the matching signal, we set the action's result. We then remove the filter, which causes the action to be finalized and freed. In the case of the executor daemon, the action has a callback (action_complete()) that runs during finalization and sets the executor's view of the action result. Monitor actions still need much of the existing workaround code in action_complete(), so we keep it for now. We bail out for start/stop actions after setting the result as described above. We also have to finalize the action explicitly if a start/stop fails due to a "not installed" error, because the action_complete() callback never gets called in that case. The unit doesn't truly exist, so no job gets enqueued and thus no JobRemoved signal is received. We can't check for this situation by checking whether the LoadUnit() method call fails. Pacemaker writes an override file that makes it look as if the unit exists, so LoadUnit() returns success. Ref T25
1 parent 11f3bc3 commit b39b822

File tree

2 files changed

+134
-10
lines changed

2 files changed

+134
-10
lines changed

daemons/execd/execd_commands.c

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the Pacemaker project contributors
2+
* Copyright 2012-2025 the Pacemaker project contributors
33
*
44
* The version control history for this file may have further details.
55
*
@@ -872,14 +872,16 @@ action_complete(svc_action_t * action)
872872
if (pcmk__result_ok(&(cmd->result))
873873
&& pcmk__strcase_any_of(cmd->action, PCMK_ACTION_START,
874874
PCMK_ACTION_STOP, NULL)) {
875-
/* systemd returns from start and stop actions after the action
876-
* begins, not after it completes. We have to jump through a few
877-
* hoops so that we don't report 'complete' to the rest of pacemaker
878-
* until it's actually done.
875+
/* Getting results for when a start or stop action completes is now
876+
* handled by watching for JobRemoved() signals from systemd and
877+
* reacting to them. So, we can bypass the rest of the code in this
878+
* function for those actions, and simply finalize cmd.
879+
*
880+
* @TODO When monitors are handled in the same way, this function
881+
* can either be drastically simplified or done away with entirely.
879882
*/
880-
goagain = true;
881-
cmd->real_action = cmd->action;
882-
cmd->action = pcmk__str_copy(PCMK_ACTION_MONITOR);
883+
services__copy_result(action, &(cmd->result));
884+
goto finalize;
883885

884886
} else if (cmd->result.execution_status == PCMK_EXEC_PENDING &&
885887
pcmk__str_any_of(cmd->action, PCMK_ACTION_MONITOR, PCMK_ACTION_STATUS, NULL) &&

lib/services/systemd.c

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
#include <crm/services_internal.h>
1515
#include <crm/common/mainloop.h>
1616

17+
#include <inttypes.h> // PRIu32
1718
#include <stdbool.h>
19+
#include <stdint.h> // uint32_t
1820
#include <sys/stat.h>
1921
#include <gio/gio.h>
2022
#include <services_private.h>
@@ -820,6 +822,103 @@ process_unit_method_reply(DBusMessage *reply, svc_action_t *op)
820822
}
821823
}
822824

825+
/*!
826+
* \internal
827+
* \brief Process a systemd \c JobRemoved signal for a given service action
828+
*
829+
* This filter is expected to be added with \c finalize_async_action_dbus() as
830+
* the \c free_data_function. Then if \p message is a \c JobRemoved signal for
831+
* the action specified by \p user_data, the action's result is set, the filter
832+
* is removed, and the action is finalized.
833+
*
834+
* \param[in,out] connection D-Bus connection
835+
* \param[in] message D-Bus message
836+
* \param[in,out] user_data Service action (\c svc_action_t)
837+
*
838+
* \retval \c DBUS_HANDLER_RESULT_HANDLED if \p message is a \c JobRemoved
839+
* signal for \p user_data
840+
* \retval \c DBUS_HANDLER_RESULT_NOT_YET_HANDLED otherwise (on error, if
841+
* \p message is not a \c JobRemoved signal, or if the signal is for
842+
* some other action's job)
843+
*/
844+
static DBusHandlerResult
845+
job_removed_filter(DBusConnection *connection, DBusMessage *message,
846+
void *user_data)
847+
{
848+
svc_action_t *action = user_data;
849+
uint32_t job_id = 0;
850+
const char *bus_path = NULL;
851+
const char *unit_name = NULL;
852+
const char *result = NULL;
853+
DBusError error;
854+
855+
CRM_CHECK((connection != NULL) && (message != NULL),
856+
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED);
857+
858+
// action should always be set when the filter is added
859+
if ((action == NULL)
860+
|| !dbus_message_is_signal(message, BUS_NAME_MANAGER, "JobRemoved")) {
861+
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
862+
}
863+
864+
dbus_error_init(&error);
865+
if (!dbus_message_get_args(message, &error,
866+
DBUS_TYPE_UINT32, &job_id,
867+
DBUS_TYPE_OBJECT_PATH, &bus_path,
868+
DBUS_TYPE_STRING, &unit_name,
869+
DBUS_TYPE_STRING, &result,
870+
DBUS_TYPE_INVALID)) {
871+
crm_err("Could not interpret systemd DBus signal: %s " QB_XS " (%s)",
872+
error.message, error.name);
873+
dbus_error_free(&error);
874+
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
875+
}
876+
877+
if (!pcmk__str_eq(bus_path, action->opaque->job_path, pcmk__str_none)) {
878+
// This filter is not for this job
879+
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
880+
}
881+
882+
crm_trace("Setting %s result for %s (JobRemoved id=%" PRIu32 ", result=%s",
883+
pcmk__s(action->action, "(unknown)"), unit_name, job_id, result);
884+
885+
if (pcmk__str_eq(result, "done", pcmk__str_none)) {
886+
services__set_result(action, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL);
887+
888+
} else if (pcmk__str_eq(result, "timeout", pcmk__str_none)) {
889+
services__set_result(action, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_TIMEOUT,
890+
"Investigate reason for timeout, and adjust "
891+
"configured operation timeout if necessary");
892+
893+
} else {
894+
/* @FIXME Should we handle additional results specially, instead of
895+
* mapping them all to a generic error with no exit reason?
896+
*/
897+
services__set_result(action, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR,
898+
NULL);
899+
}
900+
901+
/* This instance of the filter was specifically for the given action.
902+
*
903+
* The action gets finalized by services__finalize_async_op() via the
904+
* filter's free_data_function.
905+
*/
906+
dbus_connection_remove_filter(systemd_proxy, job_removed_filter, action);
907+
return DBUS_HANDLER_RESULT_HANDLED;
908+
}
909+
910+
/*!
911+
* \internal
912+
* \brief \c DBusFreeFunction wrapper for \c services__finalize_async_op()
913+
*
914+
* \param[in,out] action Asynchronous service action to finalize
915+
*/
916+
static void
917+
finalize_async_action_dbus(void *action)
918+
{
919+
services__finalize_async_op((svc_action_t *) action);
920+
}
921+
823922
/*!
824923
* \internal
825924
* \brief Process the completion of an asynchronous unit start, stop, or restart
@@ -844,12 +943,35 @@ unit_method_complete(DBusPendingCall *pending, void *user_data)
844943
CRM_LOG_ASSERT(pending == op->opaque->pending);
845944
services_set_op_pending(op, NULL);
846945

847-
// Determine result and finalize action
848946
process_unit_method_reply(reply, op);
849-
services__finalize_async_op(op);
947+
850948
if (reply != NULL) {
851949
dbus_message_unref(reply);
852950
}
951+
952+
if ((op->status != PCMK_EXEC_NOT_INSTALLED)
953+
&& pcmk__strcase_any_of(op->action, PCMK_ACTION_START, PCMK_ACTION_STOP,
954+
NULL)) {
955+
/* Start and stop method calls return when the job is enqueued, not when
956+
* it's complete. Start and stop actions must be finalized after the job
957+
* is complete, because the action callback function may use it. We add
958+
* a message filter to process the JobRemoved signal, which indicates
959+
* completion.
960+
*
961+
* The filter takes ownership of op, which will be finalized when the
962+
* filter is later removed.
963+
*
964+
* If the action failed with a NOT_INSTALLED error, we'll never receive
965+
* a JobRemoved signal, and the callback will never be called. So we
966+
* need to finalize the action now, in that case.
967+
*/
968+
if (dbus_connection_add_filter(systemd_proxy, job_removed_filter, op,
969+
finalize_async_action_dbus)) {
970+
return;
971+
}
972+
crm_err("Could not add D-Bus filter for systemd JobRemoved signals");
973+
}
974+
services__finalize_async_op(op);
853975
}
854976

855977
#define SYSTEMD_OVERRIDE_ROOT "/run/systemd/system/"

0 commit comments

Comments
 (0)