fix(server): complete shutdown with an external EventLoop

This commit is contained in:
Daniel Opitz
2026-10-12 12:17:03 +02:00
committed by Julius Pfrommer
parent 54fcc04ccc
commit e95223e142
3 changed files with 163 additions and 3 deletions
+8 -3
View File
@@ -136,19 +136,24 @@ UA_Server_runUntilInterrupt(UA_Server *server);
UA_EXPORT UA_StatusCode UA_EXPORT UA_StatusCode
UA_Server_run_startup(UA_Server *server); UA_Server_run_startup(UA_Server *server);
/* Executes a single iteration of the server's main loop. /* Executes a single iteration of the server's main loop. Completes an
* externally driven shutdown once all drivers have stopped, without stopping
* the external EventLoop.
* *
* @param server The server object. * @param server The server object.
* @param waitInternal Should we wait for messages in the networklayer? * @param waitInternal Should we wait for messages in the networklayer?
* Otherwise, the timeouts for the networklayers are set to zero. * Otherwise, the timeouts for the networklayers are set to zero.
* The default max wait time is 200ms. * The default max wait time is 500ms.
* @return Returns how long we can wait until the next scheduled * @return Returns how long we can wait until the next scheduled
* callback (in ms) */ * callback (in ms) */
UA_EXPORT UA_UInt16 UA_EXPORT UA_UInt16
UA_Server_run_iterate(UA_Server *server, UA_Boolean waitInternal); UA_Server_run_iterate(UA_Server *server, UA_Boolean waitInternal);
/* The epilogue part of UA_Server_run (no need to use if you call /* The epilogue part of UA_Server_run (no need to use if you call
* UA_Server_run or UA_Server_runUntilInterrupt) */ * UA_Server_run or UA_Server_runUntilInterrupt).
* With an external EventLoop, this initiates shutdown without waiting for the
* drivers to stop. While the server is STOPPING, drive the EventLoop and call
* this function again to check completion. */
UA_EXPORT UA_StatusCode UA_EXPORT UA_StatusCode
UA_Server_run_shutdown(UA_Server *server); UA_Server_run_shutdown(UA_Server *server);
+15
View File
@@ -1307,6 +1307,14 @@ UA_Server_run_iterate(UA_Server *server, UA_Boolean waitInternal) {
UA_UInt32 timeout = (waitInternal) ? UA_MAXTIMEOUT : 0; UA_UInt32 timeout = (waitInternal) ? UA_MAXTIMEOUT : 0;
el->run(el, timeout); el->run(el, timeout);
/* External shutdown can require several iterations and work performed by
* the caller between them. Keep the EventLoop running for its other users. */
lockServer(server);
if(server->config.externalEventLoop &&
server->state == UA_LIFECYCLESTATE_STOPPING && testStoppedCondition(server))
setServerLifecycleState(server, UA_LIFECYCLESTATE_STOPPED);
unlockServer(server);
/* Return the time until the next scheduled callback */ /* Return the time until the next scheduled callback */
UA_DateTime now = el->dateTime_nowMonotonic(el); UA_DateTime now = el->dateTime_nowMonotonic(el);
UA_DateTime nextTimeout = (el->nextTimer(el) - now) / UA_DATETIME_MSEC; UA_DateTime nextTimeout = (el->nextTimer(el) - now) / UA_DATETIME_MSEC;
@@ -1333,6 +1341,13 @@ UA_Server_run_shutdown(UA_Server *server) {
lockServer(server); lockServer(server);
/* Recheck an externally driven shutdown without stopping the drivers again. */
if(server->config.externalEventLoop && server->state == UA_LIFECYCLESTATE_STOPPING) {
UA_StatusCode res = finishShutdown(server);
unlockServer(server);
return res;
}
if(server->state != UA_LIFECYCLESTATE_STARTED) { if(server->state != UA_LIFECYCLESTATE_STARTED) {
UA_LOG_ERROR(server->config.logging, UA_LOGCATEGORY_SERVER, UA_LOG_ERROR(server->config.logging, UA_LOGCATEGORY_SERVER,
"The server is not started, cannot be shut down"); "The server is not started, cannot be shut down");
+140
View File
@@ -260,6 +260,144 @@ START_TEST(checkSharedExternalEventLoop) {
firstConfig->externalEventLoop = false; firstConfig->externalEventLoop = false;
} END_TEST } END_TEST
static void
checkLifecycleNotifications(void) {
ck_assert_uint_eq(receivedNotificationsCount, 3);
ck_assert_int_eq(receivedNotifications[0], UA_APPLICATIONNOTIFICATIONTYPE_LIFECYCLE_STARTED);
ck_assert_int_eq(receivedNotifications[1], UA_APPLICATIONNOTIFICATIONTYPE_LIFECYCLE_STOPPING);
ck_assert_int_eq(receivedNotifications[2], UA_APPLICATIONNOTIFICATIONTYPE_LIFECYCLE_STOPPED);
}
static void
stopExternalEventLoop(void) {
UA_ServerConfig *config = UA_Server_getConfig(server);
UA_EventLoop *el = config->eventLoop;
ck_assert_int_eq(el->state, UA_EVENTLOOPSTATE_STARTED);
el->stop(el);
for(unsigned i = 0; i < 32 && el->state != UA_EVENTLOOPSTATE_STOPPED; i++)
ck_assert_uint_eq(el->run(el, 0), UA_STATUSCODE_GOOD);
ck_assert_int_eq(el->state, UA_EVENTLOOPSTATE_STOPPED);
/* Return ownership for the normal config cleanup, including the logger. */
config->externalEventLoop = false;
}
static void
iterateShutdownServer(UA_Boolean driveEventLoop) {
if(!driveEventLoop) {
UA_Server_run_iterate(server, false);
return;
}
UA_EventLoop *el = UA_Server_getConfig(server)->eventLoop;
ck_assert_uint_eq(el->run(el, 0), UA_STATUSCODE_GOOD);
if(UA_Server_getLifecycleState(server) == UA_LIFECYCLESTATE_STOPPING)
ck_assert_uint_eq(UA_Server_run_shutdown(server), UA_STATUSCODE_GOOD);
}
START_TEST(checkExternalShutdownTcp) {
UA_ServerConfig *config = UA_Server_getConfig(server);
config->externalEventLoop = true;
ck_assert_uint_eq(config->serverUrlsSize, 1);
UA_String_clear(&config->serverUrls[0]);
config->serverUrls[0] = UA_STRING_ALLOC("opc.tcp://127.0.0.1:0");
ck_assert_uint_eq(UA_Server_run_startup(server), UA_STATUSCODE_GOOD);
ck_assert_uint_gt(config->applicationDescription.discoveryUrlsSize, 0);
for(UA_Driver *drv = UA_Server_getDrivers(server); drv; drv = drv->next)
ck_assert_int_eq(drv->state, UA_LIFECYCLESTATE_STARTED);
ck_assert_uint_eq(UA_Server_run_shutdown(server), UA_STATUSCODE_GOOD);
ck_assert_int_eq(UA_Server_getLifecycleState(server), UA_LIFECYCLESTATE_STOPPING);
for(unsigned i = 0; i < 32 &&
UA_Server_getLifecycleState(server) != UA_LIFECYCLESTATE_STOPPED; i++)
iterateShutdownServer((UA_Boolean)_i);
ck_assert_int_eq(UA_Server_getLifecycleState(server), UA_LIFECYCLESTATE_STOPPED);
for(UA_Driver *drv = UA_Server_getDrivers(server); drv; drv = drv->next)
ck_assert_int_eq(drv->state, UA_LIFECYCLESTATE_STOPPED);
iterateShutdownServer((UA_Boolean)_i);
iterateShutdownServer((UA_Boolean)_i);
checkLifecycleNotifications();
stopExternalEventLoop();
ck_assert_uint_eq(UA_Server_delete(server), UA_STATUSCODE_GOOD);
server = NULL;
} END_TEST
typedef struct {
UA_Driver driver;
UA_DelayedCallback callback;
UA_Boolean mayStop;
unsigned turns;
unsigned stops;
} DelayedStopDriver;
static void
delayedDriverFinish(void *application, void *context) {
DelayedStopDriver *test = (DelayedStopDriver*)context;
UA_Server *s = (UA_Server*)application;
UA_EventLoop *el = UA_Server_getConfig(s)->eventLoop;
lockServer(s);
test->turns++;
if(test->mayStop && test->turns >= 2)
test->driver.state = UA_LIFECYCLESTATE_STOPPED;
else
el->addDelayedCallback(el, &test->callback);
unlockServer(s);
}
static void
delayedDriverStop(UA_Driver *drv) {
DelayedStopDriver *test = (DelayedStopDriver*)drv;
test->stops++;
drv->state = UA_LIFECYCLESTATE_STOPPING;
test->callback.callback = delayedDriverFinish;
test->callback.application = drv->server;
test->callback.context = test;
UA_EventLoop *el = UA_Server_getConfig(drv->server)->eventLoop;
el->addDelayedCallback(el, &test->callback);
}
START_TEST(checkShutdownDelayedDriver) {
UA_ServerConfig *config = UA_Server_getConfig(server);
config->tcpEnabled = false;
config->externalEventLoop = (_i != 0);
ck_assert_uint_eq(UA_Server_run_startup(server), UA_STATUSCODE_GOOD);
DelayedStopDriver test;
memset(&test, 0, sizeof(test));
test.driver.state = UA_LIFECYCLESTATE_STARTED;
test.driver.stop = delayedDriverStop;
test.mayStop = !config->externalEventLoop;
ck_assert_uint_eq(UA_Server_addDriver(server, &test.driver), UA_STATUSCODE_GOOD);
ck_assert_uint_eq(UA_Server_run_shutdown(server), UA_STATUSCODE_GOOD);
if(config->externalEventLoop) {
ck_assert_uint_eq(test.turns, 0);
/* EventLoops may process delayed callbacks more than once per iteration. */
for(unsigned i = 0; i < 3; i++) {
unsigned turns = test.turns;
iterateShutdownServer(_i == 2);
ck_assert_uint_gt(test.turns, turns);
ck_assert_int_eq(UA_Server_getLifecycleState(server), UA_LIFECYCLESTATE_STOPPING);
}
ck_assert_uint_eq(receivedNotificationsCount, 2);
/* The outer loop can process the work that permits the driver to stop. */
unsigned turnsBeforeStop = test.turns;
test.mayStop = true;
iterateShutdownServer(_i == 2);
ck_assert_uint_eq(test.turns, turnsBeforeStop + 1);
iterateShutdownServer(_i == 2);
stopExternalEventLoop();
} else {
ck_assert_uint_eq(test.turns, 2);
ck_assert_int_eq(config->eventLoop->state, UA_EVENTLOOPSTATE_STOPPED);
}
ck_assert_int_eq(UA_Server_getLifecycleState(server), UA_LIFECYCLESTATE_STOPPED);
ck_assert_uint_eq(test.stops, 1);
checkLifecycleNotifications();
ck_assert_uint_eq(UA_Server_removeDriver(server, &test.driver), UA_STATUSCODE_GOOD);
ck_assert_uint_eq(UA_Server_delete(server), UA_STATUSCODE_GOOD);
server = NULL;
} END_TEST
static size_t static size_t
countNoneEndpointTokens(UA_UserTokenType tokenType) { countNoneEndpointTokens(UA_UserTokenType tokenType) {
UA_GetEndpointsRequest request; UA_GetEndpointsRequest request;
@@ -1017,6 +1155,8 @@ int main(void) {
tcase_add_test(tc_call, checkGetStatistics); tcase_add_test(tc_call, checkGetStatistics);
tcase_add_test(tc_call, checkGetLifecycleState); tcase_add_test(tc_call, checkGetLifecycleState);
tcase_add_test(tc_call, checkSharedExternalEventLoop); tcase_add_test(tc_call, checkSharedExternalEventLoop);
tcase_add_loop_test(tc_call, checkExternalShutdownTcp, 0, 2);
tcase_add_loop_test(tc_call, checkShutdownDelayedDriver, 0, 3);
tcase_add_test(tc_call, helloEndpointUrlLimit); tcase_add_test(tc_call, helloEndpointUrlLimit);
tcase_add_test(tc_call, helloTrailingData); tcase_add_test(tc_call, helloTrailingData);
tcase_add_test(tc_call, checkNoneEndpointUserTokens); tcase_add_test(tc_call, checkNoneEndpointUserTokens);