From 5e890b6a1f3a25ca562346f435e1697db9a81891 Mon Sep 17 00:00:00 2001
From: Andrey Saranchin <Andrey22102001@gmail.com>
Date: Wed, 28 Jun 2023 12:47:01 +0300
Subject: [PATCH] core: introduce event subsystem

The patch introduces new event subsystem. This subsystem is designed to
store user-defined triggers and has nothing in common with core triggers.

Each trigger has its own name and is represented by func_adapter.
Triggers are stored in events - named wrappers over rlist. Event objects
are opaque, hence rlist field should not be used directly - event
provides event_find_trigger, event_reset_triggers methods and
event_trigger_iterator. Iterator provides stable iteration and all the
non-deleted triggers will surely be traversed.

On way to the goal this patch also fixes include list in func_adapter.h.

Part of #8656

NO_CHANGELOG=internal
NO_DOC=internal
---
 src/lib/core/CMakeLists.txt |   1 +
 src/lib/core/event.c        | 337 +++++++++++++++++++++++++++
 src/lib/core/event.h        | 162 +++++++++++++
 src/lib/core/func_adapter.h |   3 +
 src/main.cc                 |   3 +
 test/unit/CMakeLists.txt    |   5 +
 test/unit/event.c           | 445 ++++++++++++++++++++++++++++++++++++
 7 files changed, 956 insertions(+)
 create mode 100644 src/lib/core/event.c
 create mode 100644 src/lib/core/event.h
 create mode 100644 test/unit/event.c

diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index 4f578e4c86..15803d9f58 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -46,6 +46,7 @@ set(core_sources
     cord_on_demand.cc
     tweaks.c
     tt_sort.c
+    event.c
 )
 
 if (ENABLE_BACKTRACE)
