This is an automated email from the ASF dual-hosted git repository.

pnoltes pushed a commit to branch feature/scheduled_event_on_event_thread
in repository https://gitbox.apache.org/repos/asf/celix.git

commit ab6b1aa532da88ecbbf5be1f154e82e87cb9c2cb
Author: Pepijn Noltes <[email protected]>
AuthorDate: Sun May 14 19:51:47 2023 +0200

    Add initial implement of scheduled event for the event thread
---
 libs/framework/CMakeLists.txt                      |   3 +-
 libs/framework/gtest/CMakeLists.txt                |   1 +
 .../framework/gtest/src/CelixFrameworkTestSuite.cc |  20 ++
 .../framework/gtest/src/ScheduledEventTestSuite.cc | 187 +++++++++++++++
 libs/framework/include/celix_bundle_context.h      |  85 ++++++-
 libs/framework/include/celix_framework.h           |  30 ++-
 libs/framework/src/bundle_context.c                |  16 ++
 libs/framework/src/celix_scheduled_event.c         | 144 ++++++++++++
 libs/framework/src/celix_scheduled_event.h         | 128 +++++++++++
 libs/framework/src/framework.c                     | 250 +++++++++++++++++++--
 libs/framework/src/framework_private.h             |  68 +++++-
 libs/utils/include/celix_errno.h                   |   2 +
 libs/utils/src/celix_threads.c                     |   2 +-
 13 files changed, 895 insertions(+), 41 deletions(-)

diff --git a/libs/framework/CMakeLists.txt b/libs/framework/CMakeLists.txt
index 82968846..3cde3860 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 c05fe4f5..b2844a1b 100644
--- a/libs/framework/gtest/CMakeLists.txt
+++ b/libs/framework/gtest/CMakeLists.txt
@@ -56,6 +56,7 @@ set(CELIX_FRAMEWORK_TEST_SOURCES
     src/CxxBundleActivatorTestSuite.cc
     src/BundleArchiveTestSuite.cc
     src/CelixLauncherTestSuite.cc
+    src/ScheduledEventTestSuite.cc
 )
 
 add_executable(test_framework ${CELIX_FRAMEWORK_TEST_SOURCES})
