Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/215 async-signal-safe launcher shutdown #753

Merged
merged 7 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions libs/framework/gtest/src/CelixLauncherTestSuite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

#include <gtest/gtest.h>

#include <csignal>
#include <thread>
#include <future>
#include <vector>
#include <string>
#include <pthread.h>

#include "celix_constants.h"
#include "celix_launcher.h"
Expand Down Expand Up @@ -190,19 +192,31 @@ TEST_F(CelixLauncherTestSuite, LaunchWithInvalidConfigPropertiesTest) {
//Then the launch will exit
auto status = future.wait_for(std::chrono::milliseconds{LAUNCH_WAIT_TIMEOUT});
EXPECT_EQ(status, std::future_status::ready);

//When launching the framework with a properties set with a negative shutdown period
auto* props = celix_properties_create();
ASSERT_TRUE(props != nullptr);
celix_properties_setDouble(props, CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS, -1.0);
future = launchInThread({"programName"}, props, 1);
//Then launch will exit
status = future.wait_for(std::chrono::milliseconds{LAUNCH_WAIT_TIMEOUT});
EXPECT_EQ(status, std::future_status::ready);
}


TEST_F(CelixLauncherTestSuite, StopLauncherWithSignalTest) {
auto* props = celix_properties_create();
// When launching the framework
auto future = launchInThread({"programName"}, nullptr, 0);
celix_properties_setDouble(props, CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS, 0.01);
auto future = launchInThread({"programName"}, props, 0);

// Then the launch will not exit, because the framework is running
auto status = future.wait_for(std::chrono::milliseconds{LAUNCH_WAIT_TIMEOUT});
EXPECT_EQ(status, std::future_status::timeout);

// When I stop the framework by mimicking a SIGINT signal
// When I stop the framework by sending a SIGINT signal
int signal = SIGINT;
celix_launcher_stopInternal(&signal);
EXPECT_EQ(0, pthread_kill(pthread_self(), signal));
PengZheng marked this conversation as resolved.
Show resolved Hide resolved

// Then the launch will exit
status = future.wait_for(std::chrono::milliseconds{LAUNCH_WAIT_TIMEOUT});
Expand Down
14 changes: 14 additions & 0 deletions libs/framework/gtest/src/ScheduledEventTestSuite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,21 @@ TEST_F(ScheduledEventTestSuite, InvalidOptionsAndArgumentsTest) {
auto ctx = fw->getFrameworkBundleContext();
celix_scheduled_event_options_t opts{}; // no callback
long scheduledEventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts);
// Then I expect an error
EXPECT_LT(scheduledEventId, 0);

// When I create a scheduled event with negative initial delay
opts.name = "Invalid scheduled event test";
opts.initialDelayInSeconds = -1;
opts.callback = [](void* /*data*/) { FAIL() << "Scheduled event called, but should not be called"; };
scheduledEventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts);
// Then I expect an error
EXPECT_LT(scheduledEventId, 0);

// When I create a scheduled event with negative interval value
opts.initialDelayInSeconds = 0.1;
opts.intervalInSeconds = -1;
scheduledEventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts);
// Then I expect an error
EXPECT_LT(scheduledEventId, 0);