diff --git a/src/lib/core/event.c b/src/lib/core/event.c
new file mode 100644
index 0000000000..1d97701388
--- /dev/null
+++ b/src/lib/core/event.c
@@ -0,0 +1,337 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include "event.h"
+
+#include <assert.h>
+
+#include "assoc.h"
+#include "diag.h"
+#include "trivia/util.h"
+#include "func_adapter.h"
+
+/** Registry of all events: name -> event. */
+static struct mh_strnptr_t *event_registry;
+
+/**
+ * A named node of the list, containing func_adapter. Every event_trigger is
+ * associated with an event. Since event_triggers have completely different
+ * call interface, they are not inherited from core triggers.
+ */
+struct event_trigger {
+	/** A link in a list of all registered event triggers. */
+	struct rlist link;
+	/** Trigger function. */
+	struct func_adapter *func;
+	/** Backlink to event. Needed for reference count of event. */
+	struct event *event;
+	/** Unique name of the trigger. */
+	char *name;
+	/** Trigger reference counter. */
+	uint32_t ref_count;
+	/** This flag is set when the trigger is deleted. */
+	bool is_deleted;
+};
+
+/**
+ * Creates event_trigger from func_adapter and name and binds it to passed
+ * event. Created trigger owns the adapter and will destroy it in its
+ * destructor. Passed name must be zero terminated and will be copied.
+ * Passed event must not be NULL and will be referenced by created trigger.
+ * Note that the function does not increment trigger_count field of the event.
+ */
+static struct event_trigger *
+event_trigger_new(struct func_adapter *func, struct event *event,
+		  const char *name)
+{
+	assert(name != NULL);
+	assert(event != NULL);
+	size_t name_len = strlen(name);
+	struct event_trigger *trigger =
+		xmalloc(sizeof(*trigger) + name_len + 1);
+	trigger->func = func;
+	trigger->name = (char *)(trigger + 1);
+	strlcpy(trigger->name, name, name_len + 1);
+	trigger->ref_count = 0;
+	trigger->is_deleted = false;
+	trigger->event = event;
+	event_ref(event);
+	rlist_create(&trigger->link);
+	return trigger;
+}
+
+/**
+ * Destroys an event_trigger. Underlying func_adapter is destroyed,
+ * event_trigger is deallocated.
+ */
+static void
+event_trigger_delete(struct event_trigger *trigger)
+{
+	assert(trigger->func != NULL);
+	assert(trigger->event != NULL);
+	rlist_del(&trigger->link);
+	event_unref(trigger->event);
+	func_adapter_destroy(trigger->func);
+	TRASH(trigger);
+	free(trigger);
+}
+
+/**
+ * Increments event_trigger reference counter.
+ */
+static void
+event_trigger_ref(struct event_trigger *trigger)
+{
+	trigger->ref_count++;
+}
+
+/**
+ * Decrements event_trigger reference counter. The event_trigger is destroyed
+ * if the counter reaches zero.
+ */
+static void
+event_trigger_unref(struct event_trigger *trigger)
+{
+	assert(trigger != NULL);
+	assert(trigger->ref_count > 0);
+	trigger->ref_count--;
+	assert(trigger->ref_count > 0 || trigger->is_deleted);
+	if (trigger->ref_count == 0)
+		event_trigger_delete(trigger);
+}
+
+/**
+ * Allocates, initializes a new event and inserts it to event registry.
+ * Passed name will be copied.
+ */
+static struct event *
+event_new(const char *name, size_t name_len)
+{
+	assert(name != NULL);
+	assert(name_len > 0);
+	struct event *event = xmalloc(sizeof(*event) + name_len + 1);
+	rlist_create(&event->triggers);
+	event->ref_count = 0;
+	event->trigger_count = 0;
+	event->name = (char *)(event + 1);
+	strlcpy(event->name, name, name_len + 1);
+
+	struct mh_strnptr_t *h = event_registry;
+	name = event->name;
+	uint32_t name_hash = mh_strn_hash(name, name_len);
+	struct mh_strnptr_node_t node = {name, name_len, name_hash, event};
+	struct mh_strnptr_node_t prev;
+	struct mh_strnptr_node_t *prev_ptr = &prev;
+	mh_int_t pos = mh_strnptr_put(h, &node, &prev_ptr, NULL);
+	(void)pos;
+	assert(pos != mh_end(h));
+	assert(prev_ptr == NULL);
+
+	return event;
+}
+
+/**
+ * Destroys an event and removes it from event registry.
+ * Underlying trigger list is destroyed.
+ */
+void
+event_delete(struct event *event)
+{
+	assert(event != NULL);
+
+	struct mh_strnptr_t *h = event_registry;
+	const char *name = event->name;
+	size_t name_len = strlen(name);
+	mh_int_t i = mh_strnptr_find_str(h, name, name_len);
+	assert(i != mh_end(h));
+	assert(mh_strnptr_node(h, i)->val == event);
+	mh_strnptr_del(h, i, 0);
+	TRASH(event);
+	free(event);
+}
+
+/**
+ * Finds internal event_trigger entity by name in the event.
+ * All arguments must not be NULL.
+ */
+static struct event_trigger *
+event_find_trigger_internal(struct event *event, const char *name)
+{
+	struct event_trigger *curr = NULL;
+	rlist_foreach_entry(curr, &event->triggers, link) {
+		if (!curr->is_deleted && strcmp(curr->name, name) == 0)
+			return curr;
+	}
+	return NULL;
+}
+
+struct func_adapter *
+event_find_trigger(struct event *event, const char *name)
+{
+	struct event_trigger *trigger =
+		event_find_trigger_internal(event, name);
+	return trigger != NULL ? trigger->func : NULL;
+}
+
+void
+event_reset_trigger(struct event *event, const char *name,
+		    struct func_adapter *new_trigger)
+{
+	assert(event != NULL);
+	assert(name != NULL);
+	struct event_trigger *found_trigger =
+		event_find_trigger_internal(event, name);
+	if (new_trigger != NULL) {
+		event->trigger_count++;
+		struct event_trigger *trigger =
+			event_trigger_new(new_trigger, event, name);
+		event_trigger_ref(trigger);
+		if (found_trigger == NULL) {
+			rlist_add_entry(&event->triggers, trigger, link);
+		} else {
+			/*
+			 * Insert new trigger before the replaced one not to
+			 * iterate over both of them in the case when an
+			 * iterator points to a replaced trigger.
+			 */
+			rlist_add_tail_entry(&found_trigger->link,
+					     trigger, link);
+		}
+	}
+	if (found_trigger != NULL) {
+		assert(event->trigger_count > 0);
+		event->trigger_count--;
+		found_trigger->is_deleted = true;
+		event_trigger_unref(found_trigger);
+	}
+}
+
+void
+event_trigger_iterator_create(struct event_trigger_iterator *it,
+			      struct event *event)
+{
+	event_ref(event);
+	it->event = event;
+	it->curr = &event->triggers;
+}
+
+bool
+event_trigger_iterator_next(struct event_trigger_iterator *it,
+			    struct func_adapter **func, const char **name)
+{
+	assert(func != NULL);
+	assert(name != NULL);
+
+	*func = NULL;
+	*name = NULL;
+	/* Iterator is exhausted - return. */
+	if (it->curr == NULL)
+		return false;
+	struct event_trigger *trigger;
+	struct rlist *old = it->curr;
+	/* We need to skip all the deleted triggers. */
+	do {
+		it->curr = rlist_next(it->curr);
+		trigger = rlist_entry(it->curr, struct event_trigger, link);
+		/* We have traversed the whole list. */
+		if (it->curr == &it->event->triggers) {
+			it->curr = NULL;
+			trigger = NULL;
+			goto release;
+		}
+	} while (trigger->is_deleted);
+	assert(trigger != NULL);
+	event_trigger_ref(trigger);
+release:
+	if (old != NULL && old != &it->event->triggers) {
+		struct event_trigger *old_trigger =
+			rlist_entry(old, struct event_trigger, link);
+		assert(old_trigger != trigger);
+		event_trigger_unref(old_trigger);
+	}
+	if (trigger != NULL) {
+		*func = trigger->func;
+		*name = trigger->name;
+		return true;
+	}
+	return false;
+}
+
+void
+event_trigger_iterator_destroy(struct event_trigger_iterator *it)
+{
+	if (it->curr != NULL && it->curr != &it->event->triggers) {
+		struct event_trigger *curr_trigger =
+			rlist_entry(it->curr, struct event_trigger, link);
+		event_trigger_unref(curr_trigger);
+	}
+	event_unref(it->event);
+	it->event = NULL;
+	it->curr = NULL;
+}
+
+struct event *
+event_get(const char *name, bool create_if_not_exist)
+{
+	assert(event_registry != NULL);
+	assert(name != NULL);
+	struct mh_strnptr_t *h = event_registry;
+	uint32_t name_len = strlen(name);
+	mh_int_t i = mh_strnptr_find_str(h, name, name_len);
+	if (i != mh_end(h))
+		return mh_strnptr_node(h, i)->val;
+	else if (!create_if_not_exist)
+		return NULL;
+
+	struct event *event = event_new(name, name_len);
+	return event;
+}
+
+bool
+event_foreach(event_foreach_f cb, void *arg)
+{
+	struct mh_strnptr_t *h = event_registry;
+	mh_int_t i;
+	mh_foreach(h, i) {
+		struct mh_strnptr_node_t *node = mh_strnptr_node(h, i);
+		struct event *event = node->val;
+		if (!event_has_triggers(event))
+			continue;
+		if (!cb(event, arg))
+			return false;
+	}
+	return true;
+}
+
+void
+event_init(void)
+{
+	event_registry = mh_strnptr_new();
+}
+
+void
+event_free(void)
+{
+	assert(event_registry != NULL);
+	struct mh_strnptr_t *h = event_registry;
+	mh_int_t i;
+	mh_foreach(h, i) {
+		struct event *event = mh_strnptr_node(h, i)->val;
+		/*
+		 * If the only thing that holds the event is its trigger list,
+		 * the reference counter will reach zero when the list will be
+		 * cleared and the destructor will be called. Since we will
+		 * call destructor manually, let's reference the event in order
+		 * to prevent such situation.
+		 */
+		event_ref(event);
+		struct event_trigger *trigger, *tmp;
+		rlist_foreach_entry_safe(trigger, &event->triggers, link, tmp)
+			event_trigger_delete(trigger);
+		event_delete(event);
+	}
+	mh_strnptr_delete(h);
+	event_registry = NULL;
+}
diff --git a/src/lib/core/event.h b/src/lib/core/event.h
new file mode 100644
index 0000000000..098c64d9da
--- /dev/null
+++ b/src/lib/core/event.h
@@ -0,0 +1,162 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2023, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#pragma once
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "small/rlist.h"
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct func_adapter;
+
+/**
+ * List of triggers registered on event identified by name.
+ */
+struct event {
+	/** List of triggers. */
+	struct rlist triggers;
+	/** Name of event. */
+	char *name;
+	/** Reference count. */
+	uint32_t ref_count;
+	/** Number of triggers. */
+	uint32_t trigger_count;
+};
+
+/**
+ * Destroys an event and removes it from event registry.
+ * NB: The method is private and must not be called manually.
+ */
+void
+event_delete(struct event *event);
+
+/**
+ * Increments event reference counter.
+ */
+static inline void
+event_ref(struct event *event)
+{
+	event->ref_count++;
+}
+
+/**
+ * Decrements event reference counter. The event is destroyed if the counter
+ * reaches zero.
+ */
+static inline void
+event_unref(struct event *event)
+{
+	assert(event->ref_count > 0);
+	event->ref_count--;
+	if (event->ref_count == 0) {
+		assert(rlist_empty(&event->triggers));
+		assert(event->trigger_count == 0);
+		event_delete(event);
+	}
+}
+
+/**
+ * Checks if the event has no triggers.
+ */
+static inline bool
+event_has_triggers(struct event *event)
+{
+	return event->trigger_count > 0;
+}
+
+/**
+ * Finds a trigger by name in an event. All the arguments must not be NULL.
+ */
+struct func_adapter *
+event_find_trigger(struct event *event, const char *name);
+
+/**
+ * Resets trigger by name in an event.
+ * Arguments event and name must not be NULL.
+ * If new_trigger is NULL, the function removes a trigger by name from the
+ * event. Otherwise, it replaces trigger by name or inserts it in the beginning
+ * of the underlying list of event.
+ */
+void
+event_reset_trigger(struct event *event, const char *name,
+		    struct func_adapter *new_trigger);
+
+/**
+ * Iterator over triggers from event. Never invalidates.
+ */
+struct event_trigger_iterator {
+	/**
+	 * Current element in the list of triggers.
+	 * Becomes NULL when the iterator is exhausted.
+	 */
+	struct rlist *curr;
+	/** Underlying event. */
+	struct event *event;
+};
+
+/**
+ * Initializes iterator.
+ */
+void
+event_trigger_iterator_create(struct event_trigger_iterator *it,
+			      struct event *event);
+
+/**
+ * Advances iterator. Output arguments trigger and name must not be NULL.
+ */
+bool
+event_trigger_iterator_next(struct event_trigger_iterator *it,
+			    struct func_adapter **trigger, const char **name);
+
+/**
+ * Deinitializes iterator. Does not free memory.
+ */
+void
+event_trigger_iterator_destroy(struct event_trigger_iterator *it);
+
+/**
+ * Finds an event by its name. Name must be a zero-terminated string.
+ * Creates new event and inserts it to registry if there is no event with such
+ * name when flag create_if_not_exist is true.
+ */
+struct event *
+event_get(const char *name, bool create_if_not_exist);
+
+typedef bool
+event_foreach_f(struct event *event, void *arg);
+
+/**
+ * Invokes a callback for each registered event with no particular order.
+ *
+ * The callback is passed an event object and the given argument.
+ * If it returns true, iteration continues. Otherwise, iteration breaks, and
+ * the function returns false.
+ *
+ * Empty events are guaranteed to be skipped.
+ */
+bool
+event_foreach(event_foreach_f cb, void *arg);
+
+/**
+ * Initializes event submodule.
+ */
+void
+event_init(void);
+
+/**
+ * Frees event submodule.
+ */
+void
+event_free(void);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/lib/core/func_adapter.h b/src/lib/core/func_adapter.h
index 29490d9627..8ace5cefed 100644
--- a/src/lib/core/func_adapter.h
+++ b/src/lib/core/func_adapter.h
@@ -5,10 +5,13 @@
  */
 #pragma once
 