diff --git a/libs/framework/gtest/src/CelixFrameworkTestSuite.cc 
b/libs/framework/gtest/src/CelixFrameworkTestSuite.cc
index 1a3562c1..92c9d0d1 100644
--- a/libs/framework/gtest/src/CelixFrameworkTestSuite.cc
+++ b/libs/framework/gtest/src/CelixFrameworkTestSuite.cc
@@ -79,6 +79,26 @@ 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{20});
+    };
+    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_timedWaitForEmptyEventQueue(framework.get(), 0.005);
+    EXPECT_EQ(CELIX_TIMEOUT, status);
+
+    //And a wait for empty event queue for max 30ms will return success
+    status = celix_framework_timedWaitForEmptyEventQueue(framework.get(), 
0.03);
+    EXPECT_EQ(CELIX_SUCCESS, status);
+}
+
+
 TEST_F(CelixFrameworkTestSuite, AsyncInstallStartStopAndUninstallBundleTest) {
     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 00000000..aab19a1d
--- /dev/null
+++ b/libs/framework/gtest/src/ScheduledEventTestSuite.cc
@@ -0,0 +1,187 @@
+/*
+ * 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 <gtest/gtest.h>
+
+#include "celix/FrameworkFactory.h"
+#include "celix_bundle_context.h"
+#include "celix_scheduled_event.h"
+#include "framework_private.h"
+
+class ScheduledEventTestSuite : public ::testing::Test {
+public:
+    ScheduledEventTestSuite() {
+        fw = 
celix::createFramework({{"CELIX_LOGGING_DEFAULT_ACTIVE_LOG_LEVEL", "info"}});
+    }
+
+    std::shared_ptr<celix::Framework> fw{};
+};
+
+TEST_F(ScheduledEventTestSuite, OnceShotEventTest) {
+    auto ctx = fw->getFrameworkBundleContext();
+
+    struct event_info {
+        std::atomic<int> count{0};
+    };
+    event_info info{};
+
+    auto callback = [](void *data) {
+        auto* info = static_cast<event_info*>(data);
+        info->count++;
+    };
+
+    //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.eventData = &info;
+    opts.eventCallback = callback;
+
+
+    //And I schedule the event
+    long eventId = 
celix_bundleContext_addScheduledEvent(ctx->getCBundleContext(), &opts);
+    EXPECT_GE(eventId, 0);
+
+    //Then the event is called once
+    std::this_thread::sleep_for(std::chrono::milliseconds{10});
+    EXPECT_EQ(1, info.count.load());
+}
+
+TEST_F(ScheduledEventTestSuite, ScheduledEventTest) {
+    auto ctx = fw->getFrameworkBundleContext();
+
+    struct event_info {
+        std::atomic<int> count{0};
+    };
+    event_info info{};
+
+    auto callback = [](void *data) {
+        auto* info = static_cast<event_info*>(data);
+        info->count++;
+    };
+
+    //When I create a scheduled event with a 10ms delay and a 20 ms interval
+    celix_scheduled_event_options_t opts{};
+    opts.eventName = "Scheduled event test";
+    opts.initialDelayInSeconds = 0.01;
+    opts.intervalInSeconds = 0.02;
+    opts.eventData = &info;
+    opts.eventCallback = callback;
+
+    //And I schedule the event
+    long eventId = 
celix_bundleContext_addScheduledEvent(ctx->getCBundleContext(), &opts);
+    EXPECT_GE(eventId, 0);
+
+    //And wait more than 10 ms + 2x 20ms + 10ms error margin
+    std::this_thread::sleep_for(std::chrono::milliseconds{60});
+
+    //Then the event is called at least 3 times
+    EXPECT_GE(info.count.load(),3);
+
+    //And when I remove the event
+    celix_bundleContext_removeScheduledEvent(ctx->getCBundleContext(), 
eventId);
+}
+
+TEST_F(ScheduledEventTestSuite, ManyScheduledEventTest) {
+    auto ctx = fw->getFrameworkBundleContext();
+
+    struct event_info {
+        std::atomic<int> count{0};
+    };
+    event_info info{};
+    auto callback = [](void *data) {
+        auto* info = static_cast<event_info*>(data);
+        info->count++;
+    };
+
+    std::vector<long> eventIds{};
+
+    //When 1000 scheduled events are with a random interval between 1 and 59 ms
+    for (int i = 0; i < 1000; ++i) {
+        //When I create a scheduled event with a 10ms delay and a 20 ms 
interval
+        celix_scheduled_event_options_t opts{};
+        opts.eventName = "Scheduled event test";
+        opts.intervalInSeconds = (i % 50) / 100.0; //note will also contain 
one-shot scheduled events
+        opts.eventData = &info;
+        opts.eventCallback = callback;
+        long eventId = 
celix_bundleContext_addScheduledEvent(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.eventName = "Unremoved scheduled event test";
+    opts.intervalInSeconds = 0.02;
+    opts.eventCallback = callback;
+    long scheduledEventId = 
celix_bundleContext_addScheduledEvent(ctx->getCBundleContext(), &opts);
+    EXPECT_GE(scheduledEventId, 0);
+
+    //And I do not remove the event, but let the bundle framework stpp
+    //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.eventName = "Unremoved one-shot scheduled event test";
+    opts.initialDelayInSeconds = 100;
+    opts.eventCallback = callback;
+    long scheduledEventId = 
celix_bundleContext_addScheduledEvent(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_addScheduledEvent(ctx->getCBundleContext(), &opts);
+
+    //Then I expect an error
+    EXPECT_LT(scheduledEventId, 0);
+
+    //celix_scheduleEvent_destroy and celix_scheduledEvent_waitAndDestroy can 
be called with NULL
+    celix_scheduledEvent_destroy(nullptr);
+    celix_scheduledEvent_waitAndDestroy(nullptr);
+}
diff --git a/libs/framework/include/celix_bundle_context.h 
b/libs/framework/include/celix_bundle_context.h
index 8b1ef2bc..c0e15bf0 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, \
@@ -1074,10 +1074,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
 
@@ -1225,7 +1225,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.
@@ -1254,6 +1254,69 @@ 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* eventName CELIX_OPTS_INIT; /**<
+                                            * @brief The name of the event, 
used for logging and debugging.
+                                            *
+                                            * Expected to be const char* that 
is valid during the addScheduledEvent
+                                            * 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* eventData CELIX_OPTS_INIT; /**< @brief Data passed to the 
eventCallback function when a event is scheduled.*/
+
+    void (*eventCallback)(void* eventData) CELIX_OPTS_INIT; /**< @brief 
Callback function called to process a scheduled event.*/
+} celix_scheduled_event_options_t;
+
+/**
+ * @brief Add a scheduled event to the Celix framework.
+ *
+ * The scheduled event wil be called repeatedly on the Celix framework event 
thread using the provided interval.
+ * For the interval time a monotonic clock is used. The event callback should 
be fast and non-blocking, 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 short 
scheduled event and will be called once
+ * after the provided initial delay. If a bundle stops before the one short 
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.
+ * Exception are the one shot scheduled events, these 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, which is not a one short 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_addScheduledEvent(celix_bundle_context_t* ctx,
+                                                                  const 
celix_scheduled_event_options_t* options);
+
+/**
+ * @brief Cancel and remove a scheduled event.
+ *
+ * When this function returns, no more scheduled event callbacks will be 
called.
+ *
+ * @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_removeScheduledEvent(celix_bundle_context_t* ctx,
+                                                                      long 
scheduledEventId);
 
 /**
  * @brief Returns the bundle for this bundle context.
diff --git a/libs/framework/include/celix_framework.h 
b/libs/framework/include/celix_framework.h
index e5beb0d2..9c7c2e18 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.
@@ -261,26 +260,36 @@ 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.
+ * This function can be used to ensure that all queue event are handled.
  *
  * @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 bundle 
events.
+ * This function can be used to ensure that all queue event are handled.
+ *
+ * @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 CELIX_TIMEOUT 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_timedWaitForEmptyEventQueue(celix_framework_t *fw, double 
timeoutInSeconds);
 
 /**
  * @brief wait until all events for the bundle identified by the bndId are 
processed.
@@ -301,6 +310,9 @@ CELIX_FRAMEWORK_EXPORT bool 
celix_framework_isCurrentThreadTheEventLoop(celix_fr
 /**
  * @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.
  *
diff --git a/libs/framework/src/bundle_context.c 
b/libs/framework/src/bundle_context.c
index d5bb0540..f60ea47b 100644
--- a/libs/framework/src/bundle_context.c
+++ b/libs/framework/src/bundle_context.c
@@ -112,6 +112,7 @@ celix_status_t bundleContext_destroy(bundle_context_pt 
context) {
 }
 
 void celix_bundleContext_cleanup(celix_bundle_context_t *ctx) {
+    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);
@@ -1482,6 +1483,21 @@ void 
celix_bundleContext_waitForEvents(celix_bundle_context_t* ctx) {
     celix_framework_waitUntilNoEventsForBnd(ctx->framework, 
celix_bundle_getId(ctx->bundle));
 }
 
+long celix_bundleContext_addScheduledEvent(celix_bundle_context_t* ctx,
+                                           const 
celix_scheduled_event_options_t* options) {
+    return celix_framework_addScheduledEvent(ctx->framework,
+                                             celix_bundle_getId(ctx->bundle),
+                                             options->eventName,
+                                             options->initialDelayInSeconds,
+                                             options->intervalInSeconds,
+                                             options->eventData,
+                                             options->eventCallback);
+}
+
+bool celix_bundleContext_removeScheduledEvent(celix_bundle_context_t* ctx, 
long scheduledEventId) {
+    return celix_framework_removeScheduledEvent(ctx->framework, 
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 00000000..66c02920
--- /dev/null
+++ b/libs/framework/src/celix_scheduled_event.c
@@ -0,0 +1,144 @@
+/*
+ * 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 <assert.h>
+
+#include "celix_scheduled_event.h"
+#include "celix_utils.h"
+
+celix_scheduled_event_t* celix_scheduledEvent_create(celix_framework_logger_t* 
logger,
+                                                     
celix_framework_bundle_entry_t* bndEntry,
+                                                     long scheduledEventId,
+                                                     const char* 
providedEventName,
+                                                     double 
initialDelayInSeconds,
+                                                     double intervalInSeconds,
+                                                     void* eventData,
+                                                     void 
(*eventCallback)(void* eventData)) {
+    if (eventCallback == NULL) {
+        fw_log(logger, CELIX_LOG_LEVEL_ERROR, "Cannot add scheduled event for 
bundle id %li. No event callback provided", bndEntry->bndId);
+        return  NULL;
+    }
+
+    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(logger, CELIX_LOG_LEVEL_ERROR, "Cannot add scheduled event for 
bundle id %li. Out of memory", bndEntry->bndId);
+        free(event);
+        if (eventName != CELIX_SCHEDULED_EVENT_DEFAULT_NAME) {
+            free(eventName);
+        }
+        celix_framework_bundleEntry_decreaseUseCount(bndEntry);
+        return NULL;
+    }
+
+    event->scheduledEventId = scheduledEventId;
+    event->logger = logger;
+    event->bndEntry = bndEntry;
+
+    event->eventName = eventName;
+    event->initialDelayInSeconds = initialDelayInSeconds;
+    event->intervalInSeconds = intervalInSeconds;
+    event->eventCallbackData = eventData;
+    event->eventCallback = eventCallback;
+    event->useCount = 0;
+    event->callCount = 0;
+    clock_gettime(CLOCK_MONOTONIC, &event->lastScheduledEventTime);
+    celixThreadMutex_create(&event->mutex, NULL);
+    celixThreadCondition_init(&event->cond, NULL);
+
+    return event;
+}
+
+void celix_scheduledEvent_destroy(celix_scheduled_event_t* event) {
+    if (event != NULL) {
+        celix_framework_bundleEntry_decreaseUseCount(event->bndEntry);
+        celixThreadMutex_destroy(&event->mutex);
+        celixThreadCondition_destroy(&event->cond);
+        if (event->eventName != CELIX_SCHEDULED_EVENT_DEFAULT_NAME) {
+            free(event->eventName);
+        }
+        free(event);
+    }
+}
+
+void celix_scheduledEvent_waitAndDestroy(celix_scheduled_event_t* event) {
+    if (event == NULL) {
+        return;
+    }
+
+    //wait for use count 0;
+    celixThreadMutex_lock(&event->mutex);
+    while (event->useCount) {
+        celixThreadCondition_wait(&event->cond, &event->mutex);
+    }
+    celixThreadMutex_unlock(&event->mutex);
+
+    celix_scheduledEvent_destroy(event);
+}
+
+bool celix_scheduledEvent_deadlineReached(celix_scheduled_event_t* event,
+                                          const struct timespec* currentTime,
+                                          double* nextProcessTimeInSeconds) {
+    celixThreadMutex_lock(&event->mutex);
+    double elapsed = celix_difftime(&event->lastScheduledEventTime, 
currentTime);
+    double deadline = event->callCount == 0 ? event->initialDelayInSeconds : 
event->intervalInSeconds;
+    deadline -= CELIX_SCHEDULED_EVENT_INTERVAL_ALLOW_ERROR_IN_SECONDS;
+    bool deadlineReached = elapsed >= deadline;
+    if (deadlineReached) {
+        *nextProcessTimeInSeconds =
+            event->intervalInSeconds == 0 /*one shot*/ ? 
CELIX_FRAMEWORK_DEFAULT_MAX_TIMEDWAIT_EVENT_HANDLER_IN_SECONDS
+                                                       : 
event->intervalInSeconds;
+    } else {
+        *nextProcessTimeInSeconds = event->callCount == 0 ? 
event->initialDelayInSeconds : event->intervalInSeconds;
+    }
+    celixThreadMutex_unlock(&event->mutex);
+    return deadlineReached;
+}
+
+void celix_scheduledEvent_process(celix_scheduled_event_t* event, const struct 
timespec* currentTime) {
+    void (*callback)(void*) = NULL;
+    void* callbackData = NULL;
+
+    celixThreadMutex_lock(&event->mutex);
+    callback = event->eventCallback;
+    callbackData = event->eventCallbackData;
+    event->useCount += 1;
+    celixThreadCondition_broadcast(&event->cond); //broadcast for changed 
useCount
+    celixThreadMutex_unlock(&event->mutex);
+    assert(callback != NULL);
+
+    callback(callbackData); //note called outside of lock
+
+    celixThreadMutex_lock(&event->mutex);
+    event->lastScheduledEventTime = *currentTime;
+    event->useCount -= 1;
+    event->callCount += 1;
+    celixThreadCondition_broadcast(&event->cond); //broadcast for changed 
useCount
+    celixThreadMutex_unlock(&event->mutex);
+}
+
+
+bool celix_scheduleEvent_isDone(celix_scheduled_event_t* event) {
+    bool isDone = false;
+    celixThreadMutex_lock(&event->mutex);
+    isDone = event->intervalInSeconds == 0 && event->callCount > 0;
+    celixThreadMutex_unlock(&event->mutex);
+    return isDone;
+}
diff --git a/libs/framework/src/celix_scheduled_event.h 
b/libs/framework/src/celix_scheduled_event.h
new file mode 100644
index 00000000..d1976152
--- /dev/null
+++ b/libs/framework/src/celix_scheduled_event.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.
+ */
+
+#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
+
+//Allow, 1 microsecond error in interval to ensure pthread cond wakeups result 
in a call.
+#define CELIX_SCHEDULED_EVENT_INTERVAL_ALLOW_ERROR_IN_SECONDS 0.000001
+
+static const char* const CELIX_SCHEDULED_EVENT_DEFAULT_NAME = "unnamed";
+
+typedef struct celix_scheduled_event {
+    long scheduledEventId;
+    celix_framework_logger_t* logger;
+    celix_framework_bundle_entry_t* bndEntry;
+
+    char* eventName;
+    double initialDelayInSeconds;
+    double intervalInSeconds;
+    void* eventCallbackData;
+    void (*eventCallback)(void* eventData);
+
+    celix_thread_mutex_t mutex; //protects below
+    celix_thread_cond_t cond;
+    size_t useCount; //use count, how many times the event is used to process 
a eventCallback or doneCallback.
+    size_t callCount; //nr of times the eventCallback is called
+    struct timespec lastScheduledEventTime;
+} celix_scheduled_event_t;
+
+/**
+ * @brief Create a scheduled event for the given bundle.
+ * @param[in] bndEntry The bundle entry for which the scheduled event is 
created.
+ * @param[in] scheduledEventId The id of the scheduled event.
+ * @param[in] eventName 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] eventData The event data.
+ * @param[in] eventCallback The event callback.
+ * @return A new scheduled event or NULL if failed.
+ */
+celix_scheduled_event_t* celix_scheduledEvent_create(celix_framework_logger_t* 
logger,
+                                                     
celix_framework_bundle_entry_t* bndEntry,
+                                                     long scheduledEventId,
+                                                     const char* eventName,
+                                                     double 
initialDelayInSeconds,
+                                                     double intervalInSeconds,
+                                                     void* eventData,
+                                                     void 
(*eventCallback)(void* eventData));
+
+/**
+ * @brief Destroy the event.
+ */
+void celix_scheduledEvent_destroy(celix_scheduled_event_t* event);
+
+/**
+ * @brief Wait until the useCount is 0 and destroy the event.
+ */
+void celix_scheduledEvent_waitAndDestroy(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] currentTime The current time.
+ * @param[out] nextProcessTimeInSeconds The time in seconds until the next 
event should be processed.
+ *                                      if the deadline is reached, this is 
the next interval.
+ * @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* currentTime,
+                                          double* nextProcessTimeInSeconds);
+
+/**
+ * @brief Process the event by calling the event callback.
+ *
+ * Must be called on the Celix event thread.
+ *
+ * @param[in] event The event to process.
+ * @param[in] currentTime The current time.
+ * @return The time in seconds until the next event should be processed.
+ */
+void celix_scheduledEvent_process(celix_scheduled_event_t* event, const struct 
timespec* currentTime);
+
+/**
+ * @brief Returns true if the event is a one-shot event and is done.
+ * @param[in] event The event to check.
+ * @return  true if the event is a one-shot event and is done.
+ */
+bool celix_scheduleEvent_isDone(celix_scheduled_event_t* event);
+
+/**
+ * @brief Wait for a scheduled event to reach at least the provided call count.
+ * @param[in] event The event to wait for.
+ * @param[in] callCount The call count to wait for.
+ * @param[in] timeout The max time to wait in seconds.
+ * @return CELIX_SUCCESS if the scheduled event reached the call count, 
CELIX_TIMEOUT if the scheduled event
+ *                       did not reach the call count within the timeout.
+ */
+celix_status_t
+celix_scheduledEvent_waitForAtLeastCallCount(celix_scheduled_event_t* event, 
size_t useCount, double timeout);
+
+#ifdef __cplusplus
+};
+#endif
+
+#endif //CELIX_CELIX_SCHEDULED_EVENT_H
diff --git a/libs/framework/src/framework.c b/libs/framework/src/framework.c
index df4bb7b4..b8252c01 100644
--- a/libs/framework/src/framework.c
+++ b/libs/framework/src/framework.c
@@ -18,14 +18,12 @@
  */
 
 #include <stdlib.h>