Expand Down
9 changes: 9 additions & 0 deletions libs/framework/include/celix_launcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
extern "C" {
#endif

/**
* @brief Celix launcher environment property to specify interval of the periodic shutdown check performed by launcher.
*
* The launcher will perform a periodic check to see whether to perform a shutdown, and if so, the launcher will
* stop and destroy the framework. The interval of this check can be specified in seconds using this property.
*/
#define CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS "CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS"
#define CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS_DEFAULT 1.0

/**
* @brief Launch a celix framework, wait (block) until the framework is stopped and destroy the framework when stopped.
*
Expand Down
2 changes: 1 addition & 1 deletion libs/framework/src/bundle_context.c
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ celix_dependency_manager_t* celix_bundleContext_getDependencyManager(bundle_cont
if (ctx->mng) {
return ctx->mng;
}
celixThreadRwlock_unlock(celix_steal_ptr(rlockGuard.lock));
celixRwlockRlockGuard_deinit(&rlockGuard);

celix_auto(celix_rwlock_wlock_guard_t) wlockGuard = celixRwlockWlockGuard_init(&ctx->lock);
if (ctx->mng == NULL) {
Expand Down
108 changes: 77 additions & 31 deletions libs/framework/src/celix_launcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "celix_launcher.h"
#include "celix_launcher_private.h"
#include "celix_compiler.h"

#include <signal.h>
#include <stdio.h>
Expand All @@ -36,14 +37,21 @@
#include "celix_file_utils.h"
#include "celix_framework_factory.h"
#include "celix_framework_utils.h"
#include "celix_threads.h"

#define DEFAULT_CONFIG_FILE "config.properties"

#define CELIX_LAUNCHER_OK_EXIT_CODE 0
#define CELIX_LAUNCHER_ERROR_EXIT_CODE 1

static bool g_framework_launched = false;
static framework_t* g_framework = NULL;
typedef struct {
celix_thread_mutex_t lock; // protect the following
framework_t* framework;
long shutdownEventId;
bool launched;
bool shutdown; // accessed through atomic operations
int signal; // accessed through atomic operations
} celix_launcher_t;

typedef struct {
bool showHelp;
Expand All @@ -52,6 +60,8 @@ typedef struct {
const char* configFile;
} celix_launcher_options_t;

static celix_launcher_t g_launcher = { PTHREAD_MUTEX_INITIALIZER, NULL, -1L, false, false, -1 };

/**
* @brief SIGUSR1 SIGUSR2 no-op callback handler.
*/
Expand Down Expand Up @@ -120,7 +130,7 @@ static celix_status_t celix_launcher_loadRuntimeProperties(const char* configFil
/**
* @brief Set the global framework instance.
*/
static void celix_launcher_setGlobalFramework(celix_framework_t* fw);
static celix_status_t celix_launcher_setGlobalFramework(celix_framework_t* fw);

int celix_launcher_launchAndWait(int argc, char* argv[], const char* embeddedConfig) {
celix_autoptr(celix_properties_t) embeddedProps = NULL;
Expand Down Expand Up @@ -167,18 +177,19 @@ int celix_launcher_launchAndWait(int argc, char* argv[], const char* embeddedCon

celix_framework_t* framework = NULL;
status = celix_launcher_createFramework(celix_steal_ptr(embeddedProps), runtimeProps, &framework);
if (status != CELIX_SUCCESS) {
celix_launcher_resetLauncher();
return CELIX_LAUNCHER_ERROR_EXIT_CODE;
}
status = celix_launcher_setGlobalFramework(framework);
if (status == CELIX_SUCCESS) {
celix_launcher_setGlobalFramework(framework);
celix_framework_waitForStop(framework);
celix_frameworkFactory_destroyFramework(framework);
}
celix_launcher_resetLauncher();
#ifndef CELIX_NO_CURLINIT
// Cleanup Curl
curl_global_cleanup();
// Cleanup Curl
curl_global_cleanup();
#endif
celix_launcher_resetLauncher();
} else {
celix_launcher_resetLauncher();
}
return status == CELIX_SUCCESS ? CELIX_LAUNCHER_OK_EXIT_CODE : CELIX_LAUNCHER_ERROR_EXIT_CODE;
}

Expand Down Expand Up @@ -245,12 +256,14 @@ static celix_status_t celix_launcher_createFramework(celix_properties_t* embedde
return *frameworkOut != NULL ? CELIX_SUCCESS : CELIX_FRAMEWORK_EXCEPTION;
}

// LCOV_EXCL_START
/**
* @brief SIGUSR1 SIGUSR2 no-op callback
*/
static void celix_launcher_noopSignalHandler(int signal __attribute__((unused))) {
// ignoring signal SIGUSR1, SIGUSR2. Can be used on threads
}
// LCOV_EXCL_STOP

static void celix_launcher_printUsage(char* progName) {
printf("Usage:\n %s [-h|-p|-c] [path/to/runtime/config.properties]\n", basename(progName));
Expand Down Expand Up @@ -364,40 +377,73 @@ static celix_status_t celix_launcher_loadRuntimeProperties(const char* configFil
}

static void celix_launcher_shutdownFrameworkSignalHandler(int signal) {
celix_launcher_stopInternal(&signal);
__atomic_store_n(&g_launcher.signal, signal, __ATOMIC_RELAXED);
PengZheng marked this conversation as resolved.
Show resolved Hide resolved
__atomic_store_n(&g_launcher.shutdown, true, __ATOMIC_RELEASE);
}

void celix_launcher_triggerStop() {
celix_launcher_stopInternal(NULL);
}

static void celix_launcher_setGlobalFramework(celix_framework_t* fw) {
__atomic_store_n(&g_framework, fw, __ATOMIC_SEQ_CST);
celix_auto(celix_mutex_lock_guard_t) lck = celixMutexLockGuard_init(&g_launcher.lock);
if (g_launcher.framework == NULL || g_launcher.shutdownEventId == -1) {
fprintf(stderr, "No global framework instance to stop\n");
return;
}
__atomic_store_n(&g_launcher.shutdown, true, __ATOMIC_RELAXED);
celix_bundleContext_wakeupScheduledEvent(celix_framework_getFrameworkContext(g_launcher.framework),
g_launcher.shutdownEventId);
}

void celix_launcher_stopInternal(const int* signal) {
celix_framework_t* fw = __atomic_exchange_n(&g_framework, NULL, __ATOMIC_SEQ_CST);
if (fw) {
if (signal) {
celix_bundle_context_t* ctx = celix_framework_getFrameworkContext(fw);
static void celix_launcher_shutdownCheck(void* data CELIX_UNUSED) {
celix_auto(celix_mutex_lock_guard_t) lck = celixMutexLockGuard_init(&g_launcher.lock);
if (__atomic_load_n(&g_launcher.shutdown, __ATOMIC_ACQUIRE)) {
int sig = __atomic_load_n(&g_launcher.signal, __ATOMIC_RELAXED);
if (sig != -1) {
celix_bundleContext_log(
ctx, CELIX_LOG_LEVEL_INFO, "Stopping Celix framework due to signal %s", strsignal(*signal));
celix_framework_getFrameworkContext(g_launcher.framework), CELIX_LOG_LEVEL_INFO,
"Stopping Celix framework due to signal %s", strsignal(sig));
}
celix_framework_stopBundle(fw, CELIX_FRAMEWORK_BUNDLE_ID);
} else {
fprintf(stderr, "No global framework instance to stop\n");
celix_bundleContext_removeScheduledEventAsync(celix_framework_getFrameworkContext(g_launcher.framework),
g_launcher.shutdownEventId);
celix_framework_stopBundleAsync(g_launcher.framework, CELIX_FRAMEWORK_BUNDLE_ID);
g_launcher.shutdownEventId = -1;
}
}

static celix_status_t celix_launcher_setGlobalFramework(celix_framework_t* fw) {
celix_auto(celix_mutex_lock_guard_t) lck = celixMutexLockGuard_init(&g_launcher.lock);
celix_bundle_context_t *ctx = celix_framework_getFrameworkContext(fw);
g_launcher.framework = fw;
celix_scheduled_event_options_t opts = CELIX_EMPTY_SCHEDULED_EVENT_OPTIONS;
opts.name = "celix_shutdown_check";
opts.callback = celix_launcher_shutdownCheck;
opts.callbackData = &g_launcher;
opts.initialDelayInSeconds = celix_bundleContext_getPropertyAsDouble(ctx, CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS,
CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS_DEFAULT);
opts.intervalInSeconds = celix_bundleContext_getPropertyAsDouble(ctx, CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS,
CELIX_LAUNCHER_SHUTDOWN_PERIOD_IN_SECONDS_DEFAULT);
g_launcher.shutdownEventId = celix_bundleContext_scheduleEvent(ctx, &opts);
return g_launcher.shutdownEventId >= 0 ? CELIX_SUCCESS : CELIX_FRAMEWORK_EXCEPTION;
}

static bool celix_launcher_checkFrameworkLaunched() {
bool alreadyLaunched = __atomic_exchange_n(&g_framework_launched, true, __ATOMIC_SEQ_CST);
if (alreadyLaunched) {
celix_auto(celix_mutex_lock_guard_t) lck = celixMutexLockGuard_init(&g_launcher.lock);
if (g_launcher.launched) {
fprintf(stderr, "Cannot launch framework, already launched\n");
return false;
}
return !alreadyLaunched;
g_launcher.launched = true;
return true;
}

static void celix_launcher_resetLauncher() {
__atomic_store_n(&g_framework_launched, false, __ATOMIC_SEQ_CST);
__atomic_store_n(&g_framework, NULL, __ATOMIC_SEQ_CST);
celix_auto(celix_mutex_lock_guard_t) lck = celixMutexLockGuard_init(&g_launcher.lock);
__atomic_store_n(&g_launcher.shutdown, false, __ATOMIC_RELAXED);
__atomic_store_n(&g_launcher.signal, -1, __ATOMIC_RELAXED);
if (g_launcher.framework) {
long schedId = g_launcher.shutdownEventId;
g_launcher.shutdownEventId = -1L;
celix_bundleContext_removeScheduledEvent(celix_framework_getFrameworkContext(g_launcher.framework), schedId);
celix_frameworkFactory_destroyFramework(g_launcher.framework);
g_launcher.framework = NULL;
}
g_launcher.launched = false;
}
9 changes: 0 additions & 9 deletions libs/framework/src/celix_launcher_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@
extern "C" {
#endif

/**
* @brief Stop the framework, by stopping the framework bundle.
*
* Also logs a message if the framework is stopped due to a signal.
*
* @param signal The optional signal that caused the framework to stop.
*/
void celix_launcher_stopInternal(const int* signal);

/**
* @brief Create a combined configuration properties set by combining the embedded properties with the runtime
* properties.
Expand Down
10 changes: 9 additions & 1 deletion libs/framework/src/framework.c
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,7 @@ static void celix_framework_processScheduledEvents(celix_framework_t* fw) {
celixThreadMutex_lock(&fw->dispatcher.mutex);
CELIX_LONG_HASH_MAP_ITERATE(fw->dispatcher.scheduledEvents, entry) {
celix_scheduled_event_t* visit = entry.value.ptrValue;
if (celix_scheduledEvent_isMarkedForRemoval(visit)) {
if (!fw->dispatcher.active || celix_scheduledEvent_isMarkedForRemoval(visit)) {
removeEvent = visit;
celix_longHashMap_remove(fw->dispatcher.scheduledEvents, celix_scheduledEvent_getId(visit));
break;
Expand Down Expand Up @@ -1531,6 +1531,7 @@ static void *fw_eventDispatcher(void *fw) {
}

//not active anymore, extra runs for possible request leftovers
celix_framework_processScheduledEvents(framework);
celixThreadMutex_lock(&framework->dispatcher.mutex);
bool needExtraRun = celix_framework_eventQueueSize(fw) > 0;
celixThreadMutex_unlock(&framework->dispatcher.mutex);
Expand Down Expand Up @@ -2542,6 +2543,13 @@ long celix_framework_scheduleEvent(celix_framework_t* fw,
bndId);
return -1;
}
if (initialDelayInSeconds < 0 || intervalInSeconds < 0) {
fw_log(fw->logger,
CELIX_LOG_LEVEL_ERROR,
"Cannot add scheduled event for bundle id %li. Invalid intervals: (%f,%f).",
bndId, initialDelayInSeconds, intervalInSeconds);
return -1;
}

celix_bundle_entry_t* bndEntry = celix_framework_bundleEntry_getBundleEntryAndIncreaseUseCount(fw, bndId);
if (bndEntry == NULL) {
Expand Down
Loading