fix(esp_event): Handler unregistration by itself issue

when esp_event_handler_unregister_with_internal cannot take
the loop mutex (e.g., when the handler unregisters itself),
create an event with a special base identifier and add it to
the queue of the corresponding loop to postpone the removal
of the handler from the list at a time when the loop mutex can be
successfully taken.
This commit is contained in:
Guillaume Souchere 2024-12-31 12:11:37 +01:00
parent d1a02e1854
commit 0938688e9b
4 changed files with 303 additions and 95 deletions

View File

@ -38,6 +38,7 @@
static const char* TAG = "event";
static const char* esp_event_any_base = "any";
static const char* esp_event_handler_cleanup = "cleanup";
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
static SLIST_HEAD(esp_event_loop_instance_list_t, esp_event_loop_instance) s_event_loops =
@ -142,51 +143,8 @@ static void handler_execute(esp_event_loop_instance_t* loop, esp_event_handler_n
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
diff = esp_timer_get_time() - start;
xSemaphoreTake(loop->profiling_mutex, portMAX_DELAY);
// At this point handler may be already unregistered.
// This happens in "handler instance can unregister itself" test case.
// To prevent memory corruption error it's necessary to check if pointer is still valid.
esp_event_loop_node_t* loop_node;
SLIST_FOREACH(loop_node, &(loop->loop_nodes), next) {
// try to find the handler amongst the list of handlers that are registered on the
// loop level (i.e., registered to be triggered for all events)
esp_event_handler_node_t* handler_node;
SLIST_FOREACH(handler_node, &(loop_node->handlers), next) {
if(handler_node == handler) {
handler->invoked++;
handler->time += diff;
}
}
// try to find the handler amongst the list of handlers that are registered on the
// base level (i.e., registered to be triggered for all events of a specific event base)
esp_event_base_node_t* base_node;
SLIST_FOREACH(base_node, &(loop_node->base_nodes), next) {
esp_event_handler_node_t* base_handler_node;
SLIST_FOREACH(base_handler_node, &(base_node->handlers), next) {
if (base_handler_node == handler) {
handler->invoked++;
handler->time += diff;
}
}
// try to find the handler amongst the list of handlers that are registered on the
// ID level (i.e., registered to be triggered for a specific event ID of a specific event base)
esp_event_id_node_t* id_node;
SLIST_FOREACH(id_node, &(base_node->id_nodes), next) {
esp_event_handler_node_t* id_handler_node;
SLIST_FOREACH(id_handler_node, &(id_node->handlers), next) {
if (id_handler_node == handler) {
handler->invoked++;
handler->time += diff;
}
}
}
}
}
xSemaphoreGive(loop->profiling_mutex);
handler->invoked++;
handler->time += diff;
#endif
}
@ -391,8 +349,8 @@ static esp_err_t base_node_remove_handler(esp_event_base_node_t* base_node, int3
if (SLIST_EMPTY(&(it->handlers))) {
SLIST_REMOVE(&(base_node->id_nodes), it, esp_event_id_node, next);
free(it);
return ESP_OK;
}
return ESP_OK;
}
}
}
@ -416,8 +374,8 @@ static esp_err_t loop_node_remove_handler(esp_event_loop_node_t* loop_node, esp_
if (SLIST_EMPTY(&(it->handlers)) && SLIST_EMPTY(&(it->id_nodes))) {
SLIST_REMOVE(&(loop_node->base_nodes), it, esp_event_base_node, next);
free(it);
return ESP_OK;
}
return ESP_OK;
}
}
}
@ -426,6 +384,23 @@ static esp_err_t loop_node_remove_handler(esp_event_loop_node_t* loop_node, esp_
return ESP_ERR_NOT_FOUND;
}
static esp_err_t loop_remove_handler(esp_event_remove_handler_context_t* ctx)
{
esp_event_loop_node_t *it, *temp;
SLIST_FOREACH_SAFE(it, &(ctx->loop->loop_nodes), next, temp) {
esp_err_t res = loop_node_remove_handler(it, ctx->event_base, ctx->event_id, ctx->handler_ctx, ctx->legacy);
if (res == ESP_OK) {
if (SLIST_EMPTY(&(it->base_nodes)) && SLIST_EMPTY(&(it->handlers))) {
SLIST_REMOVE(&(ctx->loop->loop_nodes), it, esp_event_loop_node, next);
free(it);
}
return ESP_OK;
}
}
return ESP_ERR_NOT_FOUND;
}
static void handler_instances_remove_all(esp_event_handler_nodes_t* handlers)
{
esp_event_handler_node_t *it, *temp;
@ -474,6 +449,104 @@ static void inline __attribute__((always_inline)) post_instance_delete(esp_event
memset(post, 0, sizeof(*post));
}
static esp_err_t find_and_unregister_handler(esp_event_remove_handler_context_t* ctx)
{
esp_event_handler_node_t *handler_to_unregister = NULL;
esp_event_handler_node_t *handler;
esp_event_loop_node_t *loop_node;
esp_event_base_node_t *base_node;
esp_event_id_node_t *id_node;
SLIST_FOREACH(loop_node, &(ctx->loop->loop_nodes), next) {
// Execute loop level handlers
SLIST_FOREACH(handler, &(loop_node->handlers), next) {
if (ctx->legacy) {
if (handler->handler_ctx->handler == ctx->handler_ctx->handler) {
handler_to_unregister = handler;
break;
}
} else {
if (handler->handler_ctx == ctx->handler_ctx) {
handler_to_unregister = handler;
break;
}
}
}
if (handler_to_unregister != NULL) {
break;
}
SLIST_FOREACH(base_node, &(loop_node->base_nodes), next) {
if (base_node->base == ctx->event_base) {
// Execute base level handlers
SLIST_FOREACH(handler, &(base_node->handlers), next) {
if (ctx->legacy) {
if (handler->handler_ctx->handler == ctx->handler_ctx->handler) {
handler_to_unregister = handler;
break;
}
} else {
if (handler->handler_ctx == ctx->handler_ctx) {
handler_to_unregister = handler;
break;
}
}
}
if (handler_to_unregister != NULL) {
break;
}
SLIST_FOREACH(id_node, &(base_node->id_nodes), next) {
if (id_node->id == ctx->event_id) {
// Execute id level handlers
SLIST_FOREACH(handler, &(id_node->handlers), next) {
if (ctx->legacy) {
if (handler->handler_ctx->handler == ctx->handler_ctx->handler) {
handler_to_unregister = handler;
break;
}
} else {
if (handler->handler_ctx == ctx->handler_ctx) {
handler_to_unregister = handler;
break;
}
}
}
}
}
}
}
}
if (handler_to_unregister == NULL) {
/* handler not found in the lists, return */
return ESP_ERR_NOT_FOUND;
}
if (handler_to_unregister->unregistered) {
/* the handler was found in a list but has already be marked
* as unregistered. It means an event was already created to
* remove from the list. return OK but do nothing */
return ESP_OK;
}
/* handler found in the lists and not already marked as unregistered. Mark it as unregistered
* and post an event to remove it from the lists */
handler_to_unregister->unregistered = true;
if (ctx->legacy) {
/* in case of legacy code, we have to copy the handler_ctx content since it was created in the calling function */
esp_event_handler_instance_context_t *handler_ctx_copy = calloc(1, sizeof(esp_event_handler_instance_context_t));
if (!handler_ctx_copy) {
return ESP_ERR_NO_MEM;
}
handler_ctx_copy->arg = ctx->handler_ctx->arg;
handler_ctx_copy->handler = ctx->handler_ctx->handler;
ctx->handler_ctx = handler_ctx_copy;
}
return esp_event_post_to(ctx->loop, esp_event_handler_cleanup, 0, ctx, sizeof(esp_event_remove_handler_context_t), portMAX_DELAY);
}
/* ---------------------------- Public API --------------------------------- */
esp_err_t esp_event_loop_create(const esp_event_loop_args_t* event_loop_args, esp_event_loop_handle_t* event_loop)
@ -509,14 +582,6 @@ esp_err_t esp_event_loop_create(const esp_event_loop_args_t* event_loop_args, es
goto on_err;
}
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
loop->profiling_mutex = xSemaphoreCreateMutex();
if (loop->profiling_mutex == NULL) {
ESP_LOGE(TAG, "create event loop profiling mutex failed");
goto on_err;
}
#endif
SLIST_INIT(&(loop->loop_nodes));
// Create the loop task if requested
@ -562,12 +627,6 @@ on_err:
vSemaphoreDelete(loop->mutex);
}
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
if (loop->profiling_mutex != NULL) {
vSemaphoreDelete(loop->profiling_mutex);
}
#endif
free(loop);
return err;
@ -594,10 +653,25 @@ esp_err_t esp_event_loop_run(esp_event_loop_handle_t event_loop, TickType_t tick
int64_t remaining_ticks = ticks_to_run;
#endif
while(xQueueReceive(loop->queue, &post, ticks_to_run) == pdTRUE) {
while (xQueueReceive(loop->queue, &post, remaining_ticks) == pdTRUE) {
// The event has already been unqueued, so ensure it gets executed.
xSemaphoreTakeRecursive(loop->mutex, portMAX_DELAY);
// check if the event retrieve from the queue is the internal event that is
// triggered when a handler needs to be removed..
if (post.base == esp_event_handler_cleanup) {
assert(post.data.ptr != NULL);
esp_event_remove_handler_context_t* ctx = (esp_event_remove_handler_context_t*)post.data.ptr;
loop_remove_handler(ctx);
// if the handler unregistration request came from legacy code,
// we have to free handler_ctx pointer since it points to memory
// allocated by esp_event_handler_unregister_with_internal
if (ctx->legacy) {
free(ctx->handler_ctx);
}
}
loop->running_task = xTaskGetCurrentTaskHandle();
bool exec = false;
@ -610,24 +684,30 @@ esp_err_t esp_event_loop_run(esp_event_loop_handle_t event_loop, TickType_t tick
SLIST_FOREACH_SAFE(loop_node, &(loop->loop_nodes), next, temp_node) {
// Execute loop level handlers
SLIST_FOREACH_SAFE(handler, &(loop_node->handlers), next, temp_handler) {
handler_execute(loop, handler, post);
exec |= true;
if (!handler->unregistered) {
handler_execute(loop, handler, post);
exec |= true;
}
}
SLIST_FOREACH_SAFE(base_node, &(loop_node->base_nodes), next, temp_base) {
if (base_node->base == post.base) {
// Execute base level handlers
SLIST_FOREACH_SAFE(handler, &(base_node->handlers), next, temp_handler) {
handler_execute(loop, handler, post);
exec |= true;
if (!handler->unregistered) {
handler_execute(loop, handler, post);
exec |= true;
}
}
SLIST_FOREACH_SAFE(id_node, &(base_node->id_nodes), next, temp_id_node) {
if (id_node->id == post.id) {
// Execute id level handlers
SLIST_FOREACH_SAFE(handler, &(id_node->handlers), next, temp_handler) {
handler_execute(loop, handler, post);
exec |= true;
if (!handler->unregistered) {
handler_execute(loop, handler, post);
exec |= true;
}
}
// Skip to next base node
break;
@ -674,14 +754,10 @@ esp_err_t esp_event_loop_delete(esp_event_loop_handle_t event_loop)
esp_event_loop_instance_t* loop = (esp_event_loop_instance_t*) event_loop;
SemaphoreHandle_t loop_mutex = loop->mutex;
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
SemaphoreHandle_t loop_profiling_mutex = loop->profiling_mutex;
#endif
xSemaphoreTakeRecursive(loop->mutex, portMAX_DELAY);
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
xSemaphoreTake(loop->profiling_mutex, portMAX_DELAY);
portENTER_CRITICAL(&s_event_loops_spinlock);
SLIST_REMOVE(&s_event_loops, loop, esp_event_loop_instance, next);
portEXIT_CRITICAL(&s_event_loops_spinlock);
@ -711,10 +787,6 @@ esp_err_t esp_event_loop_delete(esp_event_loop_handle_t event_loop)
free(loop);
// Free loop mutex before deleting
xSemaphoreGiveRecursive(loop_mutex);
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
xSemaphoreGive(loop_profiling_mutex);
vSemaphoreDelete(loop_profiling_mutex);
#endif
vSemaphoreDelete(loop_mutex);
return ESP_OK;
@ -814,24 +886,21 @@ esp_err_t esp_event_handler_unregister_with_internal(esp_event_loop_handle_t eve
}
esp_event_loop_instance_t* loop = (esp_event_loop_instance_t*) event_loop;
esp_event_remove_handler_context_t remove_handler_ctx = {loop, event_base, event_id, handler_ctx, legacy};
xSemaphoreTakeRecursive(loop->mutex, portMAX_DELAY);
esp_event_loop_node_t *it, *temp;
SLIST_FOREACH_SAFE(it, &(loop->loop_nodes), next, temp) {
esp_err_t res = loop_node_remove_handler(it, event_base, event_id, handler_ctx, legacy);
if (res == ESP_OK && SLIST_EMPTY(&(it->base_nodes)) && SLIST_EMPTY(&(it->handlers))) {
SLIST_REMOVE(&(loop->loop_nodes), it, esp_event_loop_node, next);
free(it);
break;
}
/* remove the handler if the mutex is taken successfully.
* otherwise it will be removed from the list later */
esp_err_t res = ESP_FAIL;
if (xSemaphoreTake(loop->mutex, 0) == pdTRUE) {
res = loop_remove_handler(&remove_handler_ctx);
xSemaphoreGive(loop->mutex);
} else {
xSemaphoreTakeRecursive(loop->mutex, portMAX_DELAY);
res = find_and_unregister_handler(&remove_handler_ctx);
xSemaphoreGiveRecursive(loop->mutex);
}
xSemaphoreGiveRecursive(loop->mutex);
return ESP_OK;
return res;
}
esp_err_t esp_event_handler_unregister_with(esp_event_loop_handle_t event_loop, esp_event_base_t event_base,

View File

@ -31,6 +31,7 @@ typedef struct esp_event_handler_node {
int64_t time; /**< total runtime of this handler across all calls */
#endif
SLIST_ENTRY(esp_event_handler_node) next; /**< next event handler in the list */
bool unregistered;
} esp_event_handler_node_t;
typedef SLIST_HEAD(esp_event_handler_instances, esp_event_handler_node) esp_event_handler_nodes_t;
@ -78,11 +79,18 @@ typedef struct esp_event_loop_instance {
#ifdef CONFIG_ESP_EVENT_LOOP_PROFILING
atomic_uint_least32_t events_received; /**< number of events successfully posted to the loop */
atomic_uint_least32_t events_dropped; /**< number of events dropped due to queue being full */
SemaphoreHandle_t profiling_mutex; /**< mutex used for profiliing */
SLIST_ENTRY(esp_event_loop_instance) next; /**< next event loop in the list */
#endif
} esp_event_loop_instance_t;
typedef struct esp_event_remove_handler_context_t {
esp_event_loop_instance_t* loop; /**< Instance of the event loop from which the handler has to be removed */
esp_event_base_t event_base; /**< The event base identification of the handler that has to be removed */
int32_t event_id; /**< The event identification value of the handler that has to be removed */
esp_event_handler_instance_context_t* handler_ctx; /**< The handler context of the handler that has to be removed */
bool legacy; /**< Set to true when the handler unregistration request was made from legacy code */
} esp_event_remove_handler_context_t;
#if CONFIG_ESP_EVENT_POST_FROM_ISR
typedef union esp_event_post_data {
uint32_t val;

View File

@ -1575,19 +1575,27 @@ TEST_CASE("default event loop: registering event handler instance without instan
#if CONFIG_ESP_EVENT_LOOP_PROFILING
static void handler_all(void* arg, esp_event_base_t event_base, int32_t event_id, void* data)
{
printf("Event received: base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
printf("base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
}
static void handler_base(void* arg, esp_event_base_t event_base, int32_t event_id, void* data)
{
printf("Event received: base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
printf("base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
}
static void handler_id(void* arg, esp_event_base_t event_base, int32_t event_id, void* data)
{
printf("Event received: base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
printf("base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
}
/* This test executes the following steps:
* 1) register handler_id for a event id A and event base B,
* 2) register handler_base for a given event base,
* 3) register handler_all for on the loop level,
* 4) post an event (A, B),
* 5) call esp_event_dump and make sure by parsing the string IN PYTEST ENVIRONMENT that
* all handlers profiling data is printed
* 6) unregister the handlers successfully */
TEST_CASE("profiling reports valid values", "[event][default]")
{
TEST_ESP_OK(esp_event_loop_create_default());
@ -1616,4 +1624,114 @@ TEST_CASE("profiling reports valid values", "[event][default]")
/* delete loop */
TEST_ESP_OK(esp_event_loop_delete_default());
}
static void handler_id2(void* arg, esp_event_base_t event_base, int32_t event_id, void* data)
{
printf("event received: base=%s, id=%" PRId32 ", data=%p\n", event_base, event_id, data);
/* self unregistering handler */
TEST_ESP_OK(esp_event_handler_unregister(s_test_base1, TEST_EVENT_BASE1_EV1, handler_id2));
/* register a new handler on id level */
TEST_ESP_OK(esp_event_handler_register(s_test_base1, TEST_EVENT_BASE1_EV1, handler_id, NULL));
}
/* This test executes the following steps:
* 1) register handler_id2 for a given event id and event base,
* 2) post an event to trigger the handler_id2,
* 3) unregister the handler_id2 during the execution of the handler_id2 and register
* handler_id instead.
* 4) call esp_event_dump and make sure by parsing the string IN PYTEST ENVIRONMENT that
* 1 handler profiling data is printed
* 5) unregister the handler_id successfully */
TEST_CASE("esp_event_dump does not show self unregistered handler", "[event][default]")
{
TEST_ESP_OK(esp_event_loop_create_default());
/* register handler for event base 1 and event id 1 */
TEST_ESP_OK(esp_event_handler_register(s_test_base1, TEST_EVENT_BASE1_EV1, handler_id2, NULL));
/* post an event on event base 1, event id 1 */
TEST_ESP_OK(esp_event_post(s_test_base1, TEST_EVENT_BASE1_EV1, NULL, 0, pdMS_TO_TICKS(1000)));
/* post an event 1 from base 1 and check the dump.
* - 1 handler invoked 0 times, exec time is 0 us */
esp_event_dump(stdout);
/* unregister handler id */
TEST_ESP_OK(esp_event_handler_unregister(s_test_base1, TEST_EVENT_BASE1_EV1, handler_id));
/* delete loop */
TEST_ESP_OK(esp_event_loop_delete_default());
}
static SemaphoreHandle_t s_event_mutex;
static StaticSemaphore_t s_event_mutex_buf;
static size_t s_handler_triggered = 0;
static void self_unregistering_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* data)
{
xSemaphoreTake(s_event_mutex, portMAX_DELAY);
/* self unregistering handler */
TEST_ESP_OK(esp_event_handler_unregister(s_test_base1, TEST_EVENT_BASE1_EV1, self_unregistering_handler));
s_handler_triggered++;
xSemaphoreGive(s_event_mutex);
}
/* This test makes sure that once a handler is unregistered, it is never called again.
* This test follows the update in esp event unregistration process where a handler can
* be marked as unregistered but not removed directly from the list it belongs to.
* The test creates a handler triggered by a specific combination of event base / event id
* and generates 2 consecutive events that should trigger the handler. The handler unregisters
* itself. Make sure that the second event does not trigger the handler again.
*
* Both event posts need to be queued before the unregistration happens, to make sure that event if
* the handler is still present in the handlers lists when the second event triggers, it will not
* be called. To make sure of that, the execution of the handler is blocked by a mutex released from
* test after the 2 events are posted. */
TEST_CASE("self unregistered handlers are never called again after they return", "[event][default]")
{
s_event_mutex = xSemaphoreCreateMutexStatic(&s_event_mutex_buf);
TEST_ASSERT_NOT_NULL(s_event_mutex);
esp_err_t ret = esp_event_loop_create_default();
printf("esp_event_loop_create_default %d\n", ret);
TEST_ESP_OK(ret);
/* register handler for event base 1 and event id 1 */
ret = esp_event_handler_register(s_test_base1, TEST_EVENT_BASE1_EV1, self_unregistering_handler, NULL);
printf("esp_event_handler_register %d\n", ret);
TEST_ESP_OK(ret);
/* take the mutex to block the execution of the self_unregistering_handler */
xSemaphoreTake(s_event_mutex, portMAX_DELAY);
/* post 2 times the event on event base 1, event id 1 */
ret = esp_event_post(s_test_base1, TEST_EVENT_BASE1_EV1, NULL, 0, pdMS_TO_TICKS(1000));
printf("esp_event_post %d\n", ret);
TEST_ESP_OK(ret);
ret = esp_event_post(s_test_base1, TEST_EVENT_BASE1_EV1, NULL, 0, pdMS_TO_TICKS(1000));
printf("esp_event_post %d\n", ret);
TEST_ESP_OK(ret);
/* release the mutex to execute the self_unregistering_handler */
xSemaphoreGive(s_event_mutex);
/* make sure the handler was called only once */
TEST_ASSERT(s_handler_triggered == 1);
/* delete mutex */
vSemaphoreDelete(s_event_mutex);
/* reset the static variable in case the test gets called once more */
s_handler_triggered = 0;
/* delete loop */
ret = esp_event_loop_delete_default();
printf("esp_event_loop_delete_default %d\n", ret);
TEST_ESP_OK(ret);
}
#endif // CONFIG_ESP_EVENT_LOOP_PROFILING

View File

@ -54,3 +54,16 @@ def test_esp_event_profiling(dut: Dut) -> None:
matches = dut.expect(r'HANDLER .+ inv:[1-9][0-9]{0,} time:[1-9][0-9]{0,} us', timeout=2)
matches_arr = matches.group().split(b'\r\n')
assert (len(matches_arr) == 3)
dut.expect('1 Tests 0 Failures 0 Ignored', timeout=120)
dut.expect_exact("Enter next test, or 'enter' to see menu")
dut.write('"esp_event_dump does not show self unregistered handler"')
# look for 1 handlers never invoked
matches = dut.expect(r'HANDLER .+ inv:0 time:0 us', timeout=2)
matches_arr = matches.group().split(b'\r\n')
assert (len(matches_arr) == 1)
dut.expect('1 Tests 0 Failures 0 Ignored', timeout=120)
dut.expect_exact("Enter next test, or 'enter' to see menu")
dut.write('"self unregistered handlers are never called again after they return"')
dut.expect('1 Tests 0 Failures 0 Ignored', timeout=120)