-#include <stdio.h>
 #include <string.h>
-#include <unistd.h>
 #include <stdbool.h>
 #include <uuid/uuid.h>
 #include <assert.h>
-#include <celix_log_utils.h>
 
+#include "celix_log_utils.h"
 #include "celix_dependency_manager.h"
 #include "framework_private.h"
 #include "celix_constants.h"
@@ -43,6 +41,7 @@
 #include "bundle_archive_private.h"
 #include "celix_module_private.h"
 #include "celix_convert_utils.h"
+#include "celix_scheduled_event.h"
 #include "celix_build_assert.h"
 
 struct celix_bundle_activator {
@@ -249,6 +248,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.scheduledEventQueue = celix_arrayList_create();
 
     //create and store framework uuid
     char uuid[37];
@@ -293,6 +293,11 @@ celix_status_t framework_create(framework_pt *out, 
celix_properties_t* config) {
 }
 
 celix_status_t framework_destroy(framework_pt framework) {
+    if (framework == NULL) {
+        //nop
+        return CELIX_SUCCESS;
+    }
+
     celix_status_t status = CELIX_SUCCESS;
 
     celixThreadMutex_lock(&framework->shutdown.mutex);
@@ -376,6 +381,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_arrayList_size(framework->dispatcher.scheduledEventQueue) == 
0);
+    celix_arrayList_destroy(framework->dispatcher.scheduledEventQueue);
+
     celix_bundleCache_destroy(framework->cache);
 
        celixThreadCondition_destroy(&framework->dispatcher.cond);
@@ -1388,6 +1396,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;
 }