+#include <assert.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <string.h>
 
+#include "trivia/util.h"
+
 #ifdef __cplusplus
 extern "C" {
 #endif
diff --git a/src/main.cc b/src/main.cc
index 7aca7658a5..ca72f3e356 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -88,6 +88,7 @@
 #include "core/errinj.h"
 #include "core/clock_lowres.h"
 #include "lua/utils.h"
+#include "core/event.h"
 
 static pid_t master_pid = getpid();
 static struct pidfh *pid_file_handle;
@@ -592,6 +593,7 @@ tarantool_free(void)
 	session_free();
 	user_cache_free();
 #endif
+	event_free();
 	ssl_free();
 	memtx_tx_manager_free();
 	coll_free();
@@ -875,6 +877,7 @@ main(int argc, char **argv)
 	memtx_tx_manager_init();
 	module_init();
 	ssl_init();
+	event_init();
 	systemd_init();
 
 	const int override_cert_paths_env_vars = 0;
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index c18ef408e1..4c9729026b 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -630,3 +630,8 @@ create_unit_test(PREFIX lua_func_adapter
                            ${ICU_LIBRARIES}
                            ${LUAJIT_LIBRARIES}
 )
+
+create_unit_test(PREFIX event
+                 SOURCES event.c core_test_utils.c
+                 LIBRARIES core unit
+)
diff --git a/test/unit/event.c b/test/unit/event.c
new file mode 100644
index 0000000000..d5aba3e6a6
--- /dev/null
+++ b/test/unit/event.c
@@ -0,0 +1,445 @@
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "diag.h"
+#include "error.h"
+#include "errcode.h"
+#include "fiber.h"
+#include "func_adapter.h"
+#include "memory.h"
+#include "trivia/util.h"
+#include "event.h"
+#include "tt_static.h"
+
+#define UNIT_TAP_COMPATIBLE 1
+#include "unit.h"
+
+static int func_destroy_count;
+
+/**
+ * Virtual destructor for func_adapter for the test purposes.
+ */
+void
+func_destroy(struct func_adapter *func)
+{
+	func_destroy_count++;
+}
+
+/**
+ * The test creates event with different names and check if all the basic
+ * operations works correctly.
+ */
+static void
+test_basic(void)
+{
+	plan(4 * 14);
+
+	struct func_adapter_vtab vtab = {.destroy = func_destroy};
+	struct func_adapter func = {.vtab = &vtab};
+	const char *trg_name = "my_triggers.trg[1]";
+	const char *names[] = {
+		"name",
+		"name with spaces",
+		"namespace.name",
+		"NAMESPACE[123].name"
+	};
+	for (size_t i = 0; i < lengthof(names); ++i) {
+		const char *name = names[i];
+		struct event *event = event_get(name, false);
+		is(event, NULL, "No such event - NULL must be returned");
+		event = event_get(name, true);
+		isnt(event, NULL, "Event must be created");
+		/* Reference event to prevent deletion when it'll be empty. */
+		event_ref(event);
+		struct event *found_event = event_get(name, false);
+		is(found_event, event, "Existing event must be found");
+		struct func_adapter *old = event_find_trigger(event, trg_name);
+		is(old, NULL, "No such trigger - NULL must be returned");
+		ok(!event_has_triggers(event), "Created event must be empty");
+		event_reset_trigger(event, trg_name, &func);
+		found_event = event_get(name, false);
+		is(found_event, event, "Event must still exist");
+		old = event_find_trigger(event, trg_name);
+		is(old, &func, "New trigger must be found");
+		ok(event_has_triggers(event), "Event must not be empty");
+		is(func_destroy_count, 0, "Func must not be destroyed yet")
+		event_reset_trigger(event, trg_name, NULL);
+		is(func_destroy_count, 1, "Func must be destroyed")
+		old = event_find_trigger(event, trg_name);
+		is(old, NULL, "Deleted trigger must not be found");
+		ok(!event_has_triggers(event), "Event must be empty");
+		found_event = event_get(name, false);
+		is(found_event, event, "Referenced event must not be deleted");
+		event_unref(event);
+		found_event = event_get(name, false);
+		is(found_event, NULL, "Empty unused event must be deleted");
+		func_destroy_count = 0;
+	}
+	check_plan();
+}
+
+/**
+ * A test argument for event_foreach function.
+ */
+struct test_event_foreach_arg {
+	int names_len;
+	const char **names;
+	int traversed;
+};
+
+static bool
+test_event_foreach_f(struct event *event, void *arg)
+{
+	struct test_event_foreach_arg *data =
+		(struct test_event_foreach_arg *)arg;
+	data->traversed++;
+	bool name_found = false;
+	for (int i = 0; i < data->names_len && !name_found; ++i) {
+		name_found = strcmp(event->name, data->names[i]) == 0;
+	}
+	ok(name_found, "Traversed event must really exist");
+	return true;
+}
+
+static bool
+test_event_foreach_return_false_f(struct event *event, void *arg)
+{
+	(void)event;
+	struct test_event_foreach_arg *data =
+		(struct test_event_foreach_arg *)arg;
+	data->traversed++;
+	return false;
+}
+
+static void
+test_event_foreach(void)
+{
+	plan(10);
+	const char *names[] = {
+		"event",
+		"my_events.event1",
+		"my_events.event3",
+		"my_events[15].event"
+	};
+	struct func_adapter_vtab vtab = {.destroy = func_destroy};
+	struct func_adapter func = {.vtab = &vtab};
+	for (size_t i = 0; i < lengthof(names); ++i) {
+		struct event *event = event_get(names[i], true);
+		event_ref(event);
+		const char *trg_name = tt_sprintf("%zu", i);
+		event_reset_trigger(event, trg_name, &func);
+	}
+
+	struct test_event_foreach_arg arg = {
+		.names_len = lengthof(names),
+		.names = names,
+		.traversed = 0,
+	};
+
+	bool ok = event_foreach(test_event_foreach_f, &arg);
+	ok(ok, "Traversal must return true");
+	is(arg.traversed, lengthof(names), "All the events must be traversed");
+
+	arg.traversed = 0;
+	ok = event_foreach(test_event_foreach_return_false_f, &arg);
+	ok(!ok, "Failed traversal must return false");
+	is(arg.traversed, 1, "Only one event must be traversed");
+
+	for (size_t i = 0; i < lengthof(names); ++i) {
+		struct event *event = event_get(names[i], false);
+		const char *trg_name = tt_sprintf("%zu", i);
+		event_reset_trigger(event, trg_name, NULL);
+	}
+
+	arg.traversed = 0;
+	ok = event_foreach(test_event_foreach_f, &arg);
+	ok(ok, "Traversal of empty registry must return true");
+	is(arg.traversed, 0, "All the events are empty - nothing to traverse");
+
+	/* Unreference all the events. */
+	for (size_t i = 0; i < lengthof(names); ++i) {
+		struct event *event = event_get(names[i], false);
+		fail_if(event == NULL);
+		event_unref(event);
+	}
+
+	check_plan();
+}
+
+static void
+test_event_trigger_iterator(void)
+{
+	plan(14);
+	const char *event_name = "test_event";
+	const char *trigger_names[] = {
+		"0", "1", "2", "3", "4", "5", "6", "7"
+	};
+	struct func_adapter_vtab vtab = {.destroy = func_destroy};
+	struct func_adapter func = {.vtab = &vtab};
+
+	struct event *event = event_get(event_name, true);
+	for (int i = lengthof(trigger_names) - 1; i >= 0; --i)
+		event_reset_trigger(event, trigger_names[i], &func);
+
+	struct event_trigger_iterator it;
+	event_trigger_iterator_create(&it, event);
+	size_t idx = 0;
+	struct func_adapter *curr_trg = NULL;
+	const char *curr_name = NULL;
+	while (event_trigger_iterator_next(&it, &curr_trg, &curr_name)) {
+		is(strcmp(curr_name, trigger_names[idx]), 0,
+		   "Triggers must be traversed in reversed order");
+		idx++;
+	}
+	is(idx, lengthof(trigger_names), "All the triggers must be traversed");
+	is(curr_trg, NULL, "Exhausted iterator must return NULL trigger");
+	is(curr_name, NULL, "Exhausted iterator must return NULL name");
+
+	curr_trg = &func;
+	curr_name = "garbage";
+	bool has_elems =
+		event_trigger_iterator_next(&it, &curr_trg, &curr_name);
+	ok(!has_elems, "Iterator must stay exhausted");
+	is(curr_trg, NULL, "Exhausted iterator must return NULL trigger");
+	is(curr_name, NULL, "Exhausted iterator must return NULL name");
+
+	for (size_t i = 0; i < lengthof(trigger_names); ++i)
+		event_reset_trigger(event, trigger_names[i], NULL);
+
+	event_trigger_iterator_destroy(&it);
+
+	check_plan();
+}
+
+/**
+ * Stops at breakpoint and deletes triggers which are set in del mask.
+ */
+static void
+test_event_iterator_stability_del_step(int breakpoint, const char *del_mask,
+				       int trigger_num)
+{
+	fail_unless(breakpoint < trigger_num);
+	size_t left_after_br = 0;
+	for (int i = breakpoint + 1; i < trigger_num; ++i) {
+		if (del_mask[i] == 0)
+			left_after_br++;
+	}
+	plan((breakpoint + 1) * 2 + left_after_br + 3);
+
+	const char *event_name = "test_event";
+	struct func_adapter_vtab vtab = {.destroy = func_destroy};
+	struct func_adapter func = {.vtab = &vtab};
+
+	struct event *event = event_get(event_name, true);
+	/*
+	 * Reference the event to prevent deletion for the test cases that
+	 * delete all the triggers from the event.
+	 */
+	event_ref(event);
+	for (int i = trigger_num - 1; i >= 0; --i) {
+		const char *trg_name = tt_sprintf("%d", i);
+		event_reset_trigger(event, trg_name, &func);
+	}
+
+	struct event_trigger_iterator it;
+	event_trigger_iterator_create(&it, event);
+	struct func_adapter *trg = NULL;
+	const char *name = NULL;
+	for (int i = 0; i <= breakpoint; i++) {
+		bool ok = event_trigger_iterator_next(&it, &trg, &name);
+		ok(ok, "Iterator must not be exhausted yet")
+		const char *trg_name = tt_sprintf("%d", i);
+		is(strcmp(name, trg_name), 0,
+		   "Triggers must be traversed in reversed order");
+	}
+	bool delete_all_triggers = true;
+	for (int i = 0; i < trigger_num; ++i) {
+		if (del_mask[i] != 0) {
+			const char *trg_name = tt_sprintf("%d", i);
+			event_reset_trigger(event, trg_name, NULL);
+		} else {
+			delete_all_triggers = false;
+		}
+	}
+	is(event_has_triggers(event), !delete_all_triggers,
+	   "Function event_has_triggers must work correctly");
+	for (size_t i = 0; i < left_after_br; ++i) {
+		bool ok = event_trigger_iterator_next(&it, &trg, &name);
+		ok(ok, "Traversal must continue");
+	}
+
+	bool ok = event_trigger_iterator_next(&it, &trg, &name);
+	ok(!ok, "Iterator must be exhausted");
+	event_trigger_iterator_destroy(&it);
+	is(event_has_triggers(event), !delete_all_triggers,
+	   "Function event_has_triggers must work correctly");
+
+	for (int i = 0; i < trigger_num; ++i) {
+		if (del_mask[i] != 0)
+			continue;
+		const char *trg_name = tt_sprintf("%d", i);
+		event_reset_trigger(event, trg_name, NULL);
+	}
+	event_unref(event);
+
+	check_plan();
+}
+
+/**
+ * Stops at breakpoint and replaces triggers which are set in replace mask.
+ */
+static void
+test_event_iterator_stability_replace_step(int breakpoint,
+					   const char *replace_mask,
+					   int trigger_num)
+{
+	fail_unless(breakpoint < trigger_num);
+	plan((breakpoint + 1) * 2 + 3 * (trigger_num - breakpoint - 1) + 3);
+
+	const char *event_name = "test_event";
+	struct func_adapter_vtab vtab = {.destroy = func_destroy};
+	struct func_adapter func = {.vtab = &vtab};
+	struct func_adapter new_func = {.vtab = &vtab};
+
+	struct event *event = event_get(event_name, true);
+	for (int i = trigger_num - 1; i >= 0; --i) {
+		const char *trg_name = tt_sprintf("%d", i);
+		event_reset_trigger(event, trg_name, &func);
+	}
+
+	struct event_trigger_iterator it;
+	event_trigger_iterator_create(&it, event);
+	struct func_adapter *trg = NULL;
+	const char *name = NULL;
+	for (int i = 0; i <= breakpoint; i++) {
+		bool ok = event_trigger_iterator_next(&it, &trg, &name);
+		ok(ok, "Iterator must not be exhausted yet")
+		const char *trg_name = tt_sprintf("%d", i);
+		is(strcmp(name, trg_name), 0,
+		   "Triggers must be traversed in reversed order");
+	}
+	for (int i = 0; i < trigger_num; ++i) {
+		if (replace_mask[i] != 0) {
+			const char *trg_name = tt_sprintf("%d", i);
+			event_reset_trigger(event, trg_name, &new_func);
+		}
+	}
+	ok(event_has_triggers(event), "Event must not be empty");
+	for (int i = breakpoint + 1; i < trigger_num; ++i) {
+		bool ok = event_trigger_iterator_next(&it, &trg, &name);
+		ok(ok, "Traversal must continue")
+		const char *trg_name = tt_sprintf("%d", i);
+		is(strcmp(name, trg_name), 0,
+		   "Triggers must be traversed in reversed order");
+		if (replace_mask[i] != 0) {
+			is(trg, &new_func, "Trigger must be replaced");
+		} else {
+			is(trg, &func, "Trigger must be old");
+		}
+	}
+	bool ok = event_trigger_iterator_next(&it, &trg, &name);
+	ok(!ok, "Iterator must be exhausted");
+	event_trigger_iterator_destroy(&it);
+	ok(event_has_triggers(event), "Event must not be empty");
+
+	for (int i = 0; i < trigger_num; ++i) {
+		const char *trg_name = tt_sprintf("%d", i);
+		event_reset_trigger(event, trg_name, NULL);
+	}
+
+	check_plan();
+}
+
+/**
+ * Checks if iteration is stable in the cases of deletions and replaces.
+ */
+static void
+test_event_trigger_iterator_stability(void)
+{
+	plan(6);
+	char mask[8];
+	const size_t trigger_num = lengthof(mask);
+	memset(mask, 0, trigger_num);
+	size_t br = trigger_num / 2;
+	/**
+	 * Delete or replace current trigger.
+	 */
+	mask[br] = 1;
+	test_event_iterator_stability_del_step(br, mask, trigger_num);
+	test_event_iterator_stability_replace_step(br, mask, trigger_num);
+	memset(mask, 0, trigger_num);
+	/**
+	 * Delete or replace current, previous and next triggers.
+	 */
+	mask[br - 1] = 1;
+	mask[br] = 1;
+	mask[br + 1] = 1;
+	test_event_iterator_stability_del_step(br, mask, trigger_num);
+	test_event_iterator_stability_replace_step(br, mask, trigger_num);
+	memset(mask, 0, trigger_num);
+	/**
+	 * Delete or replace all the triggers in the middle of iteration.
+	 */
+	memset(mask, 1, trigger_num);
+	test_event_iterator_stability_del_step(br, mask, trigger_num);
+	test_event_iterator_stability_replace_step(br, mask, trigger_num);
+	memset(mask, 0, trigger_num);
+
+	check_plan();
+}
+
+static void
+test_event_free(void)
+{
+	plan(1);
+
+	struct func_adapter_vtab vtab = {.destroy = func_destroy};
+	struct func_adapter func = {.vtab = &vtab};
+	const char *trigger_names[] = {
+		"trigger[1]",
+		"trigger.second",
+		"another_trigger"
+	};
+	const char *event_names[] = {
+		"name",
+		"name with spaces",
+		"namespace.name",
+		"NAMESPACE[123].name"
+	};
+	func_destroy_count = 0;
+	for (size_t i = 0; i < lengthof(event_names); ++i) {
+		const char *name = event_names[i];
+		struct event *event = event_get(name, true);
+		for (size_t j = 0; j < lengthof(trigger_names); ++j)
+			event_reset_trigger(event, trigger_names[j], &func);
+	}
+	event_free();
+	is(func_destroy_count, lengthof(event_names) * lengthof(trigger_names),
+	   "All triggers must be destroyed");
+	/* Initialize event back. */
+	event_init();
+
+	check_plan();
+}
+
+static int
+test_main(void)
+{
+	plan(5);
+	test_basic();
+	test_event_foreach();
+	test_event_trigger_iterator();
+	test_event_trigger_iterator_stability();
+	test_event_free();
+	return check_plan();
+}
+
+int
+main(void)
+{
+	event_init();
+	int rc = test_main();
+	event_free();
+	return rc;
+}
-- 
GitLab