diff --git a/.clang-format b/.clang-format index 2766cd9cc..1069ad002 100644 --- a/.clang-format +++ b/.clang-format @@ -12,7 +12,7 @@ PointerAlignment: Left SpacesBeforeTrailingComments: 1 SortIncludes: true --- -#Include regroup disabled by default. +#Include regroup disabled by default. #IncludeBlocks: Regroup #IncludeCategories: ##gtest headers diff --git a/bundles/logging/log_admin/gtest/src/LogAdminTestSuite.cc b/bundles/logging/log_admin/gtest/src/LogAdminTestSuite.cc index b0a7220ad..9f0d403eb 100644 --- a/bundles/logging/log_admin/gtest/src/LogAdminTestSuite.cc +++ b/bundles/logging/log_admin/gtest/src/LogAdminTestSuite.cc @@ -83,11 +83,11 @@ TEST_F(LogBundleTestSuite, NrOfLogServices) { EXPECT_EQ(1, control->nrOfLogServices(control->handle, nullptr)); //default the framework log services is available //request "default" log service - long trkId1 = celix_bundleContext_trackService(ctx.get(), CELIX_LOG_SERVICE_NAME, NULL, NULL); + long trkId1 = celix_bundleContext_trackService(ctx.get(), CELIX_LOG_SERVICE_NAME, nullptr, nullptr); EXPECT_EQ(2, control->nrOfLogServices(control->handle, nullptr)); //request "default" log service -> already created - long trkId2 = celix_bundleContext_trackService(ctx.get(), CELIX_LOG_SERVICE_NAME, NULL, NULL); + long trkId2 = celix_bundleContext_trackService(ctx.get(), CELIX_LOG_SERVICE_NAME, nullptr, nullptr); EXPECT_EQ(2, control->nrOfLogServices(control->handle, nullptr)); //request a 'logger1' log service @@ -224,7 +224,7 @@ TEST_F(LogBundleTestSuite, SinkLogControl) { TEST_F(LogBundleTestSuite, LogServiceControl) { //request "default" log service - long trkId1 = celix_bundleContext_trackService(ctx.get(), CELIX_LOG_SERVICE_NAME, NULL, NULL); + long trkId1 = celix_bundleContext_trackService(ctx.get(), CELIX_LOG_SERVICE_NAME, nullptr, nullptr); celix_framework_waitForEmptyEventQueue(fw.get()); EXPECT_EQ(2, control->nrOfLogServices(control->handle, nullptr)); @@ -328,7 +328,7 @@ TEST_F(LogBundleTestSuite, LogServiceAndSink) { //request a 'logger1' log service long trkId; - std::atomic logSvc; + std::atomic logSvc{}; { celix_service_tracking_options_t opts{}; opts.filter.serviceName = CELIX_LOG_SERVICE_NAME; @@ -471,7 +471,7 @@ TEST_F(LogBundleTestSuite, LogAdminCmd) { opts.filter.serviceName = CELIX_SHELL_COMMAND_SERVICE_NAME; opts.use = [](void*, void *svc) { auto* cmd = static_cast(svc); - char *cmdResult = NULL; + char *cmdResult = nullptr; size_t cmdResultLen; FILE *ss = open_memstream(&cmdResult, &cmdResultLen); cmd->executeCommand(cmd->handle, "celix::log_admin", ss, ss); //overview @@ -487,7 +487,7 @@ TEST_F(LogBundleTestSuite, LogAdminCmd) { opts.use = [](void*, void *svc) { auto* cmd = static_cast(svc); - char *cmdResult = NULL; + char *cmdResult = nullptr; size_t cmdResultLen; FILE *ss = open_memstream(&cmdResult, &cmdResultLen); cmd->executeCommand(cmd->handle, "celix::log_admin log fatal", ss, ss); //all @@ -504,7 +504,7 @@ TEST_F(LogBundleTestSuite, LogAdminCmd) { opts.use = [](void*, void *svc) { auto* cmd = static_cast(svc); - char *cmdResult = NULL; + char *cmdResult = nullptr; size_t cmdResultLen; FILE *ss = open_memstream(&cmdResult, &cmdResultLen); cmd->executeCommand(cmd->handle, "celix::log_admin sink false", ss, ss); //all @@ -523,9 +523,9 @@ TEST_F(LogBundleTestSuite, LogAdminCmd) { opts.use = [](void*, void *svc) { auto* cmd = static_cast(svc); - char *cmdResult = NULL; + char *cmdResult = nullptr; size_t cmdResultLen; - char *errResult = NULL; + char *errResult = nullptr; size_t errResultLen; FILE *ss = open_memstream(&cmdResult, &cmdResultLen); FILE *es = open_memstream(&errResult, &errResultLen); @@ -586,7 +586,7 @@ TEST_F(LogBundleTestSuite, LogAdminCmd) { opts.use = [](void*, void *svc) { auto* cmd = static_cast(svc); - char *cmdResult = NULL; + char *cmdResult = nullptr; size_t cmdResultLen; FILE *ss = open_memstream(&cmdResult, &cmdResultLen); cmd->executeCommand(cmd->handle, "celix::log_admin", ss, ss); diff --git a/bundles/pubsub/pubsub_topology_manager/src/pubsub_topology_manager.c b/bundles/pubsub/pubsub_topology_manager/src/pubsub_topology_manager.c index c73cc5876..fc055db02 100644 --- a/bundles/pubsub/pubsub_topology_manager/src/pubsub_topology_manager.c +++ b/bundles/pubsub/pubsub_topology_manager/src/pubsub_topology_manager.c @@ -1169,7 +1169,6 @@ void pubsub_topologyManager_removeMetricsService(void * handle, void *svc, const celixThreadMutex_unlock(&manager->psaMetrics.mutex); } - static celix_status_t pubsub_topologyManager_topology(pubsub_topology_manager_t *manager, const char *commandLine __attribute__((unused)), FILE *os, FILE *errorStream __attribute__((unused))) { fprintf(os, "\n"); diff --git a/bundles/remote_services/topology_manager/tms_tst/tms_tests.cpp b/bundles/remote_services/topology_manager/tms_tst/tms_tests.cpp index 92bc86b6e..500a9baec 100644 --- a/bundles/remote_services/topology_manager/tms_tst/tms_tests.cpp +++ b/bundles/remote_services/topology_manager/tms_tst/tms_tests.cpp @@ -497,8 +497,13 @@ extern "C" { celix_framework_waitForEmptyEventQueue(framework); - bool imported = testImport->IsImported(testImport); - EXPECT_EQ(true, imported); + //Then endpointImported becomes true within 1 second + bool imported; + int iteration = 0; + do { + imported = testImport->IsImported(testImport); + usleep(1000); + } while (!imported && iteration++ < 1000); rc = eplService->endpointRemoved(eplService->handle, endpoint, NULL); EXPECT_EQ(CELIX_SUCCESS, rc); @@ -544,8 +549,13 @@ extern "C" { celix_framework_waitForEmptyEventQueue(framework); - bool imported = testImport->IsImported(testImport); - EXPECT_EQ(true, imported); + //Then endpointImported becomes true within 1 second + bool imported; + int iteration = 0; + do { + imported = testImport->IsImported(testImport); + usleep(1000); + } while (!imported && iteration++ < 1000); rc = eplService->endpointRemoved(eplService->handle, endpoint, NULL); EXPECT_EQ(CELIX_SUCCESS, rc); @@ -636,8 +646,14 @@ extern "C" { celix_framework_waitForEmptyEventQueue(framework); - celix_framework_waitForEmptyEventQueue(framework); - bool imported = testImport->IsImported(testImport); + //Then endpointImported becomes true within 1 second + bool imported; + int iteration = 0; + do { + imported = testImport->IsImported(testImport); + usleep(1000); + } while (!imported && iteration++ < 1000); + EXPECT_EQ(true, imported); rc = eplService->endpointRemoved(eplService->handle, endpoint, NULL); diff --git a/documents/README.md b/documents/README.md index ea646f41d..23f0d229d 100644 --- a/documents/README.md +++ b/documents/README.md @@ -86,6 +86,7 @@ bundles contains binaries depending on the stdlibc++ library. * [Apache Celix Framework](framework.md) * [Apache Celix Containers](containers.md) * [Apache Celix Patterns](patterns.md) + * [Apache Celix Scheduled Events](scheduled_events.md) * [Apache Celix CMake Commands](cmake_commands) * [Apache Celix Sub Projects](subprojects.md) * [Apache Celix Coding Conventions Guide](development/README.md) diff --git a/documents/framework.md b/documents/framework.md index 01ca45daf..4eb0b99df 100644 --- a/documents/framework.md +++ b/documents/framework.md @@ -259,20 +259,21 @@ So changing the environment variables after the framework is created will not ha The following framework properties are supported: -| Framework Property | Default Value | Description | -|-------------------------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| CELIX_FRAMEWORK_CACHE_DIR | ".cache" | The directory where the Apache Celix framework will store its data. | -| CELIX_FRAMEWORK_CACHE_USE_TMP_DIR | "false" | If true, the Apache Celix framework will use the system temp directory for the cache directory. | -| CELIX_FRAMEWORK_CLEAN_CACHE_DIR_ON_CREATE | "false" | If true, the Apache Celix framework will clean the cache directory on create. | -| CELIX_FRAMEWORK_FRAMEWORK_UUID | "" | The UUID of the Apache Celix framework. If not set, a random UUID will be generated. | -| CELIX_BUNDLES_PATH | "bundles" | The directories where the Apache Celix framework will search for bundles. Multiple directories can be provided separated by a colon. | -| CELIX_LOAD_BUNDLES_WITH_NODELETE | "false" | If true, the Apache Celix framework will load bundle libraries with the RTLD_NODELETE flags. Note for cmake build type Debug, the default is "true", otherwise the default is "false" | -| CELIX_FRAMEWORK_STATIC_EVENT_QUEUE_SIZE | "100" | The size of the static event queue. If more than 100 events in the queue are needed, dynamic memory allocation will be used. | -| CELIX_FRAMEWORK_AUTO_START_0 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_FRAMEWORK_AUTO_START_1 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_FRAMEWORK_AUTO_START_2 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_FRAMEWORK_AUTO_START_3 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_FRAMEWORK_AUTO_START_4 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_FRAMEWORK_AUTO_START_5 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_AUTO_INSTALL | "" | The bundles to install after the framework is started. Multiple bundles can be provided separated by a space. | -| CELIX_LOGGING_DEFAULT_ACTIVE_LOG_LEVEL | "info" | The default active log level for created log services. Possible values are "trace", "debug", "info", "warning", "error" and "fatal". | +| Framework Property | Default Value | Description | +|--------------------------------------------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| CELIX_FRAMEWORK_CACHE_DIR | ".cache" | The directory where the Apache Celix framework will store its data. | +| CELIX_FRAMEWORK_CACHE_USE_TMP_DIR | "false" | If true, the Apache Celix framework will use the system temp directory for the cache directory. | +| CELIX_FRAMEWORK_CLEAN_CACHE_DIR_ON_CREATE | "false" | If true, the Apache Celix framework will clean the cache directory on create. | +| CELIX_FRAMEWORK_FRAMEWORK_UUID | "" | The UUID of the Apache Celix framework. If not set, a random UUID will be generated. | +| CELIX_BUNDLES_PATH | "bundles" | The directories where the Apache Celix framework will search for bundles. Multiple directories can be provided separated by a colon. | +| CELIX_LOAD_BUNDLES_WITH_NODELETE | "false" | If true, the Apache Celix framework will load bundle libraries with the RTLD_NODELETE flags. Note for cmake build type Debug, the default is "true", otherwise the default is "false" | +| CELIX_FRAMEWORK_STATIC_EVENT_QUEUE_SIZE | "100" | The size of the static event queue. If more than 100 events in the queue are needed, dynamic memory allocation will be used. | +| CELIX_FRAMEWORK_AUTO_START_0 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_FRAMEWORK_AUTO_START_1 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_FRAMEWORK_AUTO_START_2 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_FRAMEWORK_AUTO_START_3 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_FRAMEWORK_AUTO_START_4 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_FRAMEWORK_AUTO_START_5 | "" | The bundles to install and start after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_AUTO_INSTALL | "" | The bundles to install after the framework is started. Multiple bundles can be provided separated by a space. | +| CELIX_LOGGING_DEFAULT_ACTIVE_LOG_LEVEL | "info" | The default active log level for created log services. Possible values are "trace", "debug", "info", "warning", "error" and "fatal". | + | CELIX_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS | "2" | The allowed processing time for scheduled events in seconds, if processing takes longer a warning message will be logged. | diff --git a/documents/scheduled_events .md b/documents/scheduled_events .md new file mode 100644 index 000000000..988c73401 --- /dev/null +++ b/documents/scheduled_events .md @@ -0,0 +1,136 @@ +--- +title: Apache Celix Events +--- + + + +# Apache Celix Scheduled Events + +Apache Celix provides supports to schedule events on the Apache Celix Event Thread. This allows users to +reuse the existing Apache Celix events thread to do tasks on the event thread. + +Scheduled events will be called repeatedly using a provided interval or once if +only an initial delay is provided. For the interval time, a monotonic clock is used. + +The event callback should be relatively fast, and the scheduled event interval should be relatively high; otherwise, +the framework event queue will be blocked, and the framework will not function properly. +Network IO should not be done in the event callback, but instead, a separate thread should be used for this. + +## Scheduling an Event + +To schedule an event in the Apache Celix framework, use the `celix_bundleContext_scheduleEvent` C function or +`celix::BundleContext::scheduleEvent` C++ method. For C, an options struct is used to configure the scheduled event, +and for C++, a builder pattern is used to configure the scheduled event. + +### C Example + +```c +#include +#include + +typedef struct schedule_events_bundle_activator_data { + celix_bundle_context_t* ctx; + long scheduledEventId; +} schedule_events_bundle_activator_data_t; + +void scheduleEventsBundle_oneShot(void* data) { + schedule_events_bundle_activator_data_t* act = data; + celix_bundleContext_log(act->ctx, CELIX_LOG_LEVEL_INFO, "One shot scheduled event fired"); +} + +void scheduleEventsBundle_process(void* data) { + schedule_events_bundle_activator_data_t* act = data; + celix_bundleContext_log(act->ctx, CELIX_LOG_LEVEL_INFO, "Recurring scheduled event fired"); +} + +static celix_status_t scheduleEventsBundle_start(schedule_events_bundle_activator_data_t *data, celix_bundle_context_t *ctx) { + data->ctx = ctx; + + //schedule recurring event + { + celix_scheduled_event_options_t opts = CELIX_EMPTY_SCHEDULED_EVENT_OPTIONS; + opts.name = "recurring scheduled event example"; + opts.initialDelayInSeconds = 0.1; + opts.intervalInSeconds = 1.0; + opts.callbackData = data; + opts.callback = scheduleEventsBundle_process; + data->scheduledEventId = celix_bundleContext_scheduleEvent(ctx, &opts); + } + + //schedule one time event + { + celix_scheduled_event_options_t opts = CELIX_EMPTY_SCHEDULED_EVENT_OPTIONS; + opts.name = "one shot scheduled event example"; + opts.initialDelayInSeconds = 0.1; + opts.callbackData = data; + opts.callback = scheduleEventsBundle_oneShot; + celix_bundleContext_scheduleEvent(ctx, &opts); + } + + return CELIX_SUCCESS; +} + +static celix_status_t scheduleEventsBundle_stop(schedule_events_bundle_activator_data_t *data, celix_bundle_context_t *ctx) { + celix_bundleContext_removeScheduledEvent(ctx, data->scheduledEventId); + return CELIX_SUCCESS; +} + +CELIX_GEN_BUNDLE_ACTIVATOR(schedule_events_bundle_activator_data_t, scheduleEventsBundle_start, scheduleEventsBundle_stop) +``` + +### C++ Example + +```cpp +#include +#include "celix/BundleActivator.h" + +class ScheduleEventsBundleActivator { +public: + explicit ScheduleEventsBundleActivator(const std::shared_ptr& ctx) { + //schedule recurring event + event = ctx->scheduledEvent() + .withInitialDelay(std::chrono::milliseconds{10}) + .withInterval(std::chrono::seconds{1}) + .withCallback([ctx] { + ctx->logInfo("Recurring scheduled event fired"); + }) + .build(); + + //schedule one time event + ctx->scheduledEvent() + .withInitialDelay(std::chrono::milliseconds{10}) + .withCallback([ctx] { + ctx->logInfo("One shot scheduled event fired"); + }) + .build(); + } + + ~ScheduleEventsBundleActivator() noexcept { + std::cout << "Goodbye world" << std::endl; + } +private: + celix::ScheduledEvent event{}; +}; + +CELIX_GEN_CXX_BUNDLE_ACTIVATOR(ScheduleEventsBundleActivator) +``` + +## Waking up a Scheduled Event + +To process a scheduled event directly, you can use the `celix_bundleContext_wakeupScheduledEvent` C function or +`celix::ScheduledEvent::wakup` C++ method. This will wake up the scheduled event and call its callback function. diff --git a/examples/celix-examples/readme_c_examples/CMakeLists.txt b/examples/celix-examples/readme_c_examples/CMakeLists.txt index 6ea7c06d1..e934c067a 100644 --- a/examples/celix-examples/readme_c_examples/CMakeLists.txt +++ b/examples/celix-examples/readme_c_examples/CMakeLists.txt @@ -77,6 +77,11 @@ add_celix_bundle(component_with_service_dependency_bundle ) target_link_libraries(component_with_service_dependency_bundle PRIVATE Celix::shell_api) +add_celix_bundle(schedule_events_bundle + VERSION 1.0.0 + SOURCES src/schedule_events_bundle_activator.c +) + add_celix_container(readme_c_examples_container C BUNDLES @@ -88,6 +93,7 @@ add_celix_container(readme_c_examples_container simple_component_bundle component_with_provided_service_bundle component_with_service_dependency_bundle + schedule_events_bundle ) diff --git a/examples/celix-examples/readme_c_examples/src/schedule_events_bundle_activator.c b/examples/celix-examples/readme_c_examples/src/schedule_events_bundle_activator.c new file mode 100644 index 000000000..b17188392 --- /dev/null +++ b/examples/celix-examples/readme_c_examples/src/schedule_events_bundle_activator.c @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include +#include + +typedef struct schedule_events_bundle_activator_data { + celix_bundle_context_t* ctx; + long scheduledEventId; +} schedule_events_bundle_activator_data_t; + +void scheduleEventsBundle_oneShot(void* data) { + schedule_events_bundle_activator_data_t* act = data; + celix_bundleContext_log(act->ctx, CELIX_LOG_LEVEL_INFO, "One shot scheduled event fired"); +} + +void scheduleEventsBundle_process(void* data) { + schedule_events_bundle_activator_data_t* act = data; + celix_bundleContext_log(act->ctx, CELIX_LOG_LEVEL_INFO, "Recurring scheduled event fired"); +} + +static celix_status_t scheduleEventsBundle_start(schedule_events_bundle_activator_data_t *data, celix_bundle_context_t *ctx) { + data->ctx = ctx; + + //schedule recurring event + { + celix_scheduled_event_options_t opts = CELIX_EMPTY_SCHEDULED_EVENT_OPTIONS; + opts.name = "recurring scheduled event example"; + opts.initialDelayInSeconds = 0.1; + opts.intervalInSeconds = 1.0; + opts.callbackData = data; + opts.callback = scheduleEventsBundle_process; + data->scheduledEventId = celix_bundleContext_scheduleEvent(ctx, &opts); + } + + //schedule one time event + { + celix_scheduled_event_options_t opts = CELIX_EMPTY_SCHEDULED_EVENT_OPTIONS; + opts.name = "one shot scheduled event example"; + opts.initialDelayInSeconds = 0.1; + opts.callbackData = data; + opts.callback = scheduleEventsBundle_oneShot; + celix_bundleContext_scheduleEvent(ctx, &opts); + } + + return CELIX_SUCCESS; +} + +static celix_status_t scheduleEventsBundle_stop(schedule_events_bundle_activator_data_t *data, celix_bundle_context_t *ctx) { + celix_bundleContext_removeScheduledEvent(ctx, data->scheduledEventId); + return CELIX_SUCCESS; +} + +CELIX_GEN_BUNDLE_ACTIVATOR(schedule_events_bundle_activator_data_t, scheduleEventsBundle_start, scheduleEventsBundle_stop) diff --git a/examples/celix-examples/readme_cxx_examples/CMakeLists.txt b/examples/celix-examples/readme_cxx_examples/CMakeLists.txt index 1656d373a..0ee6b9554 100644 --- a/examples/celix-examples/readme_cxx_examples/CMakeLists.txt +++ b/examples/celix-examples/readme_cxx_examples/CMakeLists.txt @@ -140,6 +140,12 @@ add_celix_bundle(ComponentWithServiceDependencyBundle ) target_link_libraries(ComponentWithServiceDependencyBundle PRIVATE Celix::shell_api) + +add_celix_bundle(ScheduleEventsBundle + VERSION 1.0.0 + SOURCES src/ScheduleEventsBundleActivator.cc +) + add_celix_container(ReadmeCxxExamplesContainer BUNDLES Celix::ShellCxx @@ -151,6 +157,7 @@ add_celix_container(ReadmeCxxExamplesContainer SimpleComponentBundle ComponentWithProvidedServiceBundle ComponentWithServiceDependencyBundle + ScheduleEventsBundle ) if (TARGET my_shell_command_provider_bundle) diff --git a/examples/celix-examples/readme_cxx_examples/src/ScheduleEventsBundleActivator.cc b/examples/celix-examples/readme_cxx_examples/src/ScheduleEventsBundleActivator.cc new file mode 100644 index 000000000..fa4d7415b --- /dev/null +++ b/examples/celix-examples/readme_cxx_examples/src/ScheduleEventsBundleActivator.cc @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include +#include "celix/BundleActivator.h" + +class ScheduleEventsBundleActivator { +public: + explicit ScheduleEventsBundleActivator(const std::shared_ptr& ctx) { + //schedule recurring event + event = ctx->scheduledEvent() + .withInitialDelay(std::chrono::milliseconds{10}) + .withInterval(std::chrono::seconds{1}) + .withCallback([ctx] { + ctx->logInfo("Recurring scheduled event fired"); + }) + .build(); + + //schedule one time event + ctx->scheduledEvent() + .withInitialDelay(std::chrono::milliseconds{10}) + .withCallback([ctx] { + ctx->logInfo("One shot scheduled event fired"); + }) + .build(); + } + + ~ScheduleEventsBundleActivator() noexcept { + std::cout << "Goodbye world" << std::endl; + } +private: + celix::ScheduledEvent event{}; +}; + +CELIX_GEN_CXX_BUNDLE_ACTIVATOR(ScheduleEventsBundleActivator) diff --git a/libs/framework/CMakeLists.txt b/libs/framework/CMakeLists.txt index 82968846b..3cde38606 100644 --- a/libs/framework/CMakeLists.txt +++ b/libs/framework/CMakeLists.txt @@ -34,7 +34,8 @@ set(FRAMEWORK_SRC src/framework_bundle_lifecycle_handler.c src/celix_bundle_state.c src/celix_framework_utils.c - src/celix_module_private.h) + src/celix_scheduled_event.c +) set(FRAMEWORK_DEPS libuuid::libuuid CURL::libcurl ZLIB::ZLIB ${CMAKE_DL_LIBS}) add_library(framework SHARED ${FRAMEWORK_SRC}) diff --git a/libs/framework/gtest/CMakeLists.txt b/libs/framework/gtest/CMakeLists.txt index 2d57dc7ba..1e78c4474 100644 --- a/libs/framework/gtest/CMakeLists.txt +++ b/libs/framework/gtest/CMakeLists.txt @@ -57,6 +57,7 @@ set(CELIX_FRAMEWORK_TEST_SOURCES src/BundleArchiveTestSuite.cc src/CelixLauncherTestSuite.cc src/CelixBundleCacheTestSuite.cc + src/ScheduledEventTestSuite.cc ) add_executable(test_framework ${CELIX_FRAMEWORK_TEST_SOURCES}) @@ -128,7 +129,8 @@ if (LINKER_WRAP_SUPPORTED) src/CelixFrameworkUtilsErrorInjectionTestSuite.cc src/CelixBundleContextBundlesWithErrorTestSuite.cc src/CelixBundleCacheErrorInjectionTestSuite.cc - ) + src/ScheduledEventWithErrorInjectionTestSuite.cc + ) target_compile_definitions(test_framework_with_ei PRIVATE SIMPLE_TEST_BUNDLE1_LOCATION="${SIMPLE_TEST_BUNDLE1}" SIMPLE_CXX_BUNDLE_LOC="${SIMPLE_CXX_BUNDLE_LOC}" diff --git a/libs/framework/gtest/src/CelixBundleContextServicesTestSuite.cc b/libs/framework/gtest/src/CelixBundleContextServicesTestSuite.cc index e5351d4bc..f9115f416 100644 --- a/libs/framework/gtest/src/CelixBundleContextServicesTestSuite.cc +++ b/libs/framework/gtest/src/CelixBundleContextServicesTestSuite.cc @@ -477,7 +477,7 @@ TEST_F(CelixBundleContextServicesTestSuite, UseServiceInUseCallbackTest) { celix_bundleContext_unregisterService(ctx, svcId3); } -TEST_F(CelixBundleContextServicesTestSuite, TegisterAndUseServiceTest) { +TEST_F(CelixBundleContextServicesTestSuite, RegisterAndUseServiceTest) { struct calc { int (*calc)(int); }; diff --git a/libs/framework/gtest/src/CelixFrameworkTestSuite.cc b/libs/framework/gtest/src/CelixFrameworkTestSuite.cc index 95e7d434a..6a695181d 100644 --- a/libs/framework/gtest/src/CelixFrameworkTestSuite.cc +++ b/libs/framework/gtest/src/CelixFrameworkTestSuite.cc @@ -79,6 +79,25 @@ TEST_F(CelixFrameworkTestSuite, EventQueueTest) { EXPECT_EQ(4, count); } +TEST_F(CelixFrameworkTestSuite, TimedWaitEventQueueTest) { + //When there is a emtpy event queue + celix_framework_waitForEmptyEventQueue(framework.get()); + + //And a generic event is fired, that block the queue for 20ms + auto callback = [](void* /*data*/) { + std::this_thread::sleep_for(std::chrono::milliseconds{200}); + }; + celix_framework_fireGenericEvent(framework.get(), -1L, -1L, "test", nullptr, callback, nullptr, nullptr); + + //Then a wait for empty event queue for max 5ms will return a timeout + celix_status_t status = celix_framework_waitForEmptyEventQueueFor(framework.get(), 0.005); + EXPECT_EQ(ETIMEDOUT, status) << "Expected timeout, but got " << celix_strerror(status); + + //And a wait for empty event queue for max 1s will return success + status = celix_framework_waitForEmptyEventQueueFor(framework.get(), 1); + EXPECT_EQ(CELIX_SUCCESS, status); +} + TEST_F(CelixFrameworkTestSuite, AsyncInstallStartStopUpdateAndUninstallBundleTest) { long bndId = celix_framework_installBundleAsync(framework.get(), SIMPLE_TEST_BUNDLE1_LOCATION, false); EXPECT_GE(bndId, 0); diff --git a/libs/framework/gtest/src/ScheduledEventTestSuite.cc b/libs/framework/gtest/src/ScheduledEventTestSuite.cc new file mode 100644 index 000000000..90646a4bd --- /dev/null +++ b/libs/framework/gtest/src/ScheduledEventTestSuite.cc @@ -0,0 +1,706 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix/FrameworkFactory.h" +#include "celix_bundle_context.h" +#include "celix_scheduled_event.h" + +class ScheduledEventTestSuite : public ::testing::Test { + public: +#ifdef __APPLE__ + const int ALLOWED_ERROR_MARGIN_IN_MS = 500; +#else + const int ALLOWED_ERROR_MARGIN_IN_MS = 200; +#endif + + const double ALLOWED_PROCESSING_TIME_IN_SECONDS = 0.1; + + ScheduledEventTestSuite() { + fw = celix::createFramework({{"CELIX_LOGGING_DEFAULT_ACTIVE_LOG_LEVEL", "trace"}, + {CELIX_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS, + std::to_string(ALLOWED_PROCESSING_TIME_IN_SECONDS)}}); + } + std::shared_ptr fw{}; + /** + * Wait for the given predicate to become true or the given time has elapsed. + * @param predicate predicate to check. + * @param within maximum time to wait. + */ + template + void waitFor(const std::function& predicate, std::chrono::duration within) { + auto start = std::chrono::steady_clock::now(); + while (!predicate()) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration_cast(now - start); + if (elapsed > within) { + break; + } + } + } +}; + +TEST_F(ScheduledEventTestSuite, OnceShotEventTest) { + auto ctx = fw->getFrameworkBundleContext(); + + struct event_info { + std::atomic count{0}; + std::atomic removed{false}; + }; + event_info info{}; + + auto callback = [](void* data) { + auto* info = static_cast(data); + info->count++; + }; + + auto removeCallback = [](void* data) { + auto* info = static_cast(data); + info->removed = true; + }; + + // When I create a scheduled event with a 0 delay and a 0 interval (one short, directly scheduled) + celix_scheduled_event_options_t opts{}; + opts.callbackData = &info; + opts.callback = callback; + opts.removeCallbackData = &info; + opts.removeCallback = removeCallback; + + // And I schedule the event + long eventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts); + EXPECT_GE(eventId, 0); + + // Then the count becomes 1 within the error margin + waitFor([&]() { return info.count.load() == 1; }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(1, info.count.load()); + + // And the event remove callback is called within the error margin + waitFor([&]() { return info.removed.load(); }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_TRUE(info.removed.load()); + + celix_bundleContext_removeScheduledEvent(ctx->getCBundleContext(), eventId); +} + +TEST_F(ScheduledEventTestSuite, ScheduledEventTest) { + auto ctx = fw->getFrameworkBundleContext(); + + struct event_info { + std::atomic count{0}; + std::atomic removed{false}; + }; + event_info info{}; + + auto callback = [](void* data) { + auto* info = static_cast(data); + info->count++; + }; + + auto removeCallback = [](void* data) { + auto* info = static_cast(data); + info->removed = true; + }; + + // When I create a scheduled event with a 10ms delay and a 20 ms interval + celix_scheduled_event_options_t opts{}; + opts.name = "Scheduled event test"; + opts.initialDelayInSeconds = 0.01; + opts.intervalInSeconds = 0.02; + opts.callbackData = &info; + opts.callback = callback; + opts.removeCallbackData = &info; + opts.removeCallback = removeCallback; + + // And I schedule the event + long eventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts); + EXPECT_GE(eventId, 0); + + // Then count becomes 3 or more within the initial delay + 2 x internal and an allowed error margin (3x) + int allowedTimeInMs = 10 + (2 * 20) + (3 * ALLOWED_ERROR_MARGIN_IN_MS); + waitFor([&]() { return info.count.load() >= 3; }, std::chrono::milliseconds{allowedTimeInMs}); + EXPECT_GE(info.count.load(), 3); + + // And the event remove callback is not called + EXPECT_FALSE(info.removed.load()); + + // And when I remove the event + celix_bundleContext_removeScheduledEvent(ctx->getCBundleContext(), eventId); + + // Then the event remove callback is called + EXPECT_TRUE(info.removed.load()); +} + +TEST_F(ScheduledEventTestSuite, IgnoreNegativeScheduledIdsTest) { + // Scheduled event wakeup, remove functions will ignore negative ids + EXPECT_EQ(CELIX_SUCCESS, + celix_bundleContext_wakeupScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), -1)); + EXPECT_EQ(CELIX_SUCCESS, + celix_bundleContext_removeScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), -1)); +} + +TEST_F(ScheduledEventTestSuite, ManyScheduledEventTest) { + auto ctx = fw->getFrameworkBundleContext(); + + struct event_info { + std::atomic count{0}; + }; + event_info info{}; + auto callback = [](void* data) { + auto* info = static_cast(data); + info->count++; + }; + + std::vector eventIds{}; + + // When 1000 scheduled events are with a random interval between 1 and 59 ms + for (int i = 0; i < 100; ++i) { + // When I create a scheduled event with a 10ms delay and a 20 ms interval + celix_scheduled_event_options_t opts{}; + opts.name = "Scheduled event test"; + opts.intervalInSeconds = (i % 50) / 100.0; // note will also contain one-shot scheduled events + opts.callbackData = &info; + opts.callback = callback; + long eventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts); + EXPECT_GE(eventId, 0); + if (opts.intervalInSeconds > 0) { // not a one-shot event + eventIds.push_back(eventId); + } + } + + // And some time passes, to let some events be called + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + + // Then the events can safely be removed + for (auto id : eventIds) { + celix_bundleContext_removeScheduledEvent(ctx->getCBundleContext(), id); + } + EXPECT_GT(info.count, 0); +} + +TEST_F(ScheduledEventTestSuite, AddWithoutRemoveScheduledEventTest) { + // When I create a scheduled event + auto ctx = fw->getFrameworkBundleContext(); + + auto callback = [](void* /*data*/) { fprintf(stdout, "Scheduled event called\n"); }; + celix_scheduled_event_options_t opts{}; + opts.name = "Un-removed scheduled event test"; + opts.intervalInSeconds = 0.02; + opts.callback = callback; + long scheduledEventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts); + EXPECT_GE(scheduledEventId, 0); + + // And I do not remove the event, but let the bundle framework stop + // Then I expect no memory leaks +} + +TEST_F(ScheduledEventTestSuite, AddWithoutRemoveOneShotEventTest) { + // When I create a one-shot scheduled event with a long initial delay + auto ctx = fw->getFrameworkBundleContext(); + + auto callback = [](void* /*data*/) { FAIL() << "Scheduled event called, but should not be called"; }; + celix_scheduled_event_options_t opts{}; + opts.name = "Un-removed one-shot scheduled event test"; + opts.initialDelayInSeconds = 100; + opts.callback = callback; + long scheduledEventId = celix_bundleContext_scheduleEvent(ctx->getCBundleContext(), &opts); + EXPECT_GE(scheduledEventId, 0); + + // And I do let the one-shot event trigger, but let the bundle framework stop + // Then I expect no memory leaks +} + +TEST_F(ScheduledEventTestSuite, InvalidOptionsAndArgumentsTest) { + // When I create a scheduled event with an invalid options + 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); + + // celix_scheduleEvent_release and celix_scheduledEvent_retain can be called with NULL + celix_scheduledEvent_release(nullptr); + celix_scheduledEvent_retain(nullptr); + + // celix_bundleContext_removeScheduledEvent can handle invalid eventIds + celix_bundleContext_removeScheduledEvent(ctx->getCBundleContext(), -1); + celix_bundleContext_removeScheduledEvent(ctx->getCBundleContext(), 404); + + // celix_framework_scheduledEvent with no callback should return -1 + scheduledEventId = celix_framework_scheduleEvent(ctx->getFramework()->getCFramework(), + CELIX_FRAMEWORK_BUNDLE_ID, + nullptr, + 0.0, + 0.0, + nullptr, + nullptr, + nullptr, + nullptr); + EXPECT_EQ(scheduledEventId, -1); + + // celix_framework_scheduledEvent with an invalid bndId should return -1 + scheduledEventId = celix_framework_scheduleEvent( + ctx->getFramework()->getCFramework(), 404, nullptr, 0.0, 0.0, nullptr, [](void*) { /*nop*/ }, nullptr, nullptr); + EXPECT_EQ(scheduledEventId, -1); + + // celix_framework_waitForScheduledEvent with an invalid bndId should return CELIX_ILLEGAL_ARGUMENT + celix_status_t status = celix_framework_waitForScheduledEvent(ctx->getFramework()->getCFramework(), 404, 1); + EXPECT_EQ(status, CELIX_ILLEGAL_ARGUMENT); +} + +TEST_F(ScheduledEventTestSuite, WakeUpEventTest) { + // Given a counter scheduled event with a long initial delay is added + std::atomic count{0}; + celix_scheduled_event_options_t opts{}; + opts.name = "test wakeup"; + opts.initialDelayInSeconds = 0.05; + opts.intervalInSeconds = 0.05; + opts.callbackData = static_cast(&count); + opts.callback = [](void* countPtr) { + auto* count = static_cast*>(countPtr); + count->fetch_add(1); + }; + long scheduledEventId = + celix_bundleContext_scheduleEvent(fw->getFrameworkBundleContext()->getCBundleContext(), &opts); + ASSERT_NE(-1L, scheduledEventId); + EXPECT_EQ(0, count.load()); + + // When the scheduled event is woken up + celix_status_t status = celix_bundleContext_wakeupScheduledEvent( + fw->getFrameworkBundleContext()->getCBundleContext(), scheduledEventId); + + // Then the status is CELIX_SUCCESS + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the count becomes 1 within the error margin + waitFor([&]() { return count.load() == 1; }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(1, count.load()); + + // And the count becomes 2 within the interval including the error margin + auto now = std::chrono::steady_clock::now(); + waitFor([&]() { return count.load() == 2; }, std::chrono::milliseconds{50 + ALLOWED_ERROR_MARGIN_IN_MS}); + auto end = std::chrono::steady_clock::now(); + auto diff = std::chrono::duration_cast(end - now).count(); + EXPECT_EQ(2, count.load()); + EXPECT_NEAR(50, diff, ALLOWED_ERROR_MARGIN_IN_MS); + + // When waking up the scheduled event again + status = celix_bundleContext_wakeupScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), + scheduledEventId); + + // Then the status is CELIX_SUCCESS + ASSERT_EQ(CELIX_SUCCESS, status); + + // Then the count becomes 3 within the error margin + waitFor([&]() { return count.load() == 3; }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(3, count.load()); + + celix_bundleContext_removeScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), scheduledEventId); +} + +TEST_F(ScheduledEventTestSuite, WakeUpOneShotEventTest) { + // Given a counter scheduled event with a long initial delay is added + std::atomic count{0}; + celix_scheduled_event_options_t opts{}; + opts.name = "test one-shot wakeup"; + opts.initialDelayInSeconds = 5; + opts.callbackData = static_cast(&count); + opts.callback = [](void* countPtr) { + auto* count = static_cast*>(countPtr); + count->fetch_add(1); + }; + long scheduledEventId = + celix_bundleContext_scheduleEvent(fw->getFrameworkBundleContext()->getCBundleContext(), &opts); + ASSERT_GE(scheduledEventId, 0); + EXPECT_EQ(0, count.load()); + + // When the scheduled event is woken up + celix_status_t status = celix_bundleContext_wakeupScheduledEvent( + fw->getFrameworkBundleContext()->getCBundleContext(), scheduledEventId); + + // Then the status is CELIX_SUCCESS + ASSERT_EQ(CELIX_SUCCESS, status); + + // And the count becomes 1 within the error margin + waitFor([&]() { return count.load() == 1; }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(1, count.load()); + + // When the scheduled event is woken up again + status = celix_bundleContext_wakeupScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), + scheduledEventId); + + // Then the status is ILLEGAL_ARGUMENT, because the scheduled event is already woken up and a one-shot event + ASSERT_EQ(CELIX_ILLEGAL_ARGUMENT, status); +} + +TEST_F(ScheduledEventTestSuite, CxxScheduledEventTest) { + // Given a count and a callback to increase the count + std::atomic count{0}; + auto callback = [&count]() { count.fetch_add(1); }; + + std::atomic removed{false}; + auto removeCallback = [&removed]() { removed.store(true); }; + + // And a scheduled event with a initial delay and interval of 50ms + auto event = fw->getFrameworkBundleContext() + ->scheduledEvent() + .withName("test cxx") + .withInitialDelay(std::chrono::milliseconds{50}) + .withInterval(std::chrono::milliseconds{50}) + .withCallback(callback) + .withRemoveCallback(removeCallback) + .build(); + + // Then the count is not yet increased + ASSERT_EQ(0, count.load()); + + // And the count becomes 1 within the initial delay, including the error margin + auto start = std::chrono::steady_clock::now(); + waitFor([&]() { return count.load() == 1; }, std::chrono::milliseconds{50 + ALLOWED_ERROR_MARGIN_IN_MS}); + auto end = std::chrono::steady_clock::now(); + EXPECT_NEAR( + 50, std::chrono::duration_cast(end - start).count(), ALLOWED_ERROR_MARGIN_IN_MS); + EXPECT_EQ(1, count.load()); + + // When waking up the event + event.wakeup(); + + // Then the count is increased with the error margin + waitFor([&]() { return count.load() == 2; }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(2, count.load()); + + // And the remove callback is not yet called + EXPECT_FALSE(removed.load()); + + // When canceling the event + event.cancel(); + + // And waiting longer than the interval + std::this_thread::sleep_for(std::chrono::milliseconds{50 + ALLOWED_ERROR_MARGIN_IN_MS}); + + // Then the count is not increased + EXPECT_EQ(2, count.load()); + + // And the remove callback is called within the error margin + waitFor([&]() { return removed.load(); }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_TRUE(removed.load()); +} + +TEST_F(ScheduledEventTestSuite, CxxScheduledEventRAIITest) { + // Given a count and a callback to increase the count + std::atomic count{0}; + auto callback = [&count]() { count.fetch_add(1); }; + + std::atomic removed{false}; + auto removeCallback = [&removed]() { removed.store(true); }; + + { + // And a scoped scheduled event with an initial delay and interval of 50ms + auto event = fw->getFrameworkBundleContext() + ->scheduledEvent() + .withName("test cxx") + .withInitialDelay(std::chrono::milliseconds{100}) + .withInterval(std::chrono::milliseconds{50}) + .withCallback(callback) + .withRemoveCallback(removeCallback) + .build(); + + // Then the count is not yet increased + ASSERT_EQ(0, count.load()); + } + // When the event goes out of scope + + // Then the event removed callback is called within the allowed error margin + waitFor([&]() { return removed.load(); }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_TRUE(removed.load()); + + // And the count is not increased + EXPECT_EQ(0, count.load()); +} + +TEST_F(ScheduledEventTestSuite, CxxOneShotScheduledEventTest) { + // Given a count and a callback to increase the count + std::atomic count{0}; + auto callback = [&count]() { count.fetch_add(1); }; + + std::atomic removed{false}; + auto removeCallback = [&removed]() { removed.store(true); }; + + // And a scheduled event with a initial delay of 50ms + auto event = fw->getFrameworkBundleContext() + ->scheduledEvent() + .withName("test cxx one-shot") + .withInitialDelay(std::chrono::milliseconds{50}) + .withCallback(callback) + .withRemoveCallback(removeCallback) + .build(); + + // Then the count is not yet increased + ASSERT_EQ(0, count.load()); + + // And the remove callback is not yet called + EXPECT_FALSE(removed.load()); + + // And count will be increased within the initial delay (including some error margin) + waitFor([&] { return count.load() == 1; }, std::chrono::milliseconds{50 + ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(1, count.load()); + + // And the remove callback is called shortly after the initial delay, within the error margin + waitFor([&] { return removed.load(); }, std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_TRUE(removed.load()); + + // When waking up the event with a wait time of 1s + event.wakeup(); + + // And waiting a bit + std::this_thread::sleep_for(std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + + // Then the count is not increased, because the event is a one-shot event + EXPECT_EQ(1, count.load()); + + // When the event goes out of scope, it does not leak +} + +TEST_F(ScheduledEventTestSuite, CxxOneShotScheduledEventRAIITest) { + // Given a count and a callback to increase the count + std::atomic count{0}; + auto callback = [&count]() { count.fetch_add(1); }; + + std::atomic removed{false}; + auto removeCallback = [&removed]() { removed.store(true); }; + + { + // And a scoped scheduled event with a initial delay of 50ms + auto event = fw->getFrameworkBundleContext() + ->scheduledEvent() + .withName("test cxx one-shot") + .withInitialDelay(std::chrono::milliseconds{50}) + .withCallback(callback) + .withRemoveCallback(removeCallback) + .build(); + + // Then the count is not yet increased + ASSERT_EQ(0, count.load()); + } + // When the event is out of scope + + // Then the remove callback is not yet called, because a one-shot event is not canceled when out of scope + EXPECT_FALSE(removed.load()); + + // And count will be increased within the initial delay (including some error margin) + waitFor([&] { return count.load() == 1; }, std::chrono::milliseconds{50 + ALLOWED_ERROR_MARGIN_IN_MS}); + EXPECT_EQ(1, count.load()); + + // And the remove callback is called shortly after the initial delay + waitFor([&] { return removed.load(); }, std::chrono::milliseconds{10}); + EXPECT_TRUE(removed.load()); +} + +TEST_F(ScheduledEventTestSuite, CxxCreateScheduledEventWithNoCallbackTest) { + // When a scheduled event is created without a callback an exception is thrown + EXPECT_ANY_THROW(fw->getFrameworkBundleContext()->scheduledEvent().build()); // Note no callback +} + +TEST_F(ScheduledEventTestSuite, CxxCancelOneShotEventBeforeFiredTest) { + auto callback = []() { FAIL() << "Should not be called"; }; + + // Given a one shot scheduled event with an initial delay of 1s + auto event = fw->getFrameworkBundleContext() + ->scheduledEvent() + .withInitialDelay(std::chrono::seconds{1}) + .withCallback(callback) + .build(); + + // When the event is cancelled before the initial delay + event.cancel(); + + // And waiting a bit + std::this_thread::sleep_for(std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS}); + + // Then the event is not fired and does not leak +} + +TEST_F(ScheduledEventTestSuite, RemoveScheduledEventAsync) { + std::atomic count{0}; + auto callback = [](void* data) { + auto* count = static_cast*>(data); + count->fetch_add(1); + }; + + // Given a scheduled event with am initial delay of 1ms + celix_scheduled_event_options_t opts{}; + opts.initialDelayInSeconds = 0.01; + opts.callbackData = &count; + opts.callback = callback; + long eventId = celix_bundleContext_scheduleEvent(fw->getFrameworkBundleContext()->getCBundleContext(), &opts); + EXPECT_GE(eventId, 0); + + // When the event is removed async + celix_bundleContext_removeScheduledEventAsync(fw->getFrameworkBundleContext()->getCBundleContext(), eventId); + + // And waiting longer than the initial delay, including some error margin + std::this_thread::sleep_for(std::chrono::milliseconds{10 + ALLOWED_ERROR_MARGIN_IN_MS}); + + // Then the event is not fired + EXPECT_EQ(0, count.load()); +} + +TEST_F(ScheduledEventTestSuite, WaitForScheduledEvent) { + std::atomic count{0}; + auto callback = [](void* data) { + auto* count = static_cast*>(data); + count->fetch_add(1); + }; + + // Given a scheduled event with an initial delay of 1ms and an interval of 1ms + celix_scheduled_event_options_t opts{}; + opts.initialDelayInSeconds = 0.01; + opts.intervalInSeconds = 0.01; + opts.callbackData = &count; + opts.callback = callback; + long eventId = celix_bundleContext_scheduleEvent(fw->getFrameworkBundleContext()->getCBundleContext(), &opts); + EXPECT_GE(eventId, 0); + + // When waiting for the event with a timeout longer than the initial delay + auto status = + celix_bundleContext_waitForScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), eventId, 1); + + // Then the return status is success + EXPECT_EQ(CELIX_SUCCESS, status); + + // And the event is fired + EXPECT_EQ(1, count.load()); + + // When waiting too short for the event + status = celix_bundleContext_waitForScheduledEvent( + fw->getFrameworkBundleContext()->getCBundleContext(), eventId, 0.0001); + + // Then the return status is timeout + EXPECT_EQ(ETIMEDOUT, status); + + // When waiting for the event with a timeout longer than the interval + status = + celix_bundleContext_waitForScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), eventId, 1); + + // Then the return status is success + EXPECT_EQ(CELIX_SUCCESS, status); + + // And the event is fired again + EXPECT_EQ(2, count.load()); + + celix_bundleContext_removeScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), eventId); +} + +TEST_F(ScheduledEventTestSuite, CxxWaitForScheduledEvent) { + std::atomic count{0}; + auto callback = [&count]() { count.fetch_add(1); }; + + // Given a scheduled event with an initial delay of 1ms and an interval of 1ms + auto event = fw->getFrameworkBundleContext() + ->scheduledEvent() + .withInitialDelay(std::chrono::milliseconds{10}) + .withInterval(std::chrono::milliseconds{10}) + .withCallback(callback) + .build(); + + // When waiting for the event with a timeout longer than the initial delay + auto success = event.waitFor(std::chrono::milliseconds{10 + ALLOWED_ERROR_MARGIN_IN_MS}); + + // Then the return status is success + EXPECT_TRUE(success); + + // And the event is fired + EXPECT_EQ(1, count.load()); + + // When waiting to short for the event + success = event.waitFor(std::chrono::microseconds{1}); + + // Then the return status is false (timeout) + EXPECT_FALSE(success); + + // When waiting for the event with a timeout longer than the interval + success = event.waitFor(std::chrono::milliseconds{10 + ALLOWED_ERROR_MARGIN_IN_MS}); + + // Then the return status is success + EXPECT_TRUE(success); + + // And the event is fired again + EXPECT_EQ(2, count.load()); +} + +#ifndef __APPLE__ +TEST_F(ScheduledEventTestSuite, ScheduledEventTimeoutLogTest) { + //Disabled for __APPLE__, because the expected timeout in celix_scheduledEvent_waitForRemoved does not always + //trigger. see also ticket #587. + + //Given a framework with a log callback that counts the number of warning log messages + std::atomic logCount{0}; + auto logCallback = [](void* handle, celix_log_level_e level, const char*, const char*, int, const char* format, va_list args){ + std::atomic& count = *static_cast*>(handle); + if (level == CELIX_LOG_LEVEL_WARNING) { + count.fetch_add(1); + } + FILE* output = stdout; + if (level == CELIX_LOG_LEVEL_FATAL || level == CELIX_LOG_LEVEL_ERROR || level == CELIX_LOG_LEVEL_WARNING) { + output = stderr; + } + fprintf(output, "%s: ", celix_logLevel_toString(level)); + vfprintf(output, format, args); + fprintf(output, "\n"); + }; + celix_framework_setLogCallback(fw->getCFramework(), &logCount, logCallback); + + + //And a scheduled event with an initial delay of 1ms and an interval of 1ms that waits for 200ms in the callback + //and remove callback. + + auto busyWaitCallback = [](void*){ + auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < std::chrono::milliseconds{200}) { + //busy wait + } + }; + + celix_scheduled_event_options_t opts{}; + opts.name = "Sleep while Processing and Removing Scheduled Event"; + opts.initialDelayInSeconds = 0.01; + opts.intervalInSeconds = 0.01; + opts.callback = busyWaitCallback; + opts.removeCallback = busyWaitCallback; + long eventId = celix_bundleContext_scheduleEvent(fw->getFrameworkBundleContext()->getCBundleContext(), &opts); + + //When the event is woken up + celix_bundleContext_wakeupScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), eventId); + + // Then the log callback is called with a warning log message within an error margin and processing sleep time, + // because callback took too long. + waitFor([&] { return logCount.load() == 1; }, + std::chrono::milliseconds{ALLOWED_ERROR_MARGIN_IN_MS + 200}); + EXPECT_EQ(1, logCount.load()); + + //When removing the event + celix_bundleContext_removeScheduledEvent(fw->getFrameworkBundleContext()->getCBundleContext(), eventId); + + //Then the log callback is called at least one more time with a warning log message, because remove + //callback took too long + //(note the logCount can be increased more than once, due to another processing thread) + EXPECT_GE(logCount.load(), 2); +} +#endif diff --git a/libs/framework/gtest/src/ScheduledEventWithErrorInjectionTestSuite.cc b/libs/framework/gtest/src/ScheduledEventWithErrorInjectionTestSuite.cc new file mode 100644 index 000000000..ea3e96ac0 --- /dev/null +++ b/libs/framework/gtest/src/ScheduledEventWithErrorInjectionTestSuite.cc @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix/FrameworkFactory.h" +#include "celix_bundle_context.h" +#include "celix_scheduled_event.h" +#include "framework_private.h" + +#include "malloc_ei.h" + +class ScheduledEventWithErrorInjectionTestSuite : public ::testing::Test { +public: + ScheduledEventWithErrorInjectionTestSuite() { + fw = celix::createFramework({{"CELIX_LOGGING_DEFAULT_ACTIVE_LOG_LEVEL", "info"}}); + } + + ~ScheduledEventWithErrorInjectionTestSuite() noexcept override { + celix_ei_expect_malloc(nullptr, 0, nullptr); + } + + std::shared_ptr fw{}; +}; + + +TEST_F(ScheduledEventWithErrorInjectionTestSuite, MallocFailsTest) { + //Given malloc is primed to fail on the first call from celix_scheduledEvent_create (whitebox knowledge) + celix_ei_expect_malloc((void*)celix_scheduledEvent_create, 0, nullptr); + + //When a scheduled event is added + celix_scheduled_event_options_t opts{}; + opts.name = "malloc fail test"; + opts.callback = [](void*){/*nop*/}; + long scheduledEventId = celix_bundleContext_scheduleEvent(fw->getFrameworkBundleContext()->getCBundleContext(), + &opts); + + //Then the scheduled event id is -1 (error) + EXPECT_EQ(-1L, scheduledEventId); +} diff --git a/libs/framework/include/celix/BundleContext.h b/libs/framework/include/celix/BundleContext.h index eaf2ff324..59134a8fb 100644 --- a/libs/framework/include/celix/BundleContext.h +++ b/libs/framework/include/celix/BundleContext.h @@ -29,6 +29,7 @@ #include "celix/ServiceRegistrationBuilder.h" #include "celix/UseServiceBuilder.h" #include "celix/TrackerBuilders.h" +#include "celix/ScheduledEventBuilder.h" #include "celix/Bundle.h" #include "celix/Framework.h" @@ -394,6 +395,14 @@ namespace celix { return MetaTrackerBuilder(cCtx, {}); } + /** + * @brief Schedule a callback to be called after the given initial delay and/or interval using a fluent + * builder API. + */ + ScheduledEventBuilder scheduledEvent() { + return ScheduledEventBuilder{cCtx}; + } + /** * @brief Install and optional start a bundle. * diff --git a/libs/framework/include/celix/Framework.h b/libs/framework/include/celix/Framework.h index e557e55fe..e0e62eeb6 100644 --- a/libs/framework/include/celix/Framework.h +++ b/libs/framework/include/celix/Framework.h @@ -61,7 +61,7 @@ namespace celix { * The event will be added to the event loop and handled on the event loop thread. * * if bndId >=0 the bundle usage count will be increased while the event is not yet processed or finished processing. - * The eventName is expected to be const char* valid during til the event is finished processing. + * The name is expected to be const char* valid during til the event is finished processing. * * if eventId >=0 this will be used, otherwise a new event id will be generated. * diff --git a/libs/framework/include/celix/ScheduledEvent.h b/libs/framework/include/celix/ScheduledEvent.h new file mode 100644 index 000000000..ffa6c078a --- /dev/null +++ b/libs/framework/include/celix/ScheduledEvent.h @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include "celix_bundle_context.h" + +namespace celix { + +/** + * @brief A C++ abstraction for a scheduled event in Celix. + * + * A scheduled event is an event that is scheduled to be executed at a certain initial delay and/or interval. + * A new scheduld event should be created using celix::BundleContext::createScheduledEvent. + * + * This class uses RAII to automatically remove the (non one-shot) scheduled event from the bundle context + * when it is destroyed. For one-shot scheduled events, the destructor will not remove the scheduled event. + */ +class ScheduledEvent final { + public: + friend class ScheduledEventBuilder; + + /** + * @brief Constructs a empty / not-active scheduled event. + * + * During destruction the scheduled event will be removed asynchronously from the bundle context. + */ + ScheduledEvent() = default; + + /** + * @brief Destroys the scheduled event by removes it from the bundle context if it is not a one-short event. + */ + ~ScheduledEvent() noexcept { + if (!isOneShot && ctx) { + celix_bundleContext_tryRemoveScheduledEventAsync(ctx.get(), eventId); + } + } + + ScheduledEvent(const ScheduledEvent&) = delete; + ScheduledEvent& operator=(const ScheduledEvent&) = delete; + + ScheduledEvent(ScheduledEvent&& rhs) noexcept + : ctx{std::move(rhs.ctx)}, eventId{rhs.eventId}, isOneShot{rhs.isOneShot} { + rhs.eventId = -1; + } + + ScheduledEvent& operator=(ScheduledEvent&& rhs) noexcept { + if (this != &rhs) { + ctx = std::move(rhs.ctx); + eventId = rhs.eventId; + isOneShot = rhs.isOneShot; + rhs.eventId = -1; + } + return *this; + } + + /** + * @brief Cancels the scheduled event. + * + * This method will block until a possible in-progress scheduled event callback is finished, the scheduled event + * is removed and, if configured, the remove callback is called. + * Should not be called multiple times. + */ + void cancel() { + if (ctx) { + celix_bundleContext_removeScheduledEvent(ctx.get(), eventId); + } + } + + /** + * @brief Wakeup a scheduled event and returns immediately, not waiting for the scheduled event callback to be + * called. + */ + void wakeup() { + if (ctx) { + celix_bundleContext_wakeupScheduledEvent(ctx.get(), eventId); + } + } + + /** + * @brief Wait until the next scheduled event is processed. + * + * @tparam Rep The representation type of the duration. + * @tparam Period The period type of the duration. + * @param[in] waitTime The maximum time to wait for the next scheduled event. If <= 0 the function will return + * immediately. + * @return true if the next scheduled event was processed, false if a timeout occurred. + */ + template + bool waitFor(std::chrono::duration waitTime) { + double waitTimeInSeconds = std::chrono::duration_cast>(waitTime).count(); + celix_status_t status = CELIX_SUCCESS; + if (ctx) { + status = celix_bundleContext_waitForScheduledEvent(ctx.get(), eventId, waitTimeInSeconds); + } + return status == CELIX_SUCCESS; + } + + private: + struct Callbacks { + std::function callback{}; /**< The callback for the scheduled event. */ + std::function removeCallback{}; /**< The remove callback for the scheduled event. */ + }; + + static void (*callback)(void*); + + /** + * @brief Constructs a scheduled event using the given bundle context and options. + * + * @param[in] ctx The bundle context to use. + * @param[in] options The options for the scheduled event. + */ + ScheduledEvent(std::shared_ptr _cCtx, + const std::string& _name, + std::function _callback, + std::function _removeCallback, + celix_scheduled_event_options_t& options) { + static auto callback = [](void* data) { + auto* callbacks = static_cast(data); + (callbacks->callback)(); + }; + static auto removeCallback = [](void* data) { + auto* callbacks = static_cast(data); + if (callbacks->removeCallback) { + (callbacks->removeCallback)(); + } + delete callbacks; + }; + + ctx = std::move(_cCtx); + isOneShot = options.intervalInSeconds == 0; + options.name = _name.c_str(); + auto* callbacks = new Callbacks{std::move(_callback), std::move(_removeCallback)}; + options.callbackData = callbacks; + options.callback = callback; + options.removeCallbackData = callbacks; + options.removeCallback = removeCallback; + eventId = celix_bundleContext_scheduleEvent(ctx.get(), &options); + } + + std::shared_ptr ctx{}; /**< The bundle context for the scheduled event. */ + long eventId{-1}; /**< The ID of the scheduled event. */ + bool isOneShot{false}; /**< Whether the scheduled event is a one-shot event. */ +}; + +} // end namespace celix diff --git a/libs/framework/include/celix/ScheduledEventBuilder.h b/libs/framework/include/celix/ScheduledEventBuilder.h new file mode 100644 index 000000000..c93a93bdb --- /dev/null +++ b/libs/framework/include/celix/ScheduledEventBuilder.h @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#pragma once + +#include +#include + +#include "celix/ScheduledEvent.h" +#include "celix/Exception.h" + +namespace celix { + +/** + * @brief A C++ builder for a ScheduledEvent object. + */ +class ScheduledEventBuilder final { +public: + /** + * @brief Construct a scheduled event builder with the given bundle context. + * + * @param[in] ctx The bundle context to use. + */ + ScheduledEventBuilder(std::shared_ptr _cCtx) : ctx{std::move(_cCtx)}, options{} {} + + /** + * @brief Set the initial delay of the scheduled event. + * + * @tparam Rep The representation type of the duration. + * @tparam Period The period type of the duration. + * @param[in] delay The delay duration. + * @return A reference to this builder. + */ + template + ScheduledEventBuilder& withInitialDelay(std::chrono::duration delay) { + options.initialDelayInSeconds = std::chrono::duration_cast>(delay).count(); + return *this; + } + + /** + * @brief Set the interval of the scheduled event. + * + * @tparam Rep The representation type of the duration. + * @tparam Period The period type of the duration. + * @param[in] interval The interval duration. + * @return A reference to this builder. + */ + template + ScheduledEventBuilder& withInterval(std::chrono::duration interval) { + options.intervalInSeconds = std::chrono::duration_cast>(interval).count(); + return *this; + } + + /** + * @brief Set the name of the scheduled event. + * + * @param[in] name The name of the scheduled event. + * @return A reference to this builder. + */ + ScheduledEventBuilder& withName(std::string n) { + name = std::move(n); + return *this; + } + + /** + * @brief Set the callback of the scheduled event. + * + * The callback is called when the scheduled event is triggered on the event thread. + * + * @param[in] cb The callback function. + * @return A reference to this builder. + */ + ScheduledEventBuilder& withCallback(std::function cb) { + callback = std::move(cb); + return *this; + } + + /** + * @brief Set the remove callback of the scheduled event. + * + * The remove callback is called when the scheduled event is removed from the scheduler and can be called + * from any thread. + * + * @param[in] cb The callback function. + * @return A reference to this builder. + */ + ScheduledEventBuilder& withRemoveCallback(std::function cb) { + removeCallback = std::move(cb); + return *this; + } + + /** + * @brief Build the scheduled event with the given options. + * + * @return The scheduled event. + */ + ScheduledEvent build() { + if (!callback) { + throw celix::Exception{"Cannot build scheduled event without callback"}; //TODO improve error + } + return ScheduledEvent{ctx, name, std::move(callback), std::move(removeCallback), options}; + } + +private: + std::shared_ptr ctx; /**< The bundle context for the scheduled event. */ + celix_scheduled_event_options_t options; /**< The options for the scheduled event. */ + std::string name{}; /**< The name of the scheduled event. */ + std::function callback{}; /**< The callback function for the scheduled event. */ + std::function removeCallback{}; /**< The callback function for the scheduled event. */ +}; + +} // end namespace celix diff --git a/libs/framework/include/celix/dm/Component.h b/libs/framework/include/celix/dm/Component.h index cf13f39b3..19e792e4f 100644 --- a/libs/framework/include/celix/dm/Component.h +++ b/libs/framework/include/celix/dm/Component.h @@ -124,7 +124,9 @@ namespace celix { namespace dm { /** * Wait for an empty Celix event queue. + * * Should not be called on the Celix event queue thread. + * Note scheduled events are not part of the event queue. * * Can be used to ensure that all created/updated components are completely processed (services registered * and/or service trackers are created). diff --git a/libs/framework/include/celix/dm/DependencyManager.h b/libs/framework/include/celix/dm/DependencyManager.h index a36c7e7f4..020258121 100644 --- a/libs/framework/include/celix/dm/DependencyManager.h +++ b/libs/framework/include/celix/dm/DependencyManager.h @@ -131,6 +131,7 @@ namespace celix { namespace dm { * @brief Wait for an empty Celix event queue. * * Should not be called on the Celix event queue thread. + * Note scheduled events are not part of the event queue. * * Can be used to ensure that all created/updated components are completely processed (services registered * and/or service trackers are created). @@ -140,6 +141,8 @@ namespace celix { namespace dm { /** * @brief Wait (if not called on the Celix event thread) for an empty Celix event queue * + * Note scheduled events are not part of the event queue. + * * Can be used to ensure that all created/updated components are completely processed (services registered * and/or service trackers are created). */ diff --git a/libs/framework/include/celix_bundle_context.h b/libs/framework/include/celix_bundle_context.h index 2926bac43..23acaa377 100644 --- a/libs/framework/include/celix_bundle_context.h +++ b/libs/framework/include/celix_bundle_context.h @@ -203,10 +203,10 @@ typedef struct celix_service_registration_options { void (*asyncCallback)(void *data, long serviceId) CELIX_OPTS_INIT; } celix_service_registration_options_t; -/** +#ifndef __cplusplus +/*! * @brief C Macro to create a empty celix_service_registration_options_t type. */ -#ifndef __cplusplus #define CELIX_EMPTY_SERVICE_REGISTRATION_OPTIONS { .svc = NULL, \ .factory = NULL, \ .serviceName = NULL, \ @@ -356,10 +356,10 @@ typedef struct celix_service_filter_options { bool ignoreServiceLanguage CELIX_OPTS_INIT; } celix_service_filter_options_t; -/** +#ifndef __cplusplus +/*! * @brief C Macro to create a empty celix_service_filter_options_t type. */ -#ifndef __cplusplus #define CELIX_EMPTY_SERVICE_FILTER_OPTIONS {.serviceName = NULL, .versionRange = NULL, .filter = NULL, .serviceLanguage = NULL, .ignoreServiceLanguage = false} #endif @@ -563,10 +563,10 @@ typedef struct celix_service_tracking_options { void (*trackerCreatedCallback)(void *trackerCreatedCallbackData) CELIX_OPTS_INIT; } celix_service_tracking_options_t; -/** +#ifndef __cplusplus +/*! * @brief C Macro to create a empty celix_service_tracking_options_t type. */ -#ifndef __cplusplus #define CELIX_EMPTY_SERVICE_TRACKING_OPTIONS { .filter.serviceName = NULL, \ .filter.versionRange = NULL, \ .filter.filter = NULL, \ @@ -781,10 +781,10 @@ typedef struct celix_service_use_options { int flags CELIX_OPTS_INIT; } celix_service_use_options_t; -/** +#ifndef __cplusplus +/*! * @brief C Macro to create a empty celix_service_use_options_t type. */ -#ifndef __cplusplus #define CELIX_EMPTY_SERVICE_USE_OPTIONS {.filter.serviceName = NULL, \ .filter.versionRange = NULL, \ .filter.filter = NULL, \ @@ -1073,10 +1073,10 @@ typedef struct celix_bundle_tracker_options { void (*trackerCreatedCallback)(void *trackerCreatedCallbackData) CELIX_OPTS_INIT; } celix_bundle_tracking_options_t; -/** +#ifndef __cplusplus +/*! * @brief C Macro to create a empty celix_service_filter_options_t type. */ -#ifndef __cplusplus #define CELIX_EMPTY_BUNDLE_TRACKING_OPTIONS {.callbackHandle = NULL, .onInstalled = NULL, .onStarted = NULL, .onStopped = NULL, .onBundleEvent = NULL, .includeFrameworkBundle = false, .trackerCreatedCallbackData = NULL, .trackerCreatedCallback = NULL} #endif @@ -1224,7 +1224,7 @@ CELIX_FRAMEWORK_EXPORT long celix_bundleContext_trackServiceTrackersAsync( * * This tracker can be stopped with the celix_bundleContext_stopTracker function. * - * @param ctx The bundle context + * @param ctx The bundle context. * @param serviceName The target service name for the service tracker to track. * If NULL is provided, add/remove callbacks will be called for all service trackers in the framework. * @param callbackHandle The callback handle which will be provided as handle in the trackerAdd and trackerRemove callback. @@ -1253,6 +1253,143 @@ CELIX_FRAMEWORK_EXPORT celix_dependency_manager_t* celix_bundleContext_getDepend */ CELIX_FRAMEWORK_EXPORT void celix_bundleContext_waitForEvents(celix_bundle_context_t *ctx); +/** + * @struct celix_scheduled_event_options + * @brief Celix scheduled event options, used for creating scheduling events with the celix framework. + */ +typedef struct celix_scheduled_event_options { + const char* name CELIX_OPTS_INIT; /**< + * @brief The name of the event, used for logging and debugging. + * + * Expected to be const char* that is valid during the celix_bundleContext_scheduleEvent + * call. Can be NULL. */ + + double initialDelayInSeconds CELIX_OPTS_INIT; /**< @brief Initial delay in seconds before the event is processed.*/ + + double intervalInSeconds CELIX_OPTS_INIT; /**< @brief Schedule interval in seconds. + * 0 means one-shot scheduled event. + */ + + void* callbackData CELIX_OPTS_INIT; /**< @brief Data passed to the callback function when a event is scheduled.*/ + + void (*callback)(void* callbackData) CELIX_OPTS_INIT; /**< @brief Callback function called to process a scheduled + event. Will be called on the event thread.*/ + + void* removeCallbackData + CELIX_OPTS_INIT; /**< @brief Data passed to the done callback function when a scheduled event is removed.*/ + + void (*removeCallback)(void* removeCallbackData) + CELIX_OPTS_INIT; /**< @brief Callback function called when a scheduled event is removed. Will be called on + the event thread.*/ +} celix_scheduled_event_options_t; + +#define CELIX_EMPTY_SCHEDULED_EVENT_OPTIONS {NULL, 0.0, 0.0, NULL, NULL, NULL, NULL} + +/** + * @brief Add a scheduled event to the Celix framework. + * + * The scheduled event will be called on the Celix framework event thread, repeatedly using the provided interval or + * once if only a initial delay is provided. + * The event callback should be relatively fast and the scheduled event interval should be relatively long, otherwise + * the framework event queue will be blocked and framework will not function properly. + * + * Scheduled events can be scheduled later than the provided initial delay and interval, because they are processed + * after other events in the Celix event thread. + * The target - but not guaranteed - precision of the scheduled event trigger is 1 microsecond. + * + * If the provided interval is 0, the scheduled event will a one-shot scheduled event and will be called once + * after the provided initial delay. If a bundle stops before the one-shot scheduled event is called, the scheduled + * event will be removed and not called. + * + * Scheduled events should be removed by the caller when not needed anymore, except for one-shot scheduled events. + * one-shot are automatically removed after the event callback is called. + * + * Note during bundle stop the framework will check if all scheduled events for the bundle are removed. + * For every not removed scheduled event that is not a one-shot event, a warning will be logged and the + * scheduled event will be removed. + * + * @param[in] ctx The bundle context. + * @param[in] options The scheduled event options, which describe the to be added scheduled event. + * @return The scheduled event id of the scheduled event. Can be used to cancel the event. + * @retval <0 If the event could not be added. + */ +CELIX_FRAMEWORK_EXPORT long celix_bundleContext_scheduleEvent(celix_bundle_context_t* ctx, + const celix_scheduled_event_options_t* options); + +/** + * @brief Wakeup a scheduled event and returns immediately, not waiting for the scheduled event callback to be + * called. + * + * Silently ignored if the scheduled event ids < 0. + * + * @param[in] ctx The bundle context. + * @param[in] scheduledEventId The scheduled event id to wakeup. + * @return CELIX_SUCCESS if the scheduled event is woken up, CELIX_ILLEGAL_ARGUMENT if the scheduled event id is not known. + */ +CELIX_FRAMEWORK_EXPORT celix_status_t celix_bundleContext_wakeupScheduledEvent(celix_bundle_context_t* ctx, + long scheduledEventId); + +/** + * @brief Wait until the next scheduled event is processed. + * + * Silently ignored if the scheduled event ids < 0. + * + * @param[in] ctx The bundle context. + * @param[in] scheduledEventId The scheduled event id to wait for. + * @param[in] waitTimeInSeconds The maximum time to wait for the next scheduled event. If <= 0 the function will return + * immediately. + * @return CELIX_SUCCESS if the scheduled event is woken up, CELIX_ILLEGAL_ARGUMENT if the scheduled event id is not + * known and ETIMEDOUT if the waitTimeInSeconds is reached. + */ +CELIX_FRAMEWORK_EXPORT celix_status_t celix_bundleContext_waitForScheduledEvent(celix_bundle_context_t* ctx, + long scheduledEventId, + double waitTimeInSeconds); + +/** + * @brief Cancel and remove a scheduled event. + * + * Silently ignored if the scheduled event ids < 0. + * + * This function will block until a possible in-progress scheduled event callback is finished, the scheduled event + * is removed and, if configured, the remove callback is called. + * + * @param[in] ctx The bundle context. + * @param[in] scheduledEventId The scheduled event id to cancel and remove. + * @return true if a scheduled event is cancelled, false if the scheduled event id is not known. + */ +CELIX_FRAMEWORK_EXPORT bool celix_bundleContext_removeScheduledEvent(celix_bundle_context_t* ctx, + long scheduledEventId); + +/** + * @brief Cancel and remove a scheduled event asynchronously. + * + * When this function returns, no new scheduled event callbacks will be called, but it is not guaranteed that there + * is still a scheduled event callback in progress and that the remove callback is called. + * + * Silently ignored if the scheduled event ids < 0. + * + * @param[in] ctx The bundle context. + * @param[in] scheduledEventId The scheduled event id to cancel and remove. + * @return true if a scheduled event is cancelled, false if the scheduled event id is not known. + */ +CELIX_FRAMEWORK_EXPORT bool celix_bundleContext_removeScheduledEventAsync(celix_bundle_context_t* ctx, + long scheduledEventId); + +/** + * @brief Try to cancel and remove a scheduled event asynchronously. + * + * Silently ignored if the scheduled event ids < 0. + * + * When this function returns, no new scheduled event callbacks will be called, but it is not guaranteed that there + * is still a scheduled event callback in progress and that the remove callback is called. + * Will not log an error if the scheduled event id is not known. + * + * @param[in] ctx The bundle context. + * @param[in] scheduledEventId The scheduled event id to cancel. + * @return true if a scheduled event is cancelled, false if the scheduled event id is not known. + */ +CELIX_FRAMEWORK_EXPORT bool celix_bundleContext_tryRemoveScheduledEventAsync(celix_bundle_context_t* ctx, + long scheduledEventId); /** * @brief Returns the bundle for this bundle context. diff --git a/libs/framework/include/celix_constants.h b/libs/framework/include/celix_constants.h index 14a0a2655..c2ad3a8c6 100644 --- a/libs/framework/include/celix_constants.h +++ b/libs/framework/include/celix_constants.h @@ -322,6 +322,21 @@ extern "C" { */ #define CELIX_AUTO_INSTALL "CELIX_AUTO_INSTALL" +/*! + * @brief Celix framework environment property (named "CELIX_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS") + * to configure the allowed processing time for a scheduled event callback or a remove callback before a warning + * log message is printed that the processing time is too long. + * Should be a double value in seconds. + */ +#define CELIX_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS \ + "CELIX_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS" + +/** + * @brief The default , in seconds, allowed processing time for a processing a scheduled event or a remove callback + * before a warning log message is printed. + */ +#define CELIX_DEFAULT_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS 2.0 + #ifdef __cplusplus } #endif diff --git a/libs/framework/include/celix_dependency_manager.h b/libs/framework/include/celix_dependency_manager.h index 4625fa5d6..00d1859d6 100644 --- a/libs/framework/include/celix_dependency_manager.h +++ b/libs/framework/include/celix_dependency_manager.h @@ -32,7 +32,7 @@ extern "C" { /** * @file celix_dependency_manager.h - * + * * The `celix_dependencyManager_add`, `celix_dependencyManager_remove` and `celix_dependencyManager_removeAllComponents` * funcions for celix_dependency_manager_t should be called outside the Celix event thread. * Note that bundle activators are started and stopped outside the Celix event thread and thus these @@ -123,8 +123,9 @@ CELIX_FRAMEWORK_EXPORT size_t celix_dependencyManager_nrOfComponents(celix_depen /** * @brief Wait for an empty Celix event queue. - * + * * Should not be called on the Celix event queue thread. + * Note scheduled events are not part of the event queue. * * Can be used to ensure that all created/updated components are completely processed (services registered * and/or service trackers are created). @@ -133,7 +134,7 @@ CELIX_FRAMEWORK_EXPORT void celix_dependencyManager_wait(celix_dependency_manage /** * @brief Create and returns a dependency manager info struct for the specified bundle. - * + * * The dependency manager info contains information about the state of the dependency manager components * * Caller has ownership of the return value (use celix_dependencyManager_destroyInfo to free the memory). @@ -147,7 +148,7 @@ CELIX_FRAMEWORK_EXPORT celix_dependency_manager_info_t* celix_dependencyManager_ /** * @brief Create and returns a dependency manager info struct for all started bundles. - * + * * The dependency manager info contains information about the state of the dependency manager components * * Caller has ownership of the return values (use celix_arrayList_destroy to free the memory). @@ -170,7 +171,7 @@ CELIX_FRAMEWORK_EXPORT void celix_dependencyManager_destroyInfos(celix_dependenc /** * @brief Print the dependency manager info for all bundles to the provided output stream. - * + * * @param manager The dependency manager. * @param fullInfo Whether to print the full info or summary. * @param useAnsiColors Whether to use ansi colors when printing info. @@ -180,7 +181,7 @@ CELIX_FRAMEWORK_EXPORT void celix_dependencyManager_printInfo(celix_dependency_m /** * @brief Print the dependency manager info for the provided bundle id to the provided output stream. - * + * * @param manager The dependency manager. * @param fullInfo whether to print the full info or summary. * @param useAnsiColors Whether to use ansi colors when printing info. diff --git a/libs/framework/include/celix_framework.h b/libs/framework/include/celix_framework.h index 8275c0fac..66d3829c7 100644 --- a/libs/framework/include/celix_framework.h +++ b/libs/framework/include/celix_framework.h @@ -50,7 +50,6 @@ extern "C" { * @note The Celix framework instance is thread safe. */ - /** * @brief Returns the framework UUID. This is unique for every created framework and will not be the same if the process is * restarted. @@ -318,53 +317,81 @@ CELIX_FRAMEWORK_EXPORT celix_array_list_t* celix_framework_listBundles(celix_fra */ CELIX_FRAMEWORK_EXPORT celix_array_list_t* celix_framework_listInstalledBundles(celix_framework_t* framework); +/** + * @brief Sets the log function for this framework. + * Default the celix framework will log to stdout/stderr. + * + * A log function can be injected to change how the Celix framework logs. + * Can be reset by setting the log function to NULL. + */ +CELIX_FRAMEWORK_EXPORT void celix_framework_setLogCallback(celix_framework_t* fw, void* logHandle, void (*logFunction)(void* handle, celix_log_level_e level, const char* file, const char *function, int line, const char *format, va_list formatArgs)); + /** * @brief Wait until the framework event queue is empty. * - * The Celix framework has an event queue which (among others) handles bundle events. - * This function can be used to ensure that all queue event are handled, mainly useful - * for testing. + * The Celix framework has an event queue which (among others) handles various events. + * This function can be used to ensure that all queue events are handled. + * + * Note scheduled events are not part of the event queue. * * @param fw The Celix Framework */ CELIX_FRAMEWORK_EXPORT void celix_framework_waitForEmptyEventQueue(celix_framework_t *fw); /** - * @brief Sets the log function for this framework. - * Default the celix framework will log to stdout/stderr. + * @brief Wait until the framework event queue is empty or the provided period is reached. * - * A log function can be injected to change how the Celix framework logs. - * Can be reset by setting the log function to NULL. + * The Celix framework has an event queue which (among others) handles various events. + * This function can be used to ensure that all queue events are handled. + * + * Note scheduled events are not part of the event queue. + * + * @param[in] fw The Celix Framework. + * @param[in] timeoutInSeconds The period in seconds to wait for the event queue to be empty. 0 means wait forever. + * @return CELIX_SUCCESS if the event queue is empty or ETIMEDOUT if the timeoutInSeconds is reached. */ -CELIX_FRAMEWORK_EXPORT void celix_framework_setLogCallback(celix_framework_t* fw, void* logHandle, void (*logFunction)(void* handle, celix_log_level_e level, const char* file, const char *function, int line, const char *format, va_list formatArgs)); - +CELIX_FRAMEWORK_EXPORT celix_status_t celix_framework_waitForEmptyEventQueueFor(celix_framework_t *fw, double timeoutInSeconds); /** - * @brief wait until all events for the bundle identified by the bndId are processed. + * @brief wait until all events from the event queue for the bundle identified by the bndId are processed. + * + * If bndId < 0, wait until all bundle events (events associated with a bundle) from the event queue are processed. + * Note scheduled events are not part of the event queue. + * */ CELIX_FRAMEWORK_EXPORT void celix_framework_waitUntilNoEventsForBnd(celix_framework_t* fw, long bndId); /** - * @brief wait until all pending service registration are processed. + * @brief wait until all pending service registration are processed. */ CELIX_FRAMEWORK_EXPORT void celix_framework_waitUntilNoPendingRegistration(celix_framework_t* fw); /** * @brief Returns whether the current thread is the Celix framework event loop thread. + * */ CELIX_FRAMEWORK_EXPORT bool celix_framework_isCurrentThreadTheEventLoop(celix_framework_t* fw); - /** * @brief Fire a generic event. The event will be added to the event loop and handled on the event loop thread. * + * The process callback should be fast and non-blocking, otherwise + * the framework event queue will be blocked and framework will not function properly. + * * if bndId >=0 the bundle usage count will be increased while the event is not yet processed or finished processing. - * The eventName is expected to be const char* valid during til the event is finished processing. + * The name is expected to be const char* valid during til the event is finished processing. * * if eventId >=0 this will be used, otherwise a new event id will be generated * return eventId */ -CELIX_FRAMEWORK_EXPORT long celix_framework_fireGenericEvent(celix_framework_t* fw, long eventId, long bndId, const char *eventName, void* processData, void (*processCallback)(void *data), void* doneData, void (*doneCallback)(void* doneData)); +CELIX_FRAMEWORK_EXPORT long celix_framework_fireGenericEvent(celix_framework_t* fw, + long eventId, + long bndId, + const char* eventName, + void* processData, + void (*processCallback)(void* data), + void* doneData, + void (*doneCallback)(void* doneData)); /** * @brief Get the next event id. @@ -376,8 +403,10 @@ CELIX_FRAMEWORK_EXPORT long celix_framework_fireGenericEvent(celix_framework_t* CELIX_FRAMEWORK_EXPORT long celix_framework_nextEventId(celix_framework_t *fw); /** - * @brief Wait until a event with the provided event id is completely handled. + * @brief Wait until a event from the event queue with the provided event id is completely handled. * This function will directly return if the provided event id is not in the event loop (already done or never issued). + * + * Note scheduled events are not part of the event queue. */ CELIX_FRAMEWORK_EXPORT void celix_framework_waitForGenericEvent(celix_framework_t *fw, long eventId); diff --git a/libs/framework/src/bundle_context.c b/libs/framework/src/bundle_context.c index b3f702322..1d78780bb 100644 --- a/libs/framework/src/bundle_context.c +++ b/libs/framework/src/bundle_context.c @@ -111,13 +111,20 @@ celix_status_t bundleContext_destroy(bundle_context_pt context) { return status; } -void celix_bundleContext_cleanup(celix_bundle_context_t *ctx) { - //NOTE not perfect, because stopping of registrations/tracker when the activator is destroyed can lead to segfault. - //but at least we can try to warn the bundle implementer that some cleanup is missing. - bundleContext_cleanupBundleTrackers(ctx); - bundleContext_cleanupServiceTrackers(ctx); - bundleContext_cleanupServiceTrackerTrackers(ctx); - bundleContext_cleanupServiceRegistration(ctx); +void celix_bundleContext_cleanup(celix_bundle_context_t* ctx) { + fw_log(ctx->framework->logger, + CELIX_LOG_LEVEL_TRACE, + "Cleaning up bundle context `%s` (id=%li)", + celix_bundle_getSymbolicName(ctx->bundle), + celix_bundle_getId(ctx->bundle)); + + celix_framework_cleanupScheduledEvents(ctx->framework, celix_bundle_getId(ctx->bundle)); + // NOTE not perfect, because stopping of registrations/tracker when the activator is destroyed can lead to + // segfault. but at least we can try to warn the bundle implementer that some cleanup is missing. + bundleContext_cleanupBundleTrackers(ctx); + bundleContext_cleanupServiceTrackers(ctx); + bundleContext_cleanupServiceTrackerTrackers(ctx); + bundleContext_cleanupServiceRegistration(ctx); } celix_status_t bundleContext_getBundle(bundle_context_pt context, bundle_pt *out) { @@ -1492,6 +1499,41 @@ void celix_bundleContext_waitForEvents(celix_bundle_context_t* ctx) { celix_framework_waitUntilNoEventsForBnd(ctx->framework, celix_bundle_getId(ctx->bundle)); } +long celix_bundleContext_scheduleEvent(celix_bundle_context_t* ctx, + const celix_scheduled_event_options_t* options) { + return celix_framework_scheduleEvent(ctx->framework, + celix_bundle_getId(ctx->bundle), + options->name, + options->initialDelayInSeconds, + options->intervalInSeconds, + options->callbackData, + options->callback, + options->removeCallbackData, + options->removeCallback); +} + +celix_status_t celix_bundleContext_wakeupScheduledEvent(celix_bundle_context_t* ctx, long scheduledEventId) { + return celix_framework_wakeupScheduledEvent(ctx->framework, scheduledEventId); +} + +celix_status_t celix_bundleContext_waitForScheduledEvent(celix_bundle_context_t* ctx, + long scheduledEventId, + double waitTimeInSeconds) { + return celix_framework_waitForScheduledEvent(ctx->framework, scheduledEventId, waitTimeInSeconds); +} + +bool celix_bundleContext_removeScheduledEvent(celix_bundle_context_t* ctx, long scheduledEventId) { + return celix_framework_removeScheduledEvent(ctx->framework, false, true, scheduledEventId); +} + +bool celix_bundleContext_removeScheduledEventAsync(celix_bundle_context_t* ctx, long scheduledEventId) { + return celix_framework_removeScheduledEvent(ctx->framework, true, true, scheduledEventId); +} + +bool celix_bundleContext_tryRemoveScheduledEventAsync(celix_bundle_context_t* ctx, long scheduledEventId) { + return celix_framework_removeScheduledEvent(ctx->framework, true, false, scheduledEventId); +} + celix_bundle_t* celix_bundleContext_getBundle(const celix_bundle_context_t *ctx) { celix_bundle_t *bnd = NULL; if (ctx != NULL) { diff --git a/libs/framework/src/celix_scheduled_event.c b/libs/framework/src/celix_scheduled_event.c new file mode 100644 index 000000000..52e2873ef --- /dev/null +++ b/libs/framework/src/celix_scheduled_event.c @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "celix_scheduled_event.h" + +#include + +#include "celix_constants.h" +#include "celix_utils.h" + +/** + * @brief Default name for a scheduled event. + */ +static const char* const CELIX_SCHEDULED_EVENT_DEFAULT_NAME = "unnamed"; + +/** + * @brief Struct representing a scheduled event. + * + * A scheduled event is an event that is scheduled to be executed at a certain initial delay and/or interval. + * It is created using the `celix_bundleContext_scheduleEvent` function and can be woken up + * using the `celix_bundleContext_wakeupScheduledEvent` function. + * + * The struct contains information about the scheduled event, such as the event name, initial delay, + * interval, and callback function. It also contains synchronization primitives to protect the use + * count and call count of the event. + * + * @see celix_bundleContext_scheduleEvent + * @see celix_bundleContext_wakeupScheduledEvent + */ +struct celix_scheduled_event { + long scheduledEventId; /**< The ID of the scheduled event. */ + celix_framework_logger_t* logger; /**< The framework logger used to log information */ + long bndId; /**< The bundle id for the bundle that owns the scheduled event. */ + char* eventName; /**< The name of the scheduled event. Will be CELIX_SCHEDULED_EVENT_DEFAULT_NAME if no name is + provided during creation. */ + double logTimeoutInSeconds; /**< The timeout, in seconds, before a log message is printed while waiting for a + scheduled event to be processed or removed. */ + double initialDelayInSeconds; /**< The initial delay of the scheduled event in seconds. */ + double intervalInSeconds; /**< The interval of the scheduled event in seconds. */ + void* callbackData; /**< The data for the scheduled event callback. */ + void (*callback)(void* callbackData); /**< The callback function for the scheduled event. */ + void* removedCallbackData; /**< The data for the scheduled event removed callback. */ + void (*removedCallback)(void* removedCallbackData); /**< The callback function for the scheduled event removed + callback. */ + + celix_thread_mutex_t mutex; /**< The mutex to protect the data below. */ + celix_thread_cond_t cond; /**< The condition variable to signal the scheduled event for a changed callCount and + isRemoved. */ + size_t useCount; /**< The use count of the scheduled event. */ + size_t callCount; /**< The call count of the scheduled event. */ + bool isMarkedForRemoval; /**< Whether the scheduled event is marked for removal. */ + bool isRemoved; /**< Whether the scheduled event is removed. */ + struct timespec nextDeadline; /**< The next deadline of the scheduled event. */ + bool processForWakeup; /**< Whether the scheduled event should be processed directly due to a wakeupScheduledEvent + call. */ +}; + +celix_scheduled_event_t* celix_scheduledEvent_create(celix_framework_t* fw, + long bndId, + long scheduledEventId, + const char* providedEventName, + double initialDelayInSeconds, + double intervalInSeconds, + void* callbackData, + void (*callback)(void* callbackData), + void* removedCallbackData, + void (*removedCallback)(void* removedCallbackData)) { + celix_scheduled_event_t* event = malloc(sizeof(*event)); + char* eventName = + providedEventName == NULL ? (char*)CELIX_SCHEDULED_EVENT_DEFAULT_NAME : celix_utils_strdup(providedEventName); + if (event == NULL || eventName == NULL) { + fw_log(fw->logger, CELIX_LOG_LEVEL_ERROR, "Cannot add scheduled event for bundle id %li. Out of memory", bndId); + free(event); + if (eventName != CELIX_SCHEDULED_EVENT_DEFAULT_NAME) { + free(eventName); + } + return NULL; + } + + event->scheduledEventId = scheduledEventId; + event->logger = fw->logger; + event->bndId = bndId; + + event->eventName = eventName; + event->logTimeoutInSeconds = + celix_framework_getConfigPropertyAsDouble(fw, + CELIX_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS, + CELIX_DEFAULT_ALLOWED_PROCESSING_TIME_FOR_SCHEDULED_EVENT_IN_SECONDS, + NULL); + event->initialDelayInSeconds = initialDelayInSeconds; + event->intervalInSeconds = intervalInSeconds; + event->callbackData = callbackData; + event->callback = callback; + event->removedCallbackData = removedCallbackData; + event->removedCallback = removedCallback; + event->isMarkedForRemoval = false; + event->useCount = 1; + event->callCount = 0; + event->isRemoved = false; + event->nextDeadline = celixThreadCondition_getDelayedTime(event->initialDelayInSeconds); + event->processForWakeup = false; + + celixThreadMutex_create(&event->mutex, NULL); + celixThreadCondition_init(&event->cond, NULL); + + return event; +} + +static void celix_scheduledEvent_destroy(celix_scheduled_event_t* event) { + celixThreadMutex_destroy(&event->mutex); + celixThreadCondition_destroy(&event->cond); + if (event->eventName != CELIX_SCHEDULED_EVENT_DEFAULT_NAME) { + free(event->eventName); + } + free(event); +} + +celix_scheduled_event_t* celix_scheduledEvent_retain(celix_scheduled_event_t* event) { + if (event == NULL) { + return NULL; + } + + celixThreadMutex_lock(&event->mutex); + event->useCount += 1; + celixThreadMutex_unlock(&event->mutex); + return event; +} + +void celix_scheduledEvent_release(celix_scheduled_event_t* event) { + if (event == NULL) { + return; + } + + celixThreadMutex_lock(&event->mutex); + event->useCount -= 1; + bool unused = event->useCount == 0; + celixThreadMutex_unlock(&event->mutex); + + if (unused) { + celix_scheduledEvent_destroy(event); + } +} + +void celix_ScheduledEvent_cleanup(celix_scheduled_event_t** event) { + if (event) { + celix_scheduledEvent_release(*event); + } +} + +const char* celix_scheduledEvent_getName(const celix_scheduled_event_t* event) { return event->eventName; } + +long celix_scheduledEvent_getId(const celix_scheduled_event_t* event) { return event->scheduledEventId; } + +long celix_scheduledEvent_getBundleId(const celix_scheduled_event_t* event) { return event->bndId; } + +bool celix_scheduledEvent_deadlineReached(celix_scheduled_event_t* event, + const struct timespec* scheduleTime) { + celixThreadMutex_lock(&event->mutex); + double timeLeft = celix_difftime(scheduleTime, &event->nextDeadline); + bool deadlineReached = timeLeft <= 0; + if (event->processForWakeup) { + deadlineReached = true; + } + celixThreadMutex_unlock(&event->mutex); + return deadlineReached; +} + +struct timespec celix_scheduledEvent_getNextDeadline(celix_scheduled_event_t* event) { + celixThreadMutex_lock(&event->mutex); + struct timespec nextDeadline = event->nextDeadline; + celixThreadMutex_unlock(&event->mutex); + return nextDeadline; +} + +void celix_scheduledEvent_process(celix_scheduled_event_t* event) { + fw_log(event->logger, + CELIX_LOG_LEVEL_TRACE, + "Processing scheduled event %s for bundle id %li", + event->eventName, + event->bndId); + assert(event->callback != NULL); + + struct timespec start = celix_gettime(CLOCK_MONOTONIC); + event->callback(event->callbackData); // note called outside of lock + struct timespec end = celix_gettime(CLOCK_MONOTONIC); + + celixThreadMutex_lock(&event->mutex); + event->nextDeadline = celixThreadCondition_getDelayedTime(event->intervalInSeconds); + event->callCount += 1; + event->processForWakeup = false; + celixThreadCondition_broadcast(&event->cond); // for changed callCount + celixThreadMutex_unlock(&event->mutex); + + if (celix_difftime(&start, &end) > event->logTimeoutInSeconds) { + fw_log(event->logger, + CELIX_LOG_LEVEL_WARNING, + "Processing scheduled event %s for bundle id %li took %f seconds", + event->eventName, + event->bndId, + celix_difftime(&start, &end)); + } +} + +bool celix_scheduledEvent_isSingleShot(const celix_scheduled_event_t* event) { + return event->intervalInSeconds == 0; +} + +size_t celix_scheduledEvent_markForWakeup(celix_scheduled_event_t* event) { + celixThreadMutex_lock(&event->mutex); + event->processForWakeup = true; + size_t currentCallCount = event->callCount; + celixThreadMutex_unlock(&event->mutex); + + fw_log(event->logger, + CELIX_LOG_LEVEL_DEBUG, + "Wakeup scheduled event '%s' (id=%li) for bundle id %li", + event->eventName, + event->scheduledEventId, + event->bndId); + + return currentCallCount + 1; +} + +void celix_scheduledEvent_waitForRemoved(celix_scheduled_event_t* event) { + struct timespec absLogTimeout = + celixThreadCondition_getDelayedTime(event->logTimeoutInSeconds); + celixThreadMutex_lock(&event->mutex); + while (!event->isRemoved) { + celix_status_t waitStatus = celixThreadCondition_waitUntil(&event->cond, &event->mutex, &absLogTimeout); + if (waitStatus == ETIMEDOUT) { + fw_log(event->logger, + CELIX_LOG_LEVEL_WARNING, + "Timeout while waiting for removal of scheduled event '%s' (id=%li) for bundle id %li.", + event->eventName, + event->scheduledEventId, + event->bndId); + absLogTimeout = celixThreadCondition_getDelayedTime(event->logTimeoutInSeconds); + } + } + celixThreadMutex_unlock(&event->mutex); +} + +celix_status_t celix_scheduledEvent_wait(celix_scheduled_event_t* event, double timeoutInSeconds) { + celix_status_t status = CELIX_SUCCESS; + celixThreadMutex_lock(&event->mutex); + size_t targetCallCount = event->callCount + 1; + struct timespec absTimeoutTime = celixThreadCondition_getDelayedTime(timeoutInSeconds); + while (event->callCount < targetCallCount && !event->isRemoved) { + status = celixThreadCondition_waitUntil(&event->cond, &event->mutex, &absTimeoutTime); + if (status == ETIMEDOUT) { + break; + } + status = CELIX_SUCCESS; + } + celixThreadMutex_unlock(&event->mutex); + return status; +} + +void celix_scheduledEvent_setRemoved(celix_scheduled_event_t* event) { + if (event->removedCallback) { + event->removedCallback(event->removedCallbackData); + } + celixThreadMutex_lock(&event->mutex); + event->isRemoved = true; + celixThreadCondition_broadcast(&event->cond); // for changed isRemoved + celixThreadMutex_unlock(&event->mutex); +} + +void celix_scheduledEvent_markForRemoval(celix_scheduled_event_t* event) { + celixThreadMutex_lock(&event->mutex); + event->isMarkedForRemoval = true; + celixThreadMutex_unlock(&event->mutex); +} + +bool celix_scheduledEvent_isMarkedForRemoval(celix_scheduled_event_t* event) { + celixThreadMutex_lock(&event->mutex); + bool isMarkedForRemoval = event->isMarkedForRemoval; + celixThreadMutex_unlock(&event->mutex); + return isMarkedForRemoval; +} + +bool celix_scheduledEvent_requiresProcessing(celix_scheduled_event_t* event, const struct timespec* scheduleTime) { + return celix_scheduledEvent_deadlineReached(event, scheduleTime) || + celix_scheduledEvent_isMarkedForRemoval(event); +} diff --git a/libs/framework/src/celix_scheduled_event.h b/libs/framework/src/celix_scheduled_event.h new file mode 100644 index 000000000..c42f132a5 --- /dev/null +++ b/libs/framework/src/celix_scheduled_event.h @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#ifndef CELIX_CELIX_SCHEDULED_EVENT_H +#define CELIX_CELIX_SCHEDULED_EVENT_H + +#include "celix_bundle_context.h" +#include "framework_private.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief A scheduled event is an event which is scheduled to be processed at a given initial delay and interval. + */ +typedef struct celix_scheduled_event celix_scheduled_event_t; + +/** + * @brief Create a scheduled event for the given bundle. + * + * The scheduled event will be created with a use count of 1. + * + * @param[in] fw The framework. + * @param[in] bndId The bundle id for the bundle which the scheduled event is created. + * @param[in] scheduledEventId The id of the scheduled event. + * @param[in] providedEventName The name of the event. If NULL, CELIX_SCHEDULED_EVENT_DEFAULT_NAME is used. + * @param[in] initialDelayInSeconds The initial delay in seconds. + * @param[in] intervalInSeconds The interval in seconds. + * @param[in] callbackData The event data. + * @param[in] callback The event callback. + * @param[in] removedCallbackData The removed callback data. + * @param[in] removedCallback The removed callback. + * @return A new scheduled event or NULL if failed. + */ +celix_scheduled_event_t* celix_scheduledEvent_create(celix_framework_t* fw, + long bndId, + long scheduledEventId, + const char* providedEventName, + double initialDelayInSeconds, + double intervalInSeconds, + void* callbackData, + void (*callback)(void* callbackData), + void* removedCallbackData, + void (*removedCallback)(void* removedCallbackData)); + +/** + * @brief Retain the scheduled event by increasing the use count. + * Will silently ignore a NULL event. + * + * @param[in] event The event to retain. + * @return The retained event or NULL if the event is NULL. + */ +celix_scheduled_event_t* celix_scheduledEvent_retain(celix_scheduled_event_t* event); + +/** + * @brief Release the scheduled event by decreasing the use count. If the use count is 0, + * the scheduled event is destroyed. Will silently ignore a NULL event. + */ +void celix_scheduledEvent_release(celix_scheduled_event_t* event); + +/** + * @brief Call the release of provided the scheduled event pointer. + * Meant to be pair with a __cleanup__ attribute or CELIX_SCHEDULED_EVENT_RETAIN_GUARD macro. + */ +void celix_ScheduledEvent_cleanup(celix_scheduled_event_t** event); + +/*! + * @brief Retain the scheduled event, add a cleanup attribute to release the scheduled event and + * return a pointer to it. + * + * This macro can be used as a guard to automatically release the scheduled event when leaving the current scope. + * + * @param[in] __var_name__ The name of the variable to create. + * @param[in] __scheduled_event__ The scheduled event to retain. + * @return A pointer to the retained scheduled event. + */ +#define CELIX_SCHEDULED_EVENT_RETAIN_GUARD(__var_name__, __scheduled_event__) \ + __attribute__((__cleanup__(celix_ScheduledEvent_cleanup))) celix_scheduled_event_t* __var_name__ = \ + celix_scheduledEvent_retain(__scheduled_event__) + +/** + * @brief Returns the scheduled event name. + */ +const char* celix_scheduledEvent_getName(const celix_scheduled_event_t* event); + +/** + * @brief Returns the scheduled event ID. + */ +long celix_scheduledEvent_getId(const celix_scheduled_event_t* event); + +/** + * @brief Returns the bundle id of the bundle which created the scheduled event. + */ +long celix_scheduledEvent_getBundleId(const celix_scheduled_event_t* event); + +/** + * @brief Returns whether the event deadline is reached and the event should be processed. + * @param[in] event The event to check. + * @param[in] scheduleTime The schedule time. + * @return true if the event deadline is reached and the event should be processed. + */ +bool celix_scheduledEvent_deadlineReached(celix_scheduled_event_t* event, + const struct timespec* scheduleTime); + +/** + * @brief Get the next deadline for the scheduled event. + * @param[in] event The event to get the next deadline for. + * @return The next deadline for the scheduled event. + */ +struct timespec celix_scheduledEvent_getNextDeadline(celix_scheduled_event_t* event); + +/** + * @brief Process the event by calling the event callback. + * + * Must be called on the Celix event thread. + * + * @param[in] event The event to process. + */ +void celix_scheduledEvent_process(celix_scheduled_event_t* event); + +/** + * @brief Call the removed callback of the event and set the removed flag. + */ +void celix_scheduledEvent_setRemoved(celix_scheduled_event_t* event); + +/** + * @brief Wait indefinitely until the event is removed. + */ +void celix_scheduledEvent_waitForRemoved(celix_scheduled_event_t* event); + +/** + * @brief Returns true if the event is a one-shot event. + */ +bool celix_scheduledEvent_isSingleShot(const celix_scheduled_event_t* event); + +/** + * @brief Mark the event for removal. The event will be removed on the event thread, after the next processing. + */ +void celix_scheduledEvent_markForRemoval(celix_scheduled_event_t* event); + +/** + * @brief Returns true if the event is marked for removal. + */ +bool celix_scheduledEvent_isMarkedForRemoval(celix_scheduled_event_t* event); + +/** + * @brief Configure a scheduled event for a wakeup, so celix_scheduledEvent_deadlineReached will return true until + * the event is processed. + * + * @param[in] event The event to configure for wakeup. + * @return The future call count of the event after the next processing is done. + */ +size_t celix_scheduledEvent_markForWakeup(celix_scheduled_event_t* event); + +/** + * @brief Wait for a scheduled event to be done with the next scheduled processing. + * @param[in] event The event to wait for. + * @param[in] timeoutInSeconds The max time to wait in seconds. Must be > 0. + * @return CELIX_SUCCESS if the scheduled event is done with processing, ETIMEDOUT if the scheduled event is not + * done with processing within the timeout. + */ +celix_status_t celix_scheduledEvent_wait(celix_scheduled_event_t* event, double timeoutInSeconds); + +/** + * @brief Returns true if the event is marked for wakeup, the initial delay or interval deadline is reached or the + * event is marked for removal for the given time. + */ +bool celix_scheduledEvent_requiresProcessing(celix_scheduled_event_t* event, const struct timespec* scheduleTime); + +#ifdef __cplusplus +}; +#endif + +#endif // CELIX_CELIX_SCHEDULED_EVENT_H diff --git a/libs/framework/src/framework.c b/libs/framework/src/framework.c index 57f3f41f6..208afa500 100644 --- a/libs/framework/src/framework.c +++ b/libs/framework/src/framework.c @@ -45,6 +45,7 @@ #include "resolver.h" #include "service_reference_private.h" #include "service_registration_private.h" +#include "celix_scheduled_event.h" #include "utils.h" struct celix_bundle_activator { @@ -56,6 +57,7 @@ struct celix_bundle_activator { celix_bundle_activator_destroy_fp destroy; }; +static int celix_framework_eventQueueSize(celix_framework_t* fw); static celix_status_t celix_framework_stopBundleEntryInternal(celix_framework_t* framework, celix_framework_bundle_entry_t* bndEntry); static inline celix_framework_bundle_entry_t* fw_bundleEntry_create(celix_bundle_t *bnd) { @@ -69,23 +71,6 @@ static inline celix_framework_bundle_entry_t* fw_bundleEntry_create(celix_bundle return entry; } - -static inline void fw_bundleEntry_waitTillUseCountIs(celix_framework_bundle_entry_t *entry, size_t desiredUseCount) { - celixThreadMutex_lock(&entry->useMutex); - struct timespec start = celix_gettime(CLOCK_MONOTONIC); - while (entry->useCount != desiredUseCount) { - celixThreadCondition_timedwaitRelative(&entry->useCond, &entry->useMutex, 5, 0); - if (entry->useCount != desiredUseCount) { - struct timespec now = celix_gettime(CLOCK_MONOTONIC); - if (celix_difftime(&start, &now) > 5) { - fw_log(celix_frameworkLogger_globalLogger(), CELIX_LOG_LEVEL_WARNING, "Bundle '%s' (bnd id = %li) still in use. Use count is %zu, desired is %zu", celix_bundle_getSymbolicName(entry->bnd), entry->bndId, entry->useCount, desiredUseCount); - start = celix_gettime(CLOCK_MONOTONIC); - } - } - } - celixThreadMutex_unlock(&entry->useMutex); -} - static inline void fw_bundleEntry_destroy(celix_framework_bundle_entry_t *entry, bool wait) { celixThreadMutex_lock(&entry->useMutex); while (wait && entry->useCount != 0) { @@ -254,6 +239,7 @@ celix_status_t framework_create(framework_pt *out, celix_properties_t* config) { framework->dispatcher.eventQueueCap = (int)celix_framework_getConfigPropertyAsLong(framework, CELIX_FRAMEWORK_STATIC_EVENT_QUEUE_SIZE, CELIX_FRAMEWORK_DEFAULT_STATIC_EVENT_QUEUE_SIZE, NULL); framework->dispatcher.eventQueue = malloc(sizeof(celix_framework_event_t) * framework->dispatcher.eventQueueCap); framework->dispatcher.dynamicEventQueue = celix_arrayList_create(); + framework->dispatcher.scheduledEvents = celix_longHashMap_create(); //create and store framework uuid char uuid[37]; @@ -382,6 +368,9 @@ celix_status_t framework_destroy(framework_pt framework) { assert(celix_arrayList_size(framework->dispatcher.dynamicEventQueue) == 0); celix_arrayList_destroy(framework->dispatcher.dynamicEventQueue); + assert(celix_longHashMap_size(framework->dispatcher.scheduledEvents) == 0); + celix_longHashMap_destroy(framework->dispatcher.scheduledEvents); + celix_bundleCache_destroy(framework->cache); celixThreadCondition_destroy(&framework->dispatcher.cond); @@ -1179,17 +1168,6 @@ static void* framework_shutdown(void *framework) { size = celix_arrayList_size(stopEntries); - for (int i = size-1; i >= 0; --i) { //note loop in reverse order -> stop later installed bundle first - celix_framework_bundle_entry_t *entry = celix_arrayList_get(stopEntries, i); - - //NOTE possible starvation. - fw_bundleEntry_waitTillUseCountIs(entry, 1); //note this function has 1 use count. - - bundle_state_e state = celix_bundle_getState(entry->bnd); - if (state == CELIX_BUNDLE_STATE_ACTIVE || state == CELIX_BUNDLE_STATE_STARTING) { - celix_framework_stopBundleEntry(fw, entry); - } - } for (int i = size-1; i >= 0; --i) { //note loop in reverse order -> uninstall later installed bundle first celix_framework_bundle_entry_t *entry = celix_arrayList_get(stopEntries, i); celix_framework_uninstallBundleEntry(fw, entry, false); @@ -1197,19 +1175,15 @@ static void* framework_shutdown(void *framework) { celix_arrayList_destroy(stopEntries); - // 'stop' framework bundle + // make sure the framework has been stopped if (fwEntry != NULL) { - bundle_t *bnd = fwEntry->bnd; - fw_bundleEntry_waitTillUseCountIs(fwEntry, 1); //note this function has 1 use count. - - bundle_state_e state; - bundle_getState(bnd, &state); - if (state == CELIX_BUNDLE_STATE_ACTIVE || state == CELIX_BUNDLE_STATE_STARTING) { - celix_framework_stopBundleEntry(fw, fwEntry); - } + // Lock the mutex to make sure that `celix_framework_stopBundleEntryInternal` on the framework has finished. + celixThreadRwlock_readLock(&fwEntry->fsmMutex); + celixThreadRwlock_unlock(&fwEntry->fsmMutex); celix_framework_bundleEntry_decreaseUseCount(fwEntry); } + //Now that all bundled has been stopped, no more events will be sent, we can safely stop the event dispatcher. //join dispatcher thread celixThreadMutex_lock(&fw->dispatcher.mutex); fw->dispatcher.active = false; @@ -1400,6 +1374,7 @@ static inline bool fw_removeTopEventFromQueue(celix_framework_t* fw) { celix_arrayList_removeAt(fw->dispatcher.dynamicEventQueue, 0); dynamicallyAllocated = true; } + celixThreadCondition_broadcast(&fw->dispatcher.cond); //notify that the queue size is changed celixThreadMutex_unlock(&fw->dispatcher.mutex); return dynamicallyAllocated; } @@ -1408,32 +1383,160 @@ static inline bool fw_removeTopEventFromQueue(celix_framework_t* fw) { static inline void fw_handleEvents(celix_framework_t* framework) { celixThreadMutex_lock(&framework->dispatcher.mutex); int size = framework->dispatcher.eventQueueSize + celix_arrayList_size(framework->dispatcher.dynamicEventQueue); - if (size == 0 && framework->dispatcher.active) { - celixThreadCondition_timedwaitRelative(&framework->dispatcher.cond, &framework->dispatcher.mutex, 1, 0); - } - size = framework->dispatcher.eventQueueSize + celix_arrayList_size(framework->dispatcher.dynamicEventQueue); celixThreadMutex_unlock(&framework->dispatcher.mutex); while (size > 0) { celix_framework_event_t* topEvent = fw_topEventFromQueue(framework); fw_handleEventRequest(framework, topEvent); - bool dynamiclyAllocatedEvent = fw_removeTopEventFromQueue(framework); + bool dynamicallyAllocatedEvent = fw_removeTopEventFromQueue(framework); if (topEvent->bndEntry != NULL) { celix_framework_bundleEntry_decreaseUseCount(topEvent->bndEntry); } free(topEvent->serviceName); - if (dynamiclyAllocatedEvent) { + if (dynamicallyAllocatedEvent) { free(topEvent); } celixThreadMutex_lock(&framework->dispatcher.mutex); size = framework->dispatcher.eventQueueSize + celix_arrayList_size(framework->dispatcher.dynamicEventQueue); - celixThreadCondition_broadcast(&framework->dispatcher.cond); celixThreadMutex_unlock(&framework->dispatcher.mutex); } } +/** + * @brief Process all scheduled events. + */ +static void celix_framework_processScheduledEvents(celix_framework_t* fw) { + struct timespec scheduleTime = celixThreadCondition_getTime(); + celix_scheduled_event_t* callEvent; + celix_scheduled_event_t* removeEvent; + do { + callEvent = NULL; + removeEvent = NULL; + 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)) { + removeEvent = visit; + celix_longHashMap_remove(fw->dispatcher.scheduledEvents, celix_scheduledEvent_getId(visit)); + break; + } + + bool call = celix_scheduledEvent_deadlineReached(visit, &scheduleTime); + if (call) { + callEvent = visit; + if (celix_scheduledEvent_isSingleShot(visit)) { + removeEvent = visit; + celix_longHashMap_remove(fw->dispatcher.scheduledEvents, celix_scheduledEvent_getId(visit)); + } + break; + } + } + celixThreadMutex_unlock(&fw->dispatcher.mutex); + + if (callEvent != NULL) { + celix_scheduledEvent_process(callEvent); + } + if (removeEvent != NULL) { + fw_log(fw->logger, + CELIX_LOG_LEVEL_DEBUG, + "Removing processed %s""scheduled event '%s' (id=%li) for bundle if %li.", + celix_scheduledEvent_isSingleShot(removeEvent) ? "one-shot " : "", + celix_scheduledEvent_getName(removeEvent), + celix_scheduledEvent_getId(removeEvent), + celix_scheduledEvent_getBundleId(removeEvent)); + celix_scheduledEvent_setRemoved(removeEvent); + celix_scheduledEvent_release(removeEvent); + } + } while (callEvent || removeEvent); +} + +/** + * @brief Calculate the next deadline for scheduled events. + * @return The next deadline or a time 0 second and 0 nanoseconds if no scheduled events are available. + */ +static struct timespec celix_framework_nextDeadlineForScheduledEvents(celix_framework_t* framework) { + struct timespec closestDeadline = {0,0}; + celixThreadMutex_lock(&framework->dispatcher.mutex); + CELIX_LONG_HASH_MAP_ITERATE(framework->dispatcher.scheduledEvents, entry) { + celix_scheduled_event_t *visit = entry.value.ptrValue; + struct timespec eventDeadline = celix_scheduledEvent_getNextDeadline(visit); + if (closestDeadline.tv_sec == 0 && closestDeadline.tv_nsec == 0) { + closestDeadline = eventDeadline; + } else if (celix_compareTime(&eventDeadline, &closestDeadline) < 0) { + closestDeadline = eventDeadline; + } + } + celixThreadMutex_unlock(&framework->dispatcher.mutex); + return closestDeadline; +} + +void celix_framework_cleanupScheduledEvents(celix_framework_t* fw, long bndId) { + celix_scheduled_event_t* removeEvent; + do { + removeEvent = NULL; + celixThreadMutex_lock(&fw->dispatcher.mutex); + CELIX_LONG_HASH_MAP_ITERATE(fw->dispatcher.scheduledEvents, entry) { + celix_scheduled_event_t* visit = entry.value.ptrValue; + if (bndId == celix_scheduledEvent_getBundleId(visit)) { + removeEvent = visit; + celix_scheduledEvent_retain(removeEvent); + if (!celix_scheduledEvent_isSingleShot(removeEvent)) { + fw_log(fw->logger, + CELIX_LOG_LEVEL_WARNING, + "Removing dangling scheduled event '%s' (id=%li) for bundle id %li. This scheduled event should " + "have been removed up by the bundle.", + celix_scheduledEvent_getName(removeEvent), + celix_scheduledEvent_getId(removeEvent), + celix_scheduledEvent_getBundleId(removeEvent)); + } + celix_scheduledEvent_markForRemoval(removeEvent); + celixThreadCondition_broadcast(&fw->dispatcher.cond); //notify that scheduled event is marked for removal + break; + } + } + celixThreadMutex_unlock(&fw->dispatcher.mutex); + + if (removeEvent) { + celix_scheduledEvent_waitForRemoved(removeEvent); + celix_scheduledEvent_release(removeEvent); + } + } while (removeEvent != NULL); +} + +static int celix_framework_eventQueueSize(celix_framework_t* fw) { + //precondition fw->dispatcher.mutex lockedx); + return fw->dispatcher.eventQueueSize + celix_arrayList_size(fw->dispatcher.dynamicEventQueue); +} + +static bool requiresScheduledEventsProcessing(celix_framework_t* framework) { + // precondition framework->dispatcher.mutex locked + struct timespec currentTime = celixThreadCondition_getTime(); + bool eventProcessingRequired = false; + CELIX_LONG_HASH_MAP_ITERATE(framework->dispatcher.scheduledEvents, mapEntry) { + celix_scheduled_event_t* visit = mapEntry.value.ptrValue; + if (celix_scheduledEvent_requiresProcessing(visit, ¤tTime)) { + eventProcessingRequired = true; + break; + } + } + return eventProcessingRequired; +} + +static void celix_framework_waitForNextEvent(celix_framework_t* fw, struct timespec nextDeadline) { + if (nextDeadline.tv_sec == 0 && nextDeadline.tv_nsec == 0) { + nextDeadline = celixThreadCondition_getDelayedTime(1); //no next deadline, wait max 1s + } + celixThreadMutex_lock(&fw->dispatcher.mutex); + if (celix_framework_eventQueueSize(fw) == 0 && !requiresScheduledEventsProcessing(fw) && fw->dispatcher.active) { + celixThreadCondition_waitUntil(&fw->dispatcher.cond, &fw->dispatcher.mutex, &nextDeadline); + // note failing through to fw_eventDispatcher even if timeout is not reached, the fw_eventDispatcher + // will call this again after processing the events and scheduled events. + } + celixThreadMutex_unlock(&fw->dispatcher.mutex); +} + static void *fw_eventDispatcher(void *fw) { framework_pt framework = (framework_pt) fw; @@ -1443,17 +1546,24 @@ static void *fw_eventDispatcher(void *fw) { while (active) { fw_handleEvents(framework); + celix_framework_processScheduledEvents(framework); + struct timespec nextDeadline = celix_framework_nextDeadlineForScheduledEvents(framework); + celix_framework_waitForNextEvent(framework, nextDeadline); + celixThreadMutex_lock(&framework->dispatcher.mutex); active = framework->dispatcher.active; celixThreadMutex_unlock(&framework->dispatcher.mutex); } - //not active any more, last run for possible request leftovers + //not active anymore, extra runs for possible request leftovers celixThreadMutex_lock(&framework->dispatcher.mutex); - bool needLastRun = framework->dispatcher.eventQueueSize > 0 || celix_arrayList_size(framework->dispatcher.dynamicEventQueue) > 0; + bool needExtraRun = celix_framework_eventQueueSize(fw) > 0; celixThreadMutex_unlock(&framework->dispatcher.mutex); - if (needLastRun) { + while (needExtraRun) { fw_handleEvents(framework); + celixThreadMutex_lock(&framework->dispatcher.mutex); + needExtraRun = celix_framework_eventQueueSize(fw) > 0; + celixThreadMutex_unlock(&framework->dispatcher.mutex); } celixThread_exit(NULL); @@ -1837,7 +1947,6 @@ bundle_pt framework_getBundleById(framework_pt framework, long id) { return bnd; } - bool celix_framework_isBundleInstalled(celix_framework_t *fw, long bndId) { bool isInstalled = false; celix_framework_bundle_entry_t *entry = celix_framework_bundleEntry_getBundleEntryAndIncreaseUseCount(fw, bndId); @@ -2393,14 +2502,30 @@ celix_array_list_t* celix_framework_listInstalledBundles(celix_framework_t* fram return celix_framework_listBundlesInternal(framework, false); } -void celix_framework_waitForEmptyEventQueue(celix_framework_t *fw) { +celix_status_t celix_framework_waitForEmptyEventQueueFor(celix_framework_t *fw, double periodInSeconds) { assert(!celix_framework_isCurrentThreadTheEventLoop(fw)); + celix_status_t status = CELIX_SUCCESS; + struct timespec absTimeout = {0, 0}; + absTimeout = (periodInSeconds == 0) ? absTimeout : celixThreadCondition_getDelayedTime(periodInSeconds); celixThreadMutex_lock(&fw->dispatcher.mutex); - while (fw->dispatcher.eventQueueSize > 0 || celix_arrayList_size(fw->dispatcher.dynamicEventQueue) > 0) { - celixThreadCondition_wait(&fw->dispatcher.cond, &fw->dispatcher.mutex); + while (celix_framework_eventQueueSize(fw) > 0) { + if (periodInSeconds == 0) { + celixThreadCondition_wait(&fw->dispatcher.cond, &fw->dispatcher.mutex); + } else { + status = celixThreadCondition_waitUntil(&fw->dispatcher.cond, &fw->dispatcher.mutex, &absTimeout); + if (status == ETIMEDOUT) { + break; + } + } + } celixThreadMutex_unlock(&fw->dispatcher.mutex); + return status; +} + +void celix_framework_waitForEmptyEventQueue(celix_framework_t *fw) { + celix_framework_waitForEmptyEventQueueFor(fw, 0.0); } void celix_framework_waitUntilNoEventsForBnd(celix_framework_t* fw, long bndId) { @@ -2413,14 +2538,14 @@ void celix_framework_waitUntilNoEventsForBnd(celix_framework_t* fw, long bndId) for (int i = 0; i < fw->dispatcher.eventQueueSize; ++i) { int index = (fw->dispatcher.eventQueueFirstEntry + i) % fw->dispatcher.eventQueueCap; celix_framework_event_t* e = &fw->dispatcher.eventQueue[index]; - if (e->bndEntry != NULL && e->bndEntry->bndId == bndId) { + if (e->bndEntry != NULL && (bndId < 0 || e->bndEntry->bndId == bndId)) { eventInProgress = true; break; } } for (int i = 0; !eventInProgress && i < celix_arrayList_size(fw->dispatcher.dynamicEventQueue); ++i) { celix_framework_event_t* e = celix_arrayList_get(fw->dispatcher.dynamicEventQueue, i); - if (e->bndEntry != NULL && e->bndEntry->bndId == bndId) { + if (e->bndEntry != NULL && (bndId < 0 || e->bndEntry->bndId == bndId)) { eventInProgress = true; break; } @@ -2442,6 +2567,134 @@ void celix_framework_waitUntilNoPendingRegistration(celix_framework_t* fw) celixThreadMutex_unlock(&fw->dispatcher.mutex); } +long celix_framework_scheduleEvent(celix_framework_t* fw, + long bndId, + const char* eventName, + double initialDelayInSeconds, + double intervalInSeconds, + void* callbackData, + void (*callback)(void*), + void* removeCallbackData, + void (*removeCallback)(void*)) { + if (callback == NULL) { + fw_log(fw->logger, + CELIX_LOG_LEVEL_ERROR, + "Cannot add scheduled event for bundle id %li. Invalid NULL event callback.", + bndId); + return -1; + } + + celix_framework_bundle_entry_t* bndEntry = celix_framework_bundleEntry_getBundleEntryAndIncreaseUseCount(fw, bndId); + if (bndEntry == NULL) { + fw_log(fw->logger, CELIX_LOG_LEVEL_ERROR, "Cannot add scheduled event for non existing bundle id %li.", bndId); + return -1; + } + celix_scheduled_event_t* event = celix_scheduledEvent_create(fw, + bndEntry->bndId, + celix_framework_nextScheduledEventId(fw), + eventName, + initialDelayInSeconds, + intervalInSeconds, + callbackData, + callback, + removeCallbackData, + removeCallback); + + if (event == NULL) { + celix_framework_bundleEntry_decreaseUseCount(bndEntry); + return -1L; //error logged by celix_scheduledEvent_create + } + + fw_log(fw->logger, + CELIX_LOG_LEVEL_DEBUG, + "Added scheduled event '%s' (id=%li) for bundle '%s' (id=%li).", + celix_scheduledEvent_getName(event), + celix_scheduledEvent_getId(event), + celix_bundle_getSymbolicName(bndEntry->bnd), + bndId); + celix_framework_bundleEntry_decreaseUseCount(bndEntry); + + celixThreadMutex_lock(&fw->dispatcher.mutex); + celix_longHashMap_put(fw->dispatcher.scheduledEvents, celix_scheduledEvent_getId(event), event); + celixThreadCondition_broadcast(&fw->dispatcher.cond); //notify dispatcher thread for newly added scheduled event + celixThreadMutex_unlock(&fw->dispatcher.mutex); + + return celix_scheduledEvent_getId(event); +} + +celix_status_t celix_framework_wakeupScheduledEvent(celix_framework_t* fw, long scheduledEventId) { + if (scheduledEventId < 0) { + return CELIX_SUCCESS; // silently ignore + } + celixThreadMutex_lock(&fw->dispatcher.mutex); + celix_scheduled_event_t* event = celix_longHashMap_get(fw->dispatcher.scheduledEvents, scheduledEventId); + if (event != NULL) { + celix_scheduledEvent_markForWakeup(event); + celixThreadCondition_broadcast(&fw->dispatcher.cond); //notify dispatcher thread for configured wakeup + } + celixThreadMutex_unlock(&fw->dispatcher.mutex); + + if (event == NULL) { + fw_log(fw->logger, + CELIX_LOG_LEVEL_WARNING, + "celix_framework_wakeupScheduledEvent called with unknown scheduled event id %li.", + scheduledEventId); + return CELIX_ILLEGAL_ARGUMENT; + } + + return CELIX_SUCCESS; +} + +celix_status_t +celix_framework_waitForScheduledEvent(celix_framework_t* fw, long scheduledEventId, double waitTimeInSeconds) { + if (scheduledEventId < 0) { + return CELIX_SUCCESS; // silently ignore + } + + celixThreadMutex_lock(&fw->dispatcher.mutex); + CELIX_SCHEDULED_EVENT_RETAIN_GUARD(event, celix_longHashMap_get(fw->dispatcher.scheduledEvents, scheduledEventId)); + celixThreadMutex_unlock(&fw->dispatcher.mutex); + + if (event == NULL) { + fw_log(fw->logger, + CELIX_LOG_LEVEL_WARNING, + "Cannot wait for scheduled event. Unknown scheduled event id %li.", + scheduledEventId); + return CELIX_ILLEGAL_ARGUMENT; + } + + celix_status_t status = celix_scheduledEvent_wait(event, waitTimeInSeconds); + return status; +} + +bool celix_framework_removeScheduledEvent(celix_framework_t* fw, + bool async, + bool errorIfNotFound, + long scheduledEventId) { + if (scheduledEventId < 0) { + return false; // silently ignore + } + + celixThreadMutex_lock(&fw->dispatcher.mutex); + CELIX_SCHEDULED_EVENT_RETAIN_GUARD(event, celix_longHashMap_get(fw->dispatcher.scheduledEvents, scheduledEventId)); + if (event) { + celix_scheduledEvent_markForRemoval(event); + celixThreadCondition_broadcast(&fw->dispatcher.cond); //notify dispatcher thread for removed scheduled event + } + celixThreadMutex_unlock(&fw->dispatcher.mutex); + + if (!event) { + celix_log_level_e level = errorIfNotFound ? CELIX_LOG_LEVEL_ERROR : CELIX_LOG_LEVEL_TRACE; + fw_log(fw->logger, level, "Cannot remove scheduled event with id %li. Not found.", scheduledEventId); + return false; + } + + if (!async) { + celix_scheduledEvent_waitForRemoved(event); + } + return true; +} + void celix_framework_setLogCallback(celix_framework_t* fw, void* logHandle, void (*logFunction)(void* handle, celix_log_level_e level, const char* file, const char *function, int line, const char *format, va_list formatArgs)) { celix_frameworkLogger_setLogCallback(fw->logger, logHandle, logFunction); } @@ -2451,7 +2704,7 @@ long celix_framework_fireGenericEvent(framework_t* fw, long eventId, long bndId, if (bndId >=0) { bndEntry = celix_framework_bundleEntry_getBundleEntryAndIncreaseUseCount(fw, bndId); if (bndEntry == NULL) { - fw_log(fw->logger, CELIX_LOG_LEVEL_ERROR, "Cannot find bundle for id %li", bndId); + fw_log(fw->logger, CELIX_LOG_LEVEL_ERROR, "Cannot find bundle for id %li.", bndId); return -1L; } } @@ -2477,33 +2730,44 @@ long celix_framework_fireGenericEvent(framework_t* fw, long eventId, long bndId, } long celix_framework_nextEventId(framework_t *fw) { - return __atomic_fetch_add(&fw->nextGenericEventId, 1, __ATOMIC_RELAXED); + return __atomic_fetch_add(&fw->dispatcher.nextEventId, 1, __ATOMIC_RELAXED); } -void celix_framework_waitForGenericEvent(framework_t *fw, long eventId) { - assert(!celix_framework_isCurrentThreadTheEventLoop(fw)); +long celix_framework_nextScheduledEventId(framework_t *fw) { + return __atomic_fetch_add(&fw->dispatcher.nextScheduledEventId, 1, __ATOMIC_RELAXED); +} - celixThreadMutex_lock(&fw->dispatcher.mutex); - bool eventInProgress = true; - while (eventInProgress) { - eventInProgress = false; - for (int i = 0; i < fw->dispatcher.eventQueueSize; ++i) { - int index = (fw->dispatcher.eventQueueFirstEntry + i) % fw->dispatcher.eventQueueCap; - celix_framework_event_t* e = &fw->dispatcher.eventQueue[index]; - if (e->type == CELIX_GENERIC_EVENT && e->genericEventId == eventId) { - eventInProgress = true; - break; - } +/** + * @brief Checks if a generic event with the provided eventId is in progress. + */ +static bool celix_framework_isGenericEventInProgress(celix_framework_t* fw, long eventId) { + // precondition fw->dispatcher.mutex locked) + for (int i = 0; i < fw->dispatcher.eventQueueSize; ++i) { + int index = (fw->dispatcher.eventQueueFirstEntry + i) % fw->dispatcher.eventQueueCap; + celix_framework_event_t* e = &fw->dispatcher.eventQueue[index]; + if (e->type == CELIX_GENERIC_EVENT && e->genericEventId == eventId) { + return true;; } - for (int i = 0; !eventInProgress && i < celix_arrayList_size(fw->dispatcher.dynamicEventQueue); ++i) { - celix_framework_event_t* e = celix_arrayList_get(fw->dispatcher.dynamicEventQueue, i); - if (e->type == CELIX_GENERIC_EVENT && e->genericEventId == eventId) { - eventInProgress = true; - break; - } + } + for (int i = 0; i < celix_arrayList_size(fw->dispatcher.dynamicEventQueue); ++i) { + celix_framework_event_t* e = celix_arrayList_get(fw->dispatcher.dynamicEventQueue, i); + if (e->type == CELIX_GENERIC_EVENT && e->genericEventId == eventId) { + return true; } - if (eventInProgress) { - celixThreadCondition_timedwaitRelative(&fw->dispatcher.cond, &fw->dispatcher.mutex, 5, 0); + } + return false; +} + +void celix_framework_waitForGenericEvent(celix_framework_t* fw, long eventId) { + assert(!celix_framework_isCurrentThreadTheEventLoop(fw)); + struct timespec logAbsTime = celixThreadCondition_getDelayedTime(5); + celixThreadMutex_lock(&fw->dispatcher.mutex); + while (celix_framework_isGenericEventInProgress(fw, eventId)) { + celix_status_t waitStatus = + celixThreadCondition_waitUntil(&fw->dispatcher.cond, &fw->dispatcher.mutex, &logAbsTime); + if (waitStatus == ETIMEDOUT) { + fw_log(fw->logger, CELIX_LOG_LEVEL_WARNING, "Generic event with id %li not finished.", eventId); + logAbsTime = celixThreadCondition_getDelayedTime(5); } } celixThreadMutex_unlock(&fw->dispatcher.mutex); @@ -2521,3 +2785,4 @@ void celix_framework_waitForStop(celix_framework_t *framework) { celixThreadMutex_unlock(&framework->shutdown.mutex); } + diff --git a/libs/framework/src/framework_private.h b/libs/framework/src/framework_private.h index d2a36cfc0..60930dde2 100644 --- a/libs/framework/src/framework_private.h +++ b/libs/framework/src/framework_private.h @@ -20,9 +20,10 @@ #ifndef FRAMEWORK_PRIVATE_H_ #define FRAMEWORK_PRIVATE_H_ +#include + #include "celix_framework.h" #include "framework.h" - #include "manifest.h" #include "wire.h" #include "hash_map.h" @@ -37,15 +38,20 @@ #include "bundle_context.h" #include "celix_bundle_cache.h" #include "celix_log.h" - #include "celix_threads.h" #include "service_registry.h" #include +#ifdef __cplusplus +extern "C" { +#endif + #ifndef CELIX_FRAMEWORK_DEFAULT_STATIC_EVENT_QUEUE_SIZE #define CELIX_FRAMEWORK_DEFAULT_STATIC_EVENT_QUEUE_SIZE 1024 #endif +#define CELIX_FRAMEWORK_DEFAULT_MAX_TIMEDWAIT_EVENT_HANDLER_IN_SECONDS 1 + #define CELIX_FRAMEWORK_CLEAN_CACHE_DIR_ON_CREATE_DEFAULT false #define CELIX_FRAMEWORK_CACHE_USE_TMP_DIR_DEFAULT false #define CELIX_FRAMEWORK_FRAMEWORK_CACHE_DIR_DEFAULT ".cache" @@ -161,10 +167,15 @@ struct celix_framework { struct { + long nextEventId; //atomic + long nextScheduledEventId; //atomic + celix_thread_cond_t cond; celix_thread_t thread; celix_thread_mutex_t mutex; //protects below bool active; + + //normal event queue celix_framework_event_t* eventQueue; //ring buffer int eventQueueCap; int eventQueueSize; @@ -177,12 +188,11 @@ struct celix_framework { int nbUnregister; // number of pending async de-registration int nbEvent; // number of pending generic events } stats; + celix_long_hash_map_t *scheduledEvents; //key = scheduled event id, entry = celix_framework_scheduled_event_t*. Used for scheduled events } dispatcher; celix_framework_logger_t* logger; - long nextGenericEventId; - struct { celix_thread_cond_t cond; celix_thread_mutex_t mutex; //protects below @@ -437,4 +447,92 @@ celix_status_t celix_framework_uninstallBundleEntry(celix_framework_t* fw, celix */ celix_status_t celix_framework_updateBundleEntry(celix_framework_t* fw, celix_framework_bundle_entry_t* bndEntry, const char* updatedBundleUrl); + +/** @brief Return the next scheduled event id. + * @param[in] fw The Celix framework + * @return The next scheduled event id. + */ +long celix_framework_nextScheduledEventId(framework_t *fw); + +/** + * @brief Add a scheduled event to the Celix framework. + * + * + * @param[in] fw The Celix framework + * @param[in] bndId The bundle id to add the scheduled event for. + * @param[in] eventName The event name to use for the scheduled event. If NULL, a default event name is used. + * @param[in] initialDelayInSeconds The initial delay in seconds before the first event callback is called. + * @param[in] intervalInSeconds The interval in seconds between event callbacks. + * @param[in] callbackData The event data to pass to the event callback. + * @param[in] callback The event callback to call when the scheduled event is triggered. + * @param[in] removeCallbackData The removed callback data. + * @param[in] removeCallback The removed callback. + * @return The scheduled event id of the scheduled event. Can be used to cancel the event. + * @retval <0 If the event could not be added. + */ +long celix_framework_scheduleEvent(celix_framework_t* fw, + long bndId, + const char* eventName, + double initialDelayInSeconds, + double intervalInSeconds, + void* callbackData, + void (*callback)(void*), + void* removeCallbackData, + void (*removeCallback)(void*)); + +/** + * @brief Wakeup a scheduled event and returns immediately, not waiting for the scheduled event callback to be + * called. + * + * Silently ignored if the scheduled event ids < 0. + * + * @param[in] fw The Celix framework + * @param[in] scheduledEventId The scheduled event id to wakeup. + * @return CELIX_SUCCESS if the scheduled event is woken up, CELIX_ILLEGAL_ARGUMENT if the scheduled event id is not known. + */ +celix_status_t celix_framework_wakeupScheduledEvent(celix_framework_t* fw, long scheduledEventId); + +/** + * @brief Wait for the next scheduled event to be processed. + * + * Silently ignored if the scheduled event ids < 0. + * + * @param[in] fw The Celix framework + * @param[in] scheduledEventId The scheduled event id to wait for. + * @param[in] waitTimeInSeconds The maximum time to wait for the next scheduled event. If <= 0 the function will return + * immediately. + * @return CELIX_SUCCESS if the scheduled event is done with processing, CELIX_ILLEGAL_ARGUMENT if the scheduled event id is not + * known and ETIMEDOUT if the waitTimeInSeconds is reached. + */ +celix_status_t celix_framework_waitForScheduledEvent(celix_framework_t* fw, + long scheduledEventId, + double waitTimeInSeconds); + +/** + * @brief Cancel a scheduled event. + * + * When this function returns, no more scheduled event callbacks will be called. + * + * Silently ignored if the scheduled event ids < 0. + * + * @param[in] fw The Celix framework + * @param[in] async If true, the scheduled event will be cancelled asynchronously and the function will not block. + * @param[in] errorIfNotFound If true, removal of a non existing scheduled event id will be logged. + * @param[in] scheduledEventId The scheduled event id to cancel. + * @return true if a scheduled event is cancelled, false if the scheduled event id is not known. + */ +bool celix_framework_removeScheduledEvent(celix_framework_t* fw, bool async, bool errorIfNotFound, long scheduledEventId); + +/** + * Remove all scheduled events for the provided bundle id and logs warning if there are still un-removed scheduled + * events that are not a one time event. + * @param[in] fw The Celix framework. + * @param[in] bndId The bundle id to remove the scheduled events for. + */ +void celix_framework_cleanupScheduledEvents(celix_framework_t* fw, long bndId); + +#ifdef __cplusplus +} +#endif + #endif /* FRAMEWORK_PRIVATE_H_ */ diff --git a/libs/framework/src/service_tracker.c b/libs/framework/src/service_tracker.c index 7d342dc37..750f336bb 100644 --- a/libs/framework/src/service_tracker.c +++ b/libs/framework/src/service_tracker.c @@ -697,6 +697,29 @@ void celix_serviceTracker_destroy(celix_service_tracker_t *tracker) { } } +static celix_tracked_entry_t* celix_serviceTracker_findHighestRankingService(service_tracker_t *tracker, const char* serviceName) { + //precondition tracker->mutex locked + celix_tracked_entry_t* highest = NULL; + for (int i = 0; i < celix_arrayList_size(tracker->trackedServices); ++i) { + celix_tracked_entry_t* tracked = (celix_tracked_entry_t *) arrayList_get(tracker->trackedServices, i); + if (serviceName == NULL || (serviceName != NULL && tracked->serviceName != NULL && + celix_utils_stringEquals(tracked->serviceName, serviceName))) { + if (highest == NULL) { + highest = tracked; + } else { + int compare = celix_utils_compareServiceIdsAndRanking( + tracked->serviceId, tracked->serviceRanking, + highest->serviceId, highest->serviceRanking + ); + if (compare < 0) { + highest = tracked; + } + } + } + } + return highest; +} + bool celix_serviceTracker_useHighestRankingService(service_tracker_t *tracker, const char *serviceName /*sanity*/, double waitTimeoutInSeconds /*0 -> do not wait */, @@ -704,59 +727,27 @@ bool celix_serviceTracker_useHighestRankingService(service_tracker_t *tracker, void (*use)(void *handle, void *svc), void (*useWithProperties)(void *handle, void *svc, const celix_properties_t *props), void (*useWithOwner)(void *handle, void *svc, const celix_properties_t *props, const celix_bundle_t *owner)) { - bool called = false; - celix_tracked_entry_t *tracked = NULL; - celix_tracked_entry_t *highest = NULL; - unsigned int i; - struct timespec begin = celix_gettime(CLOCK_MONOTONIC); - double remaining = waitTimeoutInSeconds > INT_MAX ? INT_MAX : waitTimeoutInSeconds; - remaining = remaining < 0 ? 0 : remaining; - double elapsed = 0; - long seconds = remaining; - long nanoseconds = (remaining - seconds) * CELIX_NS_IN_SEC; - - //first lock tracker and get highest tracked entry + //first lock tracker and get highest ranking tracked entry celixThreadMutex_lock(&tracker->mutex); - while (highest == NULL) { - unsigned int size = arrayList_size(tracker->trackedServices); - - for (i = 0; i < size; i++) { - tracked = (celix_tracked_entry_t *) arrayList_get(tracker->trackedServices, i); - if (serviceName == NULL || (serviceName != NULL && tracked->serviceName != NULL && - celix_utils_stringEquals(tracked->serviceName, serviceName))) { - if (highest == NULL) { - highest = tracked; - } else { - int compare = celix_utils_compareServiceIdsAndRanking( - tracked->serviceId, tracked->serviceRanking, - highest->serviceId, highest->serviceRanking - ); - if (compare < 0) { - highest = tracked; - } - } - } - } - if(highest == NULL && (seconds > 0 || nanoseconds > 0)) { - celixThreadCondition_timedwaitRelative(&tracker->condTracked, &tracker->mutex, seconds, nanoseconds); - elapsed = celix_elapsedtime(CLOCK_MONOTONIC, begin); - remaining = remaining > elapsed ? (remaining - elapsed) : 0; - seconds = remaining; - nanoseconds = (remaining - seconds) * CELIX_NS_IN_SEC; - } else { - // highest found or timeout + struct timespec absTime = celixThreadCondition_getDelayedTime(waitTimeoutInSeconds); + celix_tracked_entry_t* highest = celix_serviceTracker_findHighestRankingService(tracker, serviceName); + while (highest == NULL && waitTimeoutInSeconds > 0) { + celix_status_t waitStatus = celixThreadCondition_waitUntil(&tracker->condTracked, &tracker->mutex, &absTime); + if (waitStatus == ETIMEDOUT) { break; } + highest = celix_serviceTracker_findHighestRankingService(tracker, serviceName); } - if (highest != NULL) { - //highest found lock tracked entry and increase use count + if (highest) { + // highest found, increase use count tracked_retain(highest); } - //unlock tracker so that the tracked entry can be removed from the trackedServices list if unregistered. + // unlock tracker so that the tracked entry can be removed from the trackedServices list if unregistered. celixThreadMutex_unlock(&tracker->mutex); - if (highest != NULL) { - //got service, call, decrease use count an signal useCond after. + bool called = false; + if (highest) { + //got service, call, decrease use count a signal useCond after. if (use != NULL) { use(callbackHandle, highest->service); } diff --git a/libs/utils/CMakeLists.txt b/libs/utils/CMakeLists.txt index eca648115..e9362dc04 100644 --- a/libs/utils/CMakeLists.txt +++ b/libs/utils/CMakeLists.txt @@ -40,6 +40,7 @@ set(UTILS_SRC src/utils.c src/ip_utils.c src/filter.c + src/celix_log_level.c src/celix_log_utils.c src/celix_hash_map.c src/celix_file_utils.c @@ -51,7 +52,7 @@ set(UTILS_SRC set(UTILS_PRIVATE_DEPS libzip::libzip) set(UTILS_PUBLIC_DEPS) -add_library(utils SHARED ${UTILS_SRC}) +add_library(utils SHARED ${UTILS_SRC} src/celix_log_level.c) target_link_libraries(utils PRIVATE libzip::libzip) set_target_properties(utils diff --git a/libs/utils/gtest/CMakeLists.txt b/libs/utils/gtest/CMakeLists.txt index 06af219dc..d6811a96f 100644 --- a/libs/utils/gtest/CMakeLists.txt +++ b/libs/utils/gtest/CMakeLists.txt @@ -24,6 +24,7 @@ set(CELIX_UTIL_TEST_SOURCES_FOR_CXX_HEADERS ) add_executable(test_utils + src/LogTestSuite.cc src/LogUtilsTestSuite.cc src/VersionRangeTestSuite.cc src/TimeUtilsTestSuite.cc diff --git a/libs/utils/gtest/src/LogTestSuite.cc b/libs/utils/gtest/src/LogTestSuite.cc new file mode 100644 index 000000000..14d57651f --- /dev/null +++ b/libs/utils/gtest/src/LogTestSuite.cc @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include + +#include "celix_log_level.h" + +class LogTestSuite : public ::testing::Test {}; + +TEST_F(LogTestSuite, LogLevelToString) { + EXPECT_STREQ("trace", celix_logLevel_toString(CELIX_LOG_LEVEL_TRACE)); + EXPECT_STREQ("debug", celix_logLevel_toString(CELIX_LOG_LEVEL_DEBUG)); + EXPECT_STREQ("info", celix_logLevel_toString(CELIX_LOG_LEVEL_INFO)); + EXPECT_STREQ("warning", celix_logLevel_toString(CELIX_LOG_LEVEL_WARNING)); + EXPECT_STREQ("error", celix_logLevel_toString(CELIX_LOG_LEVEL_ERROR)); + EXPECT_STREQ("fatal", celix_logLevel_toString(CELIX_LOG_LEVEL_FATAL)); + EXPECT_STREQ("disabled", celix_logLevel_toString(CELIX_LOG_LEVEL_DISABLED)); +} + +TEST_F(LogTestSuite, LogLevelFromString) { + EXPECT_EQ(CELIX_LOG_LEVEL_TRACE, celix_logLevel_fromString("trace", CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_DEBUG, celix_logLevel_fromString("debug", CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_INFO, celix_logLevel_fromString("info", CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_WARNING, celix_logLevel_fromString("warning", CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_ERROR, celix_logLevel_fromString("error", CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_FATAL, celix_logLevel_fromString("fatal", CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logLevel_fromString("disabled", CELIX_LOG_LEVEL_DISABLED)); + + + EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logLevel_fromString(nullptr, CELIX_LOG_LEVEL_DISABLED)); + EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logLevel_fromString("garbage", CELIX_LOG_LEVEL_DISABLED)); + + bool converted; + EXPECT_EQ(CELIX_LOG_LEVEL_FATAL, celix_logLevel_fromStringWithCheck("fatal", CELIX_LOG_LEVEL_DISABLED, &converted)); + EXPECT_TRUE(converted); + EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logLevel_fromStringWithCheck(nullptr, CELIX_LOG_LEVEL_DISABLED, &converted)); + EXPECT_FALSE(converted); + EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logLevel_fromStringWithCheck("garbage", CELIX_LOG_LEVEL_DISABLED, &converted)); + EXPECT_FALSE(converted); +} diff --git a/libs/utils/gtest/src/LogUtilsTestSuite.cc b/libs/utils/gtest/src/LogUtilsTestSuite.cc index f0062b042..067d7ace6 100644 --- a/libs/utils/gtest/src/LogUtilsTestSuite.cc +++ b/libs/utils/gtest/src/LogUtilsTestSuite.cc @@ -25,34 +25,12 @@ class LogUtilsTestSuite : public ::testing::Test {}; TEST_F(LogUtilsTestSuite, LogLevelToString) { EXPECT_STREQ("trace", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_TRACE)); - EXPECT_STREQ("debug", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_DEBUG)); - EXPECT_STREQ("info", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_INFO)); - EXPECT_STREQ("warning", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_WARNING)); - EXPECT_STREQ("error", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_ERROR)); - EXPECT_STREQ("fatal", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_FATAL)); - EXPECT_STREQ("disabled", celix_logUtils_logLevelToString(CELIX_LOG_LEVEL_DISABLED)); + //note rest is tested in LogTestSuite } TEST_F(LogUtilsTestSuite, LogLevelFromString) { EXPECT_EQ(CELIX_LOG_LEVEL_TRACE, celix_logUtils_logLevelFromString("trace", CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_DEBUG, celix_logUtils_logLevelFromString("debug", CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_INFO, celix_logUtils_logLevelFromString("info", CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_WARNING, celix_logUtils_logLevelFromString("warning", CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_ERROR, celix_logUtils_logLevelFromString("error", CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_FATAL, celix_logUtils_logLevelFromString("fatal", CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logUtils_logLevelFromString("disabled", CELIX_LOG_LEVEL_DISABLED)); - - - EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logUtils_logLevelFromString(nullptr, CELIX_LOG_LEVEL_DISABLED)); - EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logUtils_logLevelFromString("garbage", CELIX_LOG_LEVEL_DISABLED)); - - bool converted; - EXPECT_EQ(CELIX_LOG_LEVEL_FATAL, celix_logUtils_logLevelFromStringWithCheck("fatal", CELIX_LOG_LEVEL_DISABLED, &converted)); - EXPECT_TRUE(converted); - EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logUtils_logLevelFromStringWithCheck(nullptr, CELIX_LOG_LEVEL_DISABLED, &converted)); - EXPECT_FALSE(converted); - EXPECT_EQ(CELIX_LOG_LEVEL_DISABLED, celix_logUtils_logLevelFromStringWithCheck("garbage", CELIX_LOG_LEVEL_DISABLED, &converted)); - EXPECT_FALSE(converted); + //note rest is tested in LogTestSuite } TEST_F(LogUtilsTestSuite, LogToStdout) { @@ -71,4 +49,4 @@ TEST_F(LogUtilsTestSuite, LogToStdout) { EXPECT_TRUE(strstr(stdOutput.c_str(), "ignore") == nullptr); EXPECT_TRUE(strstr(errOutput.c_str(), "testing_stderr 1 2 3") != nullptr); EXPECT_TRUE(strstr(stdOutput.c_str(), "ignore") == nullptr); -} \ No newline at end of file +} diff --git a/libs/utils/gtest/src/ThreadsTestSuite.cc b/libs/utils/gtest/src/ThreadsTestSuite.cc index 999415a5a..b58d5ded5 100644 --- a/libs/utils/gtest/src/ThreadsTestSuite.cc +++ b/libs/utils/gtest/src/ThreadsTestSuite.cc @@ -21,10 +21,16 @@ #include +#include "celix_utils.h" #include "celix_threads.h" class ThreadsTestSuite : public ::testing::Test { public: +#ifdef __APPLE__ + const double ALLOWED_ERROR_MARGIN = 0.2; +#else + const double ALLOWED_ERROR_MARGIN= 0.1; +#endif }; //----------------------TEST THREAD FUNCTION DECLARATIONS---------------------- @@ -105,7 +111,7 @@ TEST_F(ThreadsTestSuite, InitializedTest) { celixThread_join(thread, nullptr); } -TEST_F(ThreadsTestSuite, CnceTest) { +TEST_F(ThreadsTestSuite, OnceTest) { int *status; celix_thread_t thread; celix_thread_t thread2; @@ -278,6 +284,38 @@ TEST_F(ThreadsTestSuite, CondBroadcastTest) { free(param); } +TEST_F(ThreadsTestSuite, CondTimedWaitTest) { + celix_thread_mutex_t mutex; + celix_thread_cond_t cond; + + auto status = celixThreadMutex_create(&mutex, nullptr); + ASSERT_EQ(status, CELIX_SUCCESS); + status = celixThreadCondition_init(&cond, nullptr); + ASSERT_EQ(status, CELIX_SUCCESS); + + //Test with nullptr abstime + status = celixThreadCondition_waitUntil(&cond, &mutex, nullptr); + ASSERT_EQ(status, CELIX_ILLEGAL_ARGUMENT); + + //Test with valid abstime + auto start = celixThreadCondition_getTime(); + auto targetEnd = celixThreadCondition_getDelayedTime(0.01); + celixThreadMutex_lock(&mutex); + status = celixThreadCondition_waitUntil(&cond, &mutex, &targetEnd); + ASSERT_EQ(status, ETIMEDOUT); + celixThreadMutex_unlock(&mutex); + auto end = celixThreadCondition_getTime(); + EXPECT_NEAR(celix_difftime(&start, &end), 0.01, ALLOWED_ERROR_MARGIN); + + start = celixThreadCondition_getTime(); + celixThreadMutex_lock(&mutex); + status = celixThreadCondition_waitUntil(&cond, &mutex, &start); + ASSERT_EQ(status, ETIMEDOUT); + celixThreadMutex_unlock(&mutex); + end = celixThreadCondition_getTime(); + EXPECT_NEAR(celix_difftime(&start, &end), 0.0, ALLOWED_ERROR_MARGIN); +} + //----------------------CELIX READ-WRITE LOCK TESTS---------------------- TEST_F(ThreadsTestSuite, CreateRwLockTest) { diff --git a/libs/utils/gtest/src/TimeUtilsTestSuite.cc b/libs/utils/gtest/src/TimeUtilsTestSuite.cc index ccbca6921..d175d7d62 100644 --- a/libs/utils/gtest/src/TimeUtilsTestSuite.cc +++ b/libs/utils/gtest/src/TimeUtilsTestSuite.cc @@ -54,4 +54,83 @@ TEST_F(TimeUtilsTestSuite, ElapsedTimeTest) { auto diff = celix_elapsedtime(CLOCK_MONOTONIC, t1); EXPECT_GE(diff, 0.00001 /*10 us*/); EXPECT_LT(diff, 0.1 /*1/10 s, note do want to rely on accuracy of sleep_for*/); -} \ No newline at end of file +} + +TEST_F(TimeUtilsTestSuite, DelayedTimespecTest) { + //Test with NULL time and delayInSeconds = 0 + struct timespec delayedTime = celix_delayedTimespec(NULL, 0); + ASSERT_EQ(delayedTime.tv_sec, 0); + ASSERT_EQ(delayedTime.tv_nsec, 0); + + //Test with NULL time and delayInSeconds = 1.5 + delayedTime = celix_delayedTimespec(NULL, 1.5); + struct timespec expectedTime = {1, 500000000}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); + + //Test with time.tv_nsec + nanoseconds > CELIX_NS_IN_SEC + struct timespec time = {0, 500000000}; + delayedTime = celix_delayedTimespec(&time, 0.6); + expectedTime = {1, 100000000}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); + + //Test with time.tv_nsec + nanoseconds < 0 + time = {1, 500000000}; + delayedTime = celix_delayedTimespec(&time, -0.6); + expectedTime = {0, 900000000}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); + + //Test with time.tv_nsec + nanoseconds = CELIX_NS_IN_SEC + time = {1, 500000000}; + delayedTime = celix_delayedTimespec(&time, 0.5); + expectedTime = {2, 0}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); + + //Test with time.tv_nsec + nanoseconds < CELIX_NS_IN_SEC + time = {1, 500000000}; + delayedTime = celix_delayedTimespec(&time, 0.4); + expectedTime = {1, 900000000}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); + + //Test if time becomes negative + time = {0, 500000000}; + delayedTime = celix_delayedTimespec(&time, -1.5); + expectedTime = {-1, 0}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); + + //Test delay <= -2 seconds + time = {3, 0}; + delayedTime = celix_delayedTimespec(&time, -2.6); + expectedTime = {0, 400000000}; + ASSERT_EQ(delayedTime.tv_sec, expectedTime.tv_sec); + ASSERT_EQ(delayedTime.tv_nsec, expectedTime.tv_nsec); +} + +TEST_F(TimeUtilsTestSuite, CompareTimeTest) { + struct timespec time1 = {0, 500000000}; + struct timespec time2 = {0, 500000000}; + ASSERT_EQ(celix_compareTime(&time1, &time2), 0); + + time1 = {0, 500000000}; + time2 = {0, 600000000}; + ASSERT_EQ(celix_compareTime(&time1, &time2), -1); //time1 is before time2 + + time1 = {0, 600000000}; + time2 = {0, 500000000}; + ASSERT_EQ(celix_compareTime(&time1, &time2), 1); //time1 is after time2 + + time1 = {1, 500000000}; + time2 = {0, 500000000}; + ASSERT_EQ(celix_compareTime(&time1, &time2), 1); //time1 is after time2 + + time1 = {0, 500000000}; + time2 = {1, 500000000}; + ASSERT_EQ(celix_compareTime(&time1, &time2), -1); //time1 is before time2 +} + + diff --git a/libs/utils/include/celix_errno.h b/libs/utils/include/celix_errno.h index 9d8cdf2e3..e942afb05 100644 --- a/libs/utils/include/celix_errno.h +++ b/libs/utils/include/celix_errno.h @@ -195,6 +195,7 @@ CELIX_UTILS_EXPORT bool celix_utils_isCustomerStatusCode(celix_status_t code); #define CELIX_FRAMEWORK_EXCEPTION CELIX_ERROR_MAKE(CELIX_FACILITY_FRAMEWORK, 4471) #define CELIX_FILE_IO_EXCEPTION CELIX_ERROR_MAKE(CELIX_FACILITY_FRAMEWORK, 4472) #define CELIX_SERVICE_EXCEPTION CELIX_ERROR_MAKE(CELIX_FACILITY_FRAMEWORK, 4473) + /*! * Exception indicating a problem with a interceptor */ diff --git a/libs/utils/include/celix_log_level.h b/libs/utils/include/celix_log_level.h index 825542985..67974e04e 100644 --- a/libs/utils/include/celix_log_level.h +++ b/libs/utils/include/celix_log_level.h @@ -20,22 +20,52 @@ #ifndef CELIX_LOG_LEVEL_H #define CELIX_LOG_LEVEL_H +#include + +#include "celix_utils_export.h" + #ifdef __cplusplus extern "C" { #endif typedef enum celix_log_level { - CELIX_LOG_LEVEL_TRACE = 0, - CELIX_LOG_LEVEL_DEBUG = 1, - CELIX_LOG_LEVEL_INFO = 2, - CELIX_LOG_LEVEL_WARNING = 3, - CELIX_LOG_LEVEL_ERROR = 4, - CELIX_LOG_LEVEL_FATAL = 5, - CELIX_LOG_LEVEL_DISABLED = 6 + CELIX_LOG_LEVEL_TRACE = 0, + CELIX_LOG_LEVEL_DEBUG = 1, + CELIX_LOG_LEVEL_INFO = 2, + CELIX_LOG_LEVEL_WARNING = 3, + CELIX_LOG_LEVEL_ERROR = 4, + CELIX_LOG_LEVEL_FATAL = 5, + CELIX_LOG_LEVEL_DISABLED = 6 } celix_log_level_e; +/** + * @brief Converts a log level to a string. + * @param level The log level to convert. + * @return The string representation of the log level. + */ +CELIX_UTILS_EXPORT const char* celix_logLevel_toString(celix_log_level_e level); + +/** + * @brief Converts a string to a log level. + * @param[in] str The string to convert. + * @param[in] fallbackLogLevel The log level to return if the string is not a valid log level. + * @return The log level. + */ +CELIX_UTILS_EXPORT celix_log_level_e celix_logLevel_fromString(const char* str, celix_log_level_e fallbackLogLevel); + +/** + * @brief Converts a string to a log level. + * @param[in] str The string to convert. + * @param[in] fallbackLogLevel The log level to return if the string is not a valid log level. + * @param[out] convertedSuccessfully A pointer to a boolean which will be set to true if the string is a valid log + * level, false otherwise. Can be NULL if the caller is not interested in this information. + * @return The log level. + */ +CELIX_UTILS_EXPORT celix_log_level_e +celix_logLevel_fromStringWithCheck(const char* str, celix_log_level_e fallbackLogLevel, bool* convertedSuccessfully); + #ifdef __cplusplus }; #endif -#endif //CELIX_LOG_LEVEL_H +#endif // CELIX_LOG_LEVEL_H diff --git a/libs/utils/include/celix_log_utils.h b/libs/utils/include/celix_log_utils.h index 92d21e674..3eecdbfc1 100644 --- a/libs/utils/include/celix_log_utils.h +++ b/libs/utils/include/celix_log_utils.h @@ -32,23 +32,23 @@ extern "C" { #endif /** - * Convert a celix log level enum to a const char* value. + * @brief Convert a celix log level enum to a const char* value. + * @deprecated Use celix_logLevel_toString instead. */ -CELIX_UTILS_EXPORT const char* celix_logUtils_logLevelToString(celix_log_level_e level); +CELIX_UTILS_DEPRECATED_EXPORT const char* celix_logUtils_logLevelToString(celix_log_level_e level); /** - * Convert a const char* value to a celix log level - * If the provided log level cannot be parsed or is NULL, the fallbackLogLevel is returned. + * @brief Convert a const char* value to a celix log level. + * @deprecated Use celix_logLevel_fromString instead. */ -CELIX_UTILS_EXPORT celix_log_level_e celix_logUtils_logLevelFromString(const char *level, celix_log_level_e fallbackLogLevel); +CELIX_UTILS_DEPRECATED_EXPORT celix_log_level_e celix_logUtils_logLevelFromString(const char* level, + celix_log_level_e fallbackLogLevel); /** - * Convert a const char* value to a celix log level - * If the provided log level cannot be parsed or is NULL, the fallbackLogLevel is returned and - * if a convertedSuccessfully pointer is provided this will be set to false. - * If converted successfully and the convertedSuccessfully pointer is provided. This will be set to true. + * @brief Convert a const char* value to a celix log level. + * @deprecated Use celix_logLevel_fromStringWithCheck instead. */ -CELIX_UTILS_EXPORT celix_log_level_e celix_logUtils_logLevelFromStringWithCheck( +CELIX_UTILS_DEPRECATED_EXPORT celix_log_level_e celix_logUtils_logLevelFromStringWithCheck( const char *level, celix_log_level_e fallbackLogLevel, bool *convertedSuccessfully); diff --git a/libs/utils/include/celix_threads.h b/libs/utils/include/celix_threads.h index fba1e2466..224edfd11 100644 --- a/libs/utils/include/celix_threads.h +++ b/libs/utils/include/celix_threads.h @@ -122,13 +122,73 @@ CELIX_UTILS_EXPORT celix_status_t celixThreadRwlockAttr_destroy(celix_thread_rwl typedef pthread_cond_t celix_thread_cond_t; typedef pthread_condattr_t celix_thread_condattr_t; +/** + * @brief Initialize the given condition variable. + * + * For Linux the condition clock is set to CLOCK_MONOTONIC whether or not the attr is NULL. + * + * @param[in] condition The condition variable to initialize. + * @param[in] attr The condition variable attributes to use. Can be NULL for default attributes. + * @return CELIX_SUCCESS if the condition variable is initialized successfully. + */ CELIX_UTILS_EXPORT celix_status_t celixThreadCondition_init(celix_thread_cond_t *condition, celix_thread_condattr_t *attr); CELIX_UTILS_EXPORT celix_status_t celixThreadCondition_destroy(celix_thread_cond_t *condition); CELIX_UTILS_EXPORT celix_status_t celixThreadCondition_wait(celix_thread_cond_t *cond, celix_thread_mutex_t *mutex); -CELIX_UTILS_EXPORT celix_status_t celixThreadCondition_timedwaitRelative(celix_thread_cond_t *cond, celix_thread_mutex_t *mutex, long seconds, long nanoseconds); +CELIX_UTILS_DEPRECATED_EXPORT celix_status_t celixThreadCondition_timedwaitRelative(celix_thread_cond_t *cond, celix_thread_mutex_t *mutex, long seconds, long nanoseconds); + +/** + * @brief Get the current time suitable for Celix thread conditions. + * + * This function returns the current time compatible with the Celix thread conditions, specifically for + * the function celixThreadCondition_waitUntil, as long as the condition is initialized with + * celixThreadCondition_init. + * + * Note: Do not use the returned time for logging or displaying the current time as the choice of clock + * varies based on the operating system. + * + * @return A struct timespec denoting the current time. + */ +CELIX_UTILS_EXPORT struct timespec celixThreadCondition_getTime(); + +/** + * @brief Calculate the current time incremented by a given delay, suitable for Celix thread conditions. + * + * This function provides the current time, increased by a specified delay (in seconds), compatible + * with Celix thread conditions. The resulting struct timespec can be used with the function + * celixThreadCondition_waitUntil, as long as the condition is initialized with celixThreadCondition_init. + * + * Note: Do not use the returned time for logging or displaying the current time as the choice of clock + * varies based on the operating system. + * + * @param[in] delayInSeconds The desired delay in seconds to be added to the current time. + * @return A struct timespec denoting the current time plus the provided delay. + */ +CELIX_UTILS_EXPORT struct timespec celixThreadCondition_getDelayedTime(double delayInSeconds); + + +/** + * @brief Wait for the condition to be signaled or until the given absolute time is reached. + * + * @section Errors + * - CELIX_SUCCESS if the condition is signaled before the delayInSeconds is reached. + * - CELIX_ILLEGAL_ARGUMENT if the absTime is null. + * - ETIMEDOUT If the absTime has passed. + * - EINTR Wait was interrupted by a signal. + * + * Values for absTime should be obtained by celixThreadCondition_getTime, celixThreadCondition_getDelayedTime or + * a modified timespec based on celixThreadCondition_getTime/celixThreadCondition_getDelayedTime. + * + * @param[in] cond The condition to wait for. + * @param[in] mutex The (locked) mutex to use. + * @param[in] absTime The absolute time to wait for the condition to be signaled. + * @return CELIX_SUCCESS if the condition is signaled before the delayInSeconds is reached. + */ +CELIX_UTILS_EXPORT celix_status_t celixThreadCondition_waitUntil(celix_thread_cond_t* cond, + celix_thread_mutex_t* mutex, + const struct timespec* absTime); CELIX_UTILS_EXPORT celix_status_t celixThreadCondition_broadcast(celix_thread_cond_t *cond); diff --git a/libs/utils/include/celix_utils.h b/libs/utils/include/celix_utils.h index ff1d3d32f..707637ebd 100644 --- a/libs/utils/include/celix_utils.h +++ b/libs/utils/include/celix_utils.h @@ -158,12 +158,28 @@ CELIX_UTILS_EXPORT double celix_difftime(const struct timespec *tBegin, const st */ CELIX_UTILS_EXPORT struct timespec celix_gettime(clockid_t clockId); +/** + * @brief Returns the absolute time for the provided delay in seconds. + * @param[in] time The time to add the delay to. Can be NULL, in which case the time is 0. + * @param[in] delayInSeconds The delay in seconds. + * @return A new time with the delay added. + */ +CELIX_UTILS_EXPORT struct timespec celix_delayedTimespec(const struct timespec* time, double delayInSeconds); + /** * @brief Returns the elapsed time - in seconds - relative to the startTime * using the clock for the provided clockid. */ CELIX_UTILS_EXPORT double celix_elapsedtime(clockid_t clockId, struct timespec startTime); +/** + * @brief Compare two time arguments. + * @param[in] a The first timespec. + * @param[in] b The second timespec. + * @return 0 if equal, -1 if a is before b and 1 if a is after b. + */ +CELIX_UTILS_EXPORT int celix_compareTime(const struct timespec* a, const struct timespec* b); + /** * @brief Creates a hash from a string */ diff --git a/libs/utils/src/celix_log_level.c b/libs/utils/src/celix_log_level.c new file mode 100644 index 000000000..630c7b6ac --- /dev/null +++ b/libs/utils/src/celix_log_level.c @@ -0,0 +1,97 @@ +/** + *Licensed to the Apache Software Foundation (ASF) under one + *or more contributor license agreements. See the NOTICE file + *distributed with this work for additional information + *regarding copyright ownership. The ASF licenses this file + *to you under the Apache License, Version 2.0 (the + *"License"); you may not use this file except in compliance + *with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + *Unless required by applicable law or agreed to in writing, + *software distributed under the License is distributed on an + *"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + *specific language governing permissions and limitations + *under the License. + */ + +#include "celix_log_utils.h" +#include + +static const char* const CELIX_STRING_VALUE_DISABLED = "disabled"; +static const char* const CELIX_STRING_VALUE_FATAL = "fatal"; +static const char* const CELIX_STRING_VALUE_ERROR = "error"; +static const char* const CELIX_STRING_VALUE_WARNING = "warning"; +static const char* const CELIX_STRING_VALUE_INFO = "info"; +static const char* const CELIX_STRING_VALUE_DEBUG = "debug"; +static const char* const CELIX_STRING_VALUE_TRACE = "trace"; + +const char* celix_logLevel_toString(celix_log_level_e level) { + switch (level) { + case CELIX_LOG_LEVEL_DISABLED: + return CELIX_STRING_VALUE_DISABLED; + case CELIX_LOG_LEVEL_FATAL: + return CELIX_STRING_VALUE_FATAL; + case CELIX_LOG_LEVEL_ERROR: + return CELIX_STRING_VALUE_ERROR; + case CELIX_LOG_LEVEL_WARNING: + return CELIX_STRING_VALUE_WARNING; + case CELIX_LOG_LEVEL_INFO: + return CELIX_STRING_VALUE_INFO; + case CELIX_LOG_LEVEL_DEBUG: + return CELIX_STRING_VALUE_DEBUG; + default: // only trace left + return CELIX_STRING_VALUE_TRACE; + } +} + +celix_log_level_e celix_logLevel_fromString(const char* str, celix_log_level_e fallbackLogLevel) { + return celix_logLevel_fromStringWithCheck(str, fallbackLogLevel, NULL); +} + +celix_log_level_e +celix_logLevel_fromStringWithCheck(const char* str, celix_log_level_e fallbackLogLevel, bool* convertedSuccessfully) { + celix_log_level_e level = fallbackLogLevel; + if (convertedSuccessfully != NULL) { + *convertedSuccessfully = true; + } + if (str != NULL) { + if (strncasecmp(CELIX_STRING_VALUE_DISABLED, str, strlen(CELIX_STRING_VALUE_DISABLED)) == 0) { + level = CELIX_LOG_LEVEL_DISABLED; + } else if (strncasecmp(CELIX_STRING_VALUE_FATAL, str, strlen(CELIX_STRING_VALUE_FATAL)) == 0) { + level = CELIX_LOG_LEVEL_FATAL; + } else if (strncasecmp(CELIX_STRING_VALUE_ERROR, str, strlen(CELIX_STRING_VALUE_ERROR)) == 0) { + level = CELIX_LOG_LEVEL_ERROR; + } else if (strncasecmp(CELIX_STRING_VALUE_WARNING, str, strlen(CELIX_STRING_VALUE_WARNING)) == 0) { + level = CELIX_LOG_LEVEL_WARNING; + } else if (strncasecmp(CELIX_STRING_VALUE_INFO, str, strlen(CELIX_STRING_VALUE_INFO)) == 0) { + level = CELIX_LOG_LEVEL_INFO; + } else if (strncasecmp(CELIX_STRING_VALUE_DEBUG, str, strlen(CELIX_STRING_VALUE_DEBUG)) == 0) { + level = CELIX_LOG_LEVEL_DEBUG; + } else if (strncasecmp(CELIX_STRING_VALUE_TRACE, str, strlen(CELIX_STRING_VALUE_TRACE)) == 0) { + level = CELIX_LOG_LEVEL_TRACE; + } else { + celix_logUtils_logToStdout( + "logUtils", + CELIX_LOG_LEVEL_ERROR, + "Cannot match log level str '%s' to an existing log level. Falling back to log level %s", + str, + celix_logUtils_logLevelToString(fallbackLogLevel)); + if (convertedSuccessfully != NULL) { + *convertedSuccessfully = false; + } + } + } else { + celix_logUtils_logToStdout( + "logUtils", + CELIX_LOG_LEVEL_ERROR, + "Cannot match NULL log level str to an existing log level. Falling back to log level %s", + celix_logUtils_logLevelToString(fallbackLogLevel)); + if (convertedSuccessfully != NULL) { + *convertedSuccessfully = false; + } + } + return level; +} diff --git a/libs/utils/src/celix_log_utils.c b/libs/utils/src/celix_log_utils.c index 01349d61b..62a1be63f 100644 --- a/libs/utils/src/celix_log_utils.c +++ b/libs/utils/src/celix_log_utils.c @@ -30,70 +30,16 @@ #include #include -static const char * const CELIX_STRING_VALUE_DISABLED = "disabled"; -static const char * const CELIX_STRING_VALUE_FATAL = "fatal"; -static const char * const CELIX_STRING_VALUE_ERROR = "error"; -static const char * const CELIX_STRING_VALUE_WARNING = "warning"; -static const char * const CELIX_STRING_VALUE_INFO = "info"; -static const char * const CELIX_STRING_VALUE_DEBUG = "debug"; -static const char * const CELIX_STRING_VALUE_TRACE = "trace"; - -const char* celix_logUtils_logLevelToString(celix_log_level_e level) { - switch(level) { - case CELIX_LOG_LEVEL_DISABLED: - return CELIX_STRING_VALUE_DISABLED; - case CELIX_LOG_LEVEL_FATAL: - return CELIX_STRING_VALUE_FATAL; - case CELIX_LOG_LEVEL_ERROR: - return CELIX_STRING_VALUE_ERROR; - case CELIX_LOG_LEVEL_WARNING: - return CELIX_STRING_VALUE_WARNING; - case CELIX_LOG_LEVEL_INFO: - return CELIX_STRING_VALUE_INFO; - case CELIX_LOG_LEVEL_DEBUG: - return CELIX_STRING_VALUE_DEBUG; - default: //only trace left - return CELIX_STRING_VALUE_TRACE; - } -} +const char* celix_logUtils_logLevelToString(celix_log_level_e level) { return celix_logLevel_toString(level); } -celix_log_level_e celix_logUtils_logLevelFromString(const char *str, celix_log_level_e fallbackLogLevel) { - return celix_logUtils_logLevelFromStringWithCheck(str, fallbackLogLevel, NULL); +celix_log_level_e celix_logUtils_logLevelFromString(const char* str, celix_log_level_e fallbackLogLevel) { + return celix_logLevel_fromString(str, fallbackLogLevel); } -celix_log_level_e celix_logUtils_logLevelFromStringWithCheck(const char *str, celix_log_level_e fallbackLogLevel, bool *convertedSuccessfully) { - celix_log_level_e level = fallbackLogLevel; - if (convertedSuccessfully != NULL) { - *convertedSuccessfully = true; - } - if (str != NULL) { - if (strncasecmp(CELIX_STRING_VALUE_DISABLED, str, strlen(CELIX_STRING_VALUE_DISABLED)) == 0) { - level = CELIX_LOG_LEVEL_DISABLED; - } else if (strncasecmp(CELIX_STRING_VALUE_FATAL, str, strlen(CELIX_STRING_VALUE_FATAL)) == 0) { - level = CELIX_LOG_LEVEL_FATAL; - } else if (strncasecmp(CELIX_STRING_VALUE_ERROR, str, strlen(CELIX_STRING_VALUE_ERROR)) == 0) { - level = CELIX_LOG_LEVEL_ERROR; - } else if (strncasecmp(CELIX_STRING_VALUE_WARNING, str, strlen(CELIX_STRING_VALUE_WARNING)) == 0) { - level = CELIX_LOG_LEVEL_WARNING; - } else if (strncasecmp(CELIX_STRING_VALUE_INFO, str, strlen(CELIX_STRING_VALUE_INFO)) == 0) { - level = CELIX_LOG_LEVEL_INFO; - } else if (strncasecmp(CELIX_STRING_VALUE_DEBUG, str, strlen(CELIX_STRING_VALUE_DEBUG)) == 0) { - level = CELIX_LOG_LEVEL_DEBUG; - } else if (strncasecmp(CELIX_STRING_VALUE_TRACE, str, strlen(CELIX_STRING_VALUE_TRACE)) == 0) { - level = CELIX_LOG_LEVEL_TRACE; - } else { - celix_logUtils_logToStdout("logUtils", CELIX_LOG_LEVEL_ERROR, "Cannot match log level str '%s' to an existing log level. Falling back to log level %s", str, celix_logUtils_logLevelToString(fallbackLogLevel)); - if (convertedSuccessfully != NULL) { - *convertedSuccessfully = false; - } - } - } else { - celix_logUtils_logToStdout("logUtils", CELIX_LOG_LEVEL_ERROR, "Cannot match NULL log level str to an existing log level. Falling back to log level %s", celix_logUtils_logLevelToString(fallbackLogLevel)); - if (convertedSuccessfully != NULL) { - *convertedSuccessfully = false; - } - } - return level; +celix_log_level_e celix_logUtils_logLevelFromStringWithCheck(const char* str, + celix_log_level_e fallbackLogLevel, + bool* convertedSuccessfully) { + return celix_logLevel_fromStringWithCheck(str, fallbackLogLevel, convertedSuccessfully); } #ifndef __UCLIBC__ diff --git a/libs/utils/src/celix_threads.c b/libs/utils/src/celix_threads.c index 088f216ef..9289ac231 100644 --- a/libs/utils/src/celix_threads.c +++ b/libs/utils/src/celix_threads.c @@ -16,18 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -/** - * celix_threads.c - * - * \date 4 Jun 2014 - * \author Apache Celix Project Team - * \copyright Apache License, Version 2.0 - */ #include #include #include -#include "signal.h" +#include + #include "celix_threads.h" #include "celix_utils.h" @@ -150,11 +144,10 @@ celix_status_t celixThreadCondition_init(celix_thread_cond_t *condition, celix_t return pthread_cond_init(condition, attr); #else celix_status_t status = CELIX_SUCCESS; - if(attr) { + if (attr) { status = pthread_condattr_setclock(attr, CLOCK_MONOTONIC); status = CELIX_DO_IF(status, pthread_cond_init(condition, attr)); - } - else { + } else { celix_thread_condattr_t condattr; (void)pthread_condattr_init(&condattr); // always return 0 status = pthread_condattr_setclock(&condattr, CLOCK_MONOTONIC); @@ -182,21 +175,43 @@ celix_status_t celixThreadCondition_timedwaitRelative(celix_thread_cond_t *cond, } #else celix_status_t celixThreadCondition_timedwaitRelative(celix_thread_cond_t *cond, celix_thread_mutex_t *mutex, long seconds, long nanoseconds) { - struct timespec time; - seconds = seconds >= 0 ? seconds : 0; - time = celix_gettime(CLOCK_MONOTONIC); - time.tv_sec += seconds; - if(nanoseconds > 0) { - time.tv_nsec += nanoseconds; - while (time.tv_nsec > CELIX_NS_IN_SEC) { - time.tv_sec++; - time.tv_nsec -= CELIX_NS_IN_SEC; - } - } + double delay = (double)seconds + ((double)nanoseconds / 1000000000); + struct timespec time = celixThreadCondition_getDelayedTime(delay); return pthread_cond_timedwait(cond, mutex, &time); } #endif +struct timespec celixThreadCondition_getTime() { + return celixThreadCondition_getDelayedTime(0); +} + +struct timespec celixThreadCondition_getDelayedTime(double delayInSeconds) { + struct timespec now = celix_gettime(CLOCK_MONOTONIC); + if (delayInSeconds == 0) { + return now; + } + return celix_delayedTimespec(&now, delayInSeconds); +} + +celix_status_t +celixThreadCondition_waitUntil(celix_thread_cond_t* cond, celix_thread_mutex_t* mutex, const struct timespec* absTime) { + if (absTime == NULL) { + return CELIX_ILLEGAL_ARGUMENT; + } +#if __APPLE__ + struct timespec now = celix_gettime(CLOCK_MONOTONIC); + double diff = celix_difftime(&now, absTime); + if (diff <= 0) { + return ETIMEDOUT; + } + long seconds = diff; + long nanoseconds = (diff - seconds) * CELIX_NS_IN_SEC; + return celixThreadCondition_timedwaitRelative(cond, mutex, seconds, nanoseconds); +#else + return pthread_cond_timedwait(cond, mutex, absTime); +#endif +} + celix_status_t celixThreadCondition_broadcast(celix_thread_cond_t *cond) { return pthread_cond_broadcast(cond); } diff --git a/libs/utils/src/utils.c b/libs/utils/src/utils.c index 95ab561f4..f80b3bd1e 100644 --- a/libs/utils/src/utils.c +++ b/libs/utils/src/utils.c @@ -265,11 +265,47 @@ struct timespec celix_gettime(clockid_t clockId) { return t; } +struct timespec celix_delayedTimespec(const struct timespec* time, double delayInSeconds) { + struct timespec delayedTime; + if (time != NULL) { + delayedTime = *time; + } else { + delayedTime.tv_nsec = 0; + delayedTime.tv_sec = 0; + } + + long seconds = (long)delayInSeconds; + double nanoseconds = (delayInSeconds - (double)seconds) * CELIX_NS_IN_SEC; + delayedTime.tv_sec += seconds; + delayedTime.tv_nsec += (long)nanoseconds; + + if (delayedTime.tv_nsec >= CELIX_NS_IN_SEC) { + delayedTime.tv_sec += 1; + delayedTime.tv_nsec -= CELIX_NS_IN_SEC; + } else if (delayedTime.tv_nsec < 0) { + delayedTime.tv_sec -= 1; + delayedTime.tv_nsec += CELIX_NS_IN_SEC; + } + + return delayedTime; +} + double celix_elapsedtime(clockid_t clockId, struct timespec startTime) { struct timespec now = celix_gettime(clockId); return celix_difftime(&startTime, &now); } +int celix_compareTime(const struct timespec *a, const struct timespec *b) { + if (a->tv_sec == b->tv_sec && a->tv_nsec == b->tv_nsec) { + return 0; + } + double diff = celix_difftime(a, b); + if (diff < 0) { + return 1; + } + return -1; +} + char* celix_utils_strdup(const char *str) { if (str != NULL) { return strndup(str, CELIX_UTILS_MAX_STRLEN);