@@ -1396,32 +1405,128 @@ 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 double celix_framework_processScheduledEvents(celix_framework_t* fw) {
+    double nextScheduledEvent = 0.0;
+    struct timespec ts;
+    clock_gettime(CLOCK_MONOTONIC, &ts);
+
+    celix_scheduled_event_t* callEvent;
+    do {
+        callEvent = NULL;
+        double nextEvent;
+        celixThreadMutex_lock(&fw->dispatcher.mutex);
+        for (int i = 0; i< 
celix_arrayList_size(fw->dispatcher.scheduledEventQueue); ++i) {
+            celix_scheduled_event_t* event = 
celix_arrayList_get(fw->dispatcher.scheduledEventQueue, i);
+            bool call = celix_scheduledEvent_deadlineReached(event, &ts, 
&nextEvent);
+            if (nextScheduledEvent == 0.0 || nextEvent < nextScheduledEvent) {
+                nextScheduledEvent = nextEvent;
+            }
+            if (call) {
+                callEvent = event;
+                break;
+            }
+        }
+        celixThreadMutex_unlock(&fw->dispatcher.mutex);
+
+        if (callEvent != NULL) {
+            celix_scheduledEvent_process(callEvent, &ts);
+            if (celix_scheduleEvent_isDone(callEvent)) {
+                celixThreadMutex_lock(&fw->dispatcher.mutex);
+                fw_log(fw->logger,
+                       CELIX_LOG_LEVEL_DEBUG,
+                       "Removing processed one-shot scheduled event '%s' 
(id=%li) for bundle '%s' (id=%li)",
+                       callEvent->eventName,
+                       callEvent->scheduledEventId,
+                       celix_bundle_getSymbolicName(callEvent->bndEntry->bnd),
+                       callEvent->bndEntry->bndId);
+                celix_arrayList_remove(fw->dispatcher.scheduledEventQueue, 
callEvent);
+                celixThreadMutex_unlock(&fw->dispatcher.mutex);
+                celix_scheduledEvent_destroy(callEvent);
+            }
+        }
+    } while (callEvent != NULL);
+
+    return nextScheduledEvent;
+}
+
+void celix_framework_cleanupScheduledEvents(celix_framework_t* fw, long bndId) 
{
+    celix_scheduled_event_t* removedEvent;
+    do {
+        removedEvent = NULL;
+        celixThreadMutex_lock(&fw->dispatcher.mutex);
+        for (int i = 0; i< 
celix_arrayList_size(fw->dispatcher.scheduledEventQueue); ++i) {
+            celix_scheduled_event_t* visit = 
celix_arrayList_get(fw->dispatcher.scheduledEventQueue, i);
+            if (visit->bndEntry->bndId == bndId) {
+                removedEvent = visit;
+                celix_arrayList_removeAt(fw->dispatcher.scheduledEventQueue, 
i);
+                break;
+            }
+        }
+        celixThreadMutex_unlock(&fw->dispatcher.mutex);
+
+        if (removedEvent) {
+            if (removedEvent->intervalInSeconds > 0) {
+                fw_log(fw->logger,
+                       CELIX_LOG_LEVEL_WARNING,
+                       "Removing dangling scheduled event '%s' (id=%li) for 
bundle '%s' (id=%li). This should have been cleaned up by the bundle.",
+                       removedEvent->eventName,
+                       removedEvent->scheduledEventId,
+                       
celix_bundle_getSymbolicName(removedEvent->bndEntry->bnd),
+                       removedEvent->bndEntry->bndId);
+            } else {
+                fw_log(fw->logger,
+                       CELIX_LOG_LEVEL_DEBUG,
+                       "Removing unprocessed one-shot scheduled event '%s' 
(id=%li) for bundle '%s' (id=%li)",
+                       removedEvent->eventName,
+                       removedEvent->scheduledEventId,
+                       
celix_bundle_getSymbolicName(removedEvent->bndEntry->bnd),
+                       removedEvent->bndEntry->bndId);
+            }
+            celix_scheduledEvent_waitAndDestroy(removedEvent);
+        }
+    } while (removedEvent != NULL);
+}
+
+static void celix_framework_waitForNextEvent(celix_framework_t* fw, double 
nextScheduledEvent) {
+    long seconds = 
CELIX_FRAMEWORK_DEFAULT_MAX_TIMEDWAIT_EVENT_HANDLER_IN_SECONDS;
+    long nanoseconds = 0;
+    if (nextScheduledEvent > 0) {
+        seconds = (long) nextScheduledEvent;
+        nanoseconds = ((long)(nextScheduledEvent * 1000000000L)) - seconds;
+    }
+
+    celixThreadMutex_lock(&fw->dispatcher.mutex);
+    int size = fw->dispatcher.eventQueueSize + 
celix_arrayList_size(fw->dispatcher.dynamicEventQueue);
+    if (size == 0 && fw->dispatcher.active) {
+        celixThreadCondition_timedwaitRelative(&fw->dispatcher.cond, 
&fw->dispatcher.mutex, seconds, nanoseconds);
+    }
+    celixThreadMutex_unlock(&fw->dispatcher.mutex);
+}
+
 static void *fw_eventDispatcher(void *fw) {
     framework_pt framework = (framework_pt) fw;
 
@@ -1431,6 +1536,9 @@ static void *fw_eventDispatcher(void *fw) {
 
     while (active) {
         fw_handleEvents(framework);
+        double nextScheduledEvent = 
celix_framework_processScheduledEvents(framework);
+        celix_framework_waitForNextEvent(framework, nextScheduledEvent);
+
         celixThreadMutex_lock(&framework->dispatcher.mutex);
         active = framework->dispatcher.active;
         celixThreadMutex_unlock(&framework->dispatcher.mutex);
@@ -1811,7 +1919,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);
@@ -2350,14 +2457,31 @@ 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_timedWaitForEmptyEventQueue(celix_framework_t 
*fw, double periodInSeconds) {
     assert(!celix_framework_isCurrentThreadTheEventLoop(fw));
 
+    celix_status_t status = CELIX_SUCCESS;
+    long seconds = (long) periodInSeconds;
+    long nanoseconds = (long) ((periodInSeconds - (double)seconds) * 
1000000000L);
+
     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);
+        if (periodInSeconds > 0) {
+            status = 
celixThreadCondition_timedwaitRelative(&fw->dispatcher.cond, 
&fw->dispatcher.mutex, seconds, nanoseconds);
+            if (status != CELIX_SUCCESS) {
+                status = CELIX_TIMEOUT;
+                break;
+            }
+        } else {
+            celixThreadCondition_wait(&fw->dispatcher.cond, 
&fw->dispatcher.mutex);
+        }
     }
     celixThreadMutex_unlock(&fw->dispatcher.mutex);
+    return status;
+}
+
+void celix_framework_waitForEmptyEventQueue(celix_framework_t *fw) {
+    celix_framework_waitUntilNoEventsForBnd(fw, 0);
 }
 
 void celix_framework_waitUntilNoEventsForBnd(celix_framework_t* fw, long 
bndId) {
@@ -2399,6 +2523,96 @@ void 
celix_framework_waitUntilNoPendingRegistration(celix_framework_t* fw)
     celixThreadMutex_unlock(&fw->dispatcher.mutex);
 }
 
+long celix_framework_addScheduledEvent(celix_framework_t* fw,
+                                       long bndId,
+                                       const char* eventName,
+                                       double initialDelayInSeconds,
+                                       double intervalInSeconds,
+                                       void* eventData,
+                                       void (*eventCallback)(void* eventData)) 
{
+    if (eventCallback == 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->logger,
+                                                                 bndEntry,
+                                                                 
celix_framework_nextScheduledEventId(fw),
+                                                                 eventName,
+                                                                 
initialDelayInSeconds,
+                                                                 
intervalInSeconds,
+                                                                 eventData,
+                                                                 
eventCallback);
+
+    if (event == NULL) {
+        return -1; //error logged by celix_scheduledEvent_create
+    }
+
+    celixThreadMutex_lock(&fw->dispatcher.mutex);
+    celix_status_t addStatus = 
celix_arrayList_add(fw->dispatcher.scheduledEventQueue, event);
+    celixThreadCondition_broadcast(&fw->dispatcher.cond); //notify dispatcher 
thread for newly added scheduled event
+    celixThreadMutex_unlock(&fw->dispatcher.mutex);
+
+    if (addStatus != CELIX_SUCCESS) {
+        fw_log(fw->logger, CELIX_LOG_LEVEL_ERROR, "Cannot add scheduled event 
for bundle id %li. Out of memory", bndId);
+        celix_scheduledEvent_destroy(event);
+        return -1;
+    }
+
+    fw_log(fw->logger,
+           CELIX_LOG_LEVEL_DEBUG,
+           "Added scheduled event '%s' (id=%li) for bundle '%s' (id=%li)",
+           event->eventName,
+           event->scheduledEventId,
+           celix_bundle_getSymbolicName(bndEntry->bnd),
+           bndId);
+
+    return event->scheduledEventId;
+}
+
+bool celix_framework_removeScheduledEvent(celix_framework_t* fw, long 
scheduledEventId) {
+    if (scheduledEventId < 0) {
+        return false; //silently ignore
+    }
+
+    celix_scheduled_event_t* event = NULL;
+    celixThreadMutex_lock(&fw->dispatcher.mutex);
+    for (int i = 0; i < 
celix_arrayList_size(fw->dispatcher.scheduledEventQueue); ++i) {
+        celix_scheduled_event_t* visit = 
celix_arrayList_get(fw->dispatcher.scheduledEventQueue, i);
+        if (visit->scheduledEventId == scheduledEventId) {
+            event = visit;
+            celix_arrayList_removeAt(fw->dispatcher.scheduledEventQueue, i);
+            break;
+        }
+    }
+    celixThreadMutex_unlock(&fw->dispatcher.mutex);
+
+    if (event == NULL) {
+        fw_log(fw->logger, CELIX_LOG_LEVEL_ERROR, "Cannot remove scheduled 
event with id %li. Not found",
+               scheduledEventId);
+        return false;
+    }
+
+    fw_log(fw->logger,
+           CELIX_LOG_LEVEL_DEBUG,
+           "Removing scheduled event '%s' (id=%li) for bundle '%s' (id=%li)",
+           event->eventName,
+           event->scheduledEventId,
+           celix_bundle_getSymbolicName(event->bndEntry->bnd),
+           event->bndEntry->bndId);
+    celix_scheduledEvent_waitAndDestroy(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);
 }
@@ -2434,7 +2648,11 @@ 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);
+}
+
+long celix_framework_nextScheduledEventId(framework_t *fw) {
+    return __atomic_fetch_add(&fw->dispatcher.nextScheduledEventId, 1, 
__ATOMIC_RELAXED);
 }
 
 void celix_framework_waitForGenericEvent(framework_t *fw, long eventId) {
@@ -2478,3 +2696,7 @@ void celix_framework_waitForStop(celix_framework_t 
*framework) {
 
     celixThreadMutex_unlock(&framework->shutdown.mutex);
 }
+
+celix_framework_logger_t* celix_framework_getLogger(celix_framework_t* fw) {
+    return fw->logger;
+}
diff --git a/libs/framework/src/framework_private.h 
b/libs/framework/src/framework_private.h
index 8d60d4c7..04b4b700 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 <stdbool.h>
+
 #include "celix_framework.h"
 #include "framework.h"
-
 #include "manifest.h"
 #include "wire.h"
 #include "hash_map.h"
@@ -37,15 +38,15 @@
 #include "bundle_context.h"
 #include "celix_bundle_cache.h"
 #include "celix_log.h"
-
 #include "celix_threads.h"
 #include "service_registry.h"
-#include <stdbool.h>
 
 #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"
@@ -158,10 +159,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;
@@ -174,12 +180,11 @@ struct celix_framework {
             int nbUnregister; // number of pending async de-registration
             int nbEvent; // number of pending generic events
         } stats;
+        celix_array_list_t *scheduledEventQueue; //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
@@ -434,4 +439,57 @@ 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. If < 0 the 
framework bundle is used.
+ * @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] eventData The event data to pass to the event callback.
+ * @param[in] eventCallback The event callback to call when the scheduled 
event is triggered.
+ * @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_addScheduledEvent(celix_framework_t* fw,
+                                       long bndId,
+                                       const char* eventName,
+                                       double initialDelayInSeconds,
+                                       double intervalInSeconds,
+                                       void* eventData,
+                                       void (*eventCallback)(void* eventData));
+
+/**
+ * @brief Cancel a scheduled event.
+ *
+ * When this function returns, no more scheduled event callbacks will be 
called.
+ *
+ * @param[in] fw The Celix framework
+ * @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, 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);
+
+/**
+ * @brief Return the framework logger. Note logger lifetime is the same as the 
framework.
+ */
+celix_framework_logger_t* celix_framework_getLogger(celix_framework_t* fw);
+
 #endif /* FRAMEWORK_PRIVATE_H_ */
diff --git a/libs/utils/include/celix_errno.h b/libs/utils/include/celix_errno.h
index 68250016..a11d44a4 100644
--- a/libs/utils/include/celix_errno.h
+++ b/libs/utils/include/celix_errno.h
@@ -190,6 +190,8 @@ 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)
+#define CELIX_TIMEOUT                   
CELIX_ERROR_MAKE(CELIX_FACILITY_FRAMEWORK, 4474)
+
 /*!
  * Exception indicating a problem with a interceptor
  */
diff --git a/libs/utils/src/celix_threads.c b/libs/utils/src/celix_threads.c
index 088f216e..bd56f3d2 100644
--- a/libs/utils/src/celix_threads.c
+++ b/libs/utils/src/celix_threads.c
@@ -186,7 +186,7 @@ celix_status_t 
celixThreadCondition_timedwaitRelative(celix_thread_cond_t *cond,
     seconds = seconds >= 0 ? seconds : 0;
     time = celix_gettime(CLOCK_MONOTONIC);
     time.tv_sec += seconds;
-    if(nanoseconds > 0) {
+    if (nanoseconds > 0) {
         time.tv_nsec += nanoseconds;
         while (time.tv_nsec > CELIX_NS_IN_SEC) {
             time.tv_sec++;

Reply via email to