From 0e8df1c7e48bcae2285c7c138bd50f932049bd24 Mon Sep 17 00:00:00 2001 From: Neels Hofmeyr Date: Mon, 11 Feb 2019 20:32:25 +0100 Subject: [PATCH] add osmo_use_count API Provide a common implementation of use counting that supports naming each user as well as counting more than just one use per user, depending on the rules the caller implies. In osmo-msc, we were originally using a simple int counter to see whether a connection is still in use or should be discarded. For clarity, we later added names to each user in the form of a bitmask of flags, to figure out exactly which users are still active: for logging and to debug double get / double put bugs. This however is still not adequate, since there may be more than one CM Service Request pending. Also, it is a specialized implementation that is not re-usable. With this generalized implementation, we can: - fix the problem of inadequate counting of multiple concurrent CM Service Requests (more than one use count per user category), - directly use arbitrary names for uses like __func__ or "foo" (no need to define enums and value_string[]s), - re-use the same code for e.g. vlr_subscr and get fairly detailed VLR susbscriber usage logging for free. Change-Id: Ife31e6798b4e728a23913179e346552a7dd338c0 --- include/Makefile.am | 1 + include/osmocom/core/use_count.h | 228 +++++++++++++++++++ src/Makefile.am | 1 + src/use_count.c | 279 ++++++++++++++++++++++++ tests/Makefile.am | 5 + tests/testsuite.at | 7 + tests/use_count/use_count_test.c | 339 +++++++++++++++++++++++++++++ tests/use_count/use_count_test.err | 171 +++++++++++++++ tests/use_count/use_count_test.ok | 0 9 files changed, 1031 insertions(+) create mode 100644 include/osmocom/core/use_count.h create mode 100644 src/use_count.c create mode 100644 tests/use_count/use_count_test.c create mode 100644 tests/use_count/use_count_test.err create mode 100644 tests/use_count/use_count_test.ok diff --git a/include/Makefile.am b/include/Makefile.am index 6ed7fe676..7b9e3479f 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -53,6 +53,7 @@ nobase_include_HEADERS = \ osmocom/core/utils.h \ osmocom/core/write_queue.h \ osmocom/core/sockaddr_str.h \ + osmocom/core/use_count.h \ osmocom/crypt/auth.h \ osmocom/crypt/gprs_cipher.h \ osmocom/ctrl/control_cmd.h \ diff --git a/include/osmocom/core/use_count.h b/include/osmocom/core/use_count.h new file mode 100644 index 000000000..6a4bf1f34 --- /dev/null +++ b/include/osmocom/core/use_count.h @@ -0,0 +1,228 @@ +/*! \file use_count.h + * Generic object usage counter API (get, put and deallocate on zero count). + */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +#include + +/*! \defgroup use_count Use Counter + * @{ + * \file use_count.h + */ + +struct osmo_use_count_entry; + +/*! Invoked when a use count changes. + * + * The implementation is free to trigger actions on arbitrary use count changes, typically to free the + * use_count->talloc_object when the total use count reaches zero. + * + * The implementation may modify use_count_entry->count, for example for handling of get()/put() bugs, to clamp specific use + * tokens to specific counts, or to prevent the caller from put()ting into negative counts. When returning an error, + * there is no implicit undo -- if errors need to be corrected, this function is responsible for that. + * + * Be aware: use token strings are not copied, and use count entries usually remain listed also when they reach a zero + * count. This is trivially perfectly ok when using string literals as use tokens. It is also possible to use + * dynamically allocated string tokens, but should a use token string become invalid memory when reaching zero count, it + * is the responsibility of this function to set the use_count_entry->use = NULL; this is required to avoid subsequent + * osmo_use_count_get_put() invocations from calling strcmp() on invalid memory. (Setting use = NULL cannot be done + * implicitly after this callback invocation, because callback implementations are allowed to completely deallocate the + * talloc_object and the use_count list entries, and setting use = NULL after that would be a use-after-free.) + * + * \param[in] use_count_entry Use count entry that is being modified. + * \param[in] old_use_count Use count the item had before the change in use count. + * \param[in] file Source file string, passed in as __FILE__ from macro osmo_use_count_get_put(). + * \param[in] line Source file line, passed in as __LINE__ from macro osmo_use_count_get_put(). + * \return 0 on success, negative if any undesired use count is reached; this rc will be returned by + * osmo_use_count_get_put(). + */ +typedef int (* osmo_use_count_cb_t )(struct osmo_use_count_entry *use_count_entry, int32_t old_use_count, + const char *file, int line); + +/*! Use counter state for one used object, managing N distinct named counters. + * + * Manage any number of uses of an object, with name tokens given to each use. + * + * A typical use tracking done by a single instance of this struct may look like: + * "VLR subscr MSISDN-23 + SMS-receiver: now used by 6 (attached,2*SMS-receiver,SMS-pending,SMS,Paging)" + * (This is a DREF log statement from an osmo-msc run delivering an SMS.) + * + * Use tokens are given as const char* strings. Typically string literals like "foo", __func__, or also NULL. Tokens may + * be dynamically allocated or static char[] buffers as long as they are guaranteed to remain unchanged while referenced + * by an osmo_use_count_entry. (Breakage occurs if one token magically changes to equal another listed token.) + * + * Instead of using string literals in the code directly, callers should use a #define, so that typos are caught at + * compile time rather than introducing obscure failures that are hard to spot for humans -- don't use foo_get("bar") + * and foo_put("bar"), but '#define FOO_USE_BAR "bar"' for foo_get(FOO_USE_BAR) and foo_put(FOO_USE_BAR). + * + * Counts are int32_t values, a separate count per use token string. Counts can be negative, though in the typical use + * case are only positive or 0. Enforcing a range is entirely up to the osmo_use_count_cb_t() implementation. + * + * The talloc_object must be a pointer eligible to be a talloc context, i.e. either obtained from a function like + * talloc_zero() or NULL. talloc_object is typically a pointer to the object that this struct is a member of. Use count + * entries may be allocated as talloc children of this (see also "Avoiding dynamic allocation" below). + * + * The use_cb() implementation allows to trigger actions when reaching specific use counts, e.g. deallocate when + * reaching a total sum across all use tokens of zero. + * + * On initialization, this struct can be left fully zero initialized (the llist_head use_counts is implicitly + * initialized upon the first osmo_use_count_get_put()). Usually, set only a talloc_object and a use_cb, though neither + * is strictly required. + * + * Avoiding dynamic allocation: dynamic allocation can be avoided completely by providing sufficient static use count + * entries with osmo_use_count_make_static_entries(). Otherwise, each new use token will dynamically allocate a new + * osmo_use_count_entry; note that once allocated, these entries stay around even if they reached an entry count of + * zero, and will be re-used for subsequent use count tokens. So even if not using osmo_use_count_make_static_entries(), + * each osmo_use_count will keep dynamic allocations at a minimum. See also the documentation for osmo_use_count_cb_t. + * + * List traversal considerations: your typical use count list would max at about six entries in practice. Traversing six + * llist->next pointers is less effort than doing a common strlen(). + * + * Obtaining the total use count: osmo_use_count_total() traverses all use token entries and forms a sum. It is trivial + * to keep a separate total count that completely avoids the need for calling this function, which is entirely up to the + * individual osmo_use_count_cb_t() implementation. The optimization gained is usually not worth it, though. + * + * Use token comparison considerations: strcmp() to compare use tokens is a fairly good tradeoff: + * - when the strings differ, strcmp() usually exits on the first or second character. + * - when the strings are identical, they are usually the exact same char* address (from compile-time string constant), + * meaning that strcmp() is completely skipped. + * (quote: "if (e->use == use || (use && e->use && !strcmp(e->use, use)))") + * - if we specified compile-time string constant use as requirement, we wouldn't need strcmp() at all, but this + * minuscule overhead has the benefit of complete correctness for any kinds of use token strings. + * + * Example: + * + * struct foo { + * struct osmo_use_count use_count; + * }; + * + * // Convenience macros for struct foo instances. These are strict about use count errors. + * #define foo_get(FOO, USE) OSMO_ASSERT( osmo_use_count_get_put(&(FOO)->use_count, USE, 1) == 0 ); + * #define foo_put(FOO, USE) OSMO_ASSERT( osmo_use_count_get_put(&(FOO)->use_count, USE, -1) == 0 ); + * + * int foo_use_cb(struct osmo_use_count_entry *use_count_entry, int32_t old_use_count, const char *file, int line) + * { + * struct foo *foo = use_count_entry->use_count->talloc_object; + * if (osmo_use_count_total(&use_count_entry->use_count) == 0) + * talloc_free(foo); + * } + * + * // The function name is a convenient use token: + * void rx_stop_baz_request(struct foo *foo) + * { + * foo_get(foo, __func__); + * + * foo_put(foo, "baz"); + * printf("Stopped Bazing (%p)\n", foo); + * + * foo_put(foo, __func__); + * } + * + * void use_count_example() + * { + * struct foo *foo = talloc_zero(ctx, struct foo); + * *foo = (struct foo){ + * .use_count = { + * .talloc_object = foo, + * .use_cb = foo_use_cb, + * }, + * }; + * + * foo_get(foo, "bar"); // one osmo_use_count_entry was allocated + * foo_get(foo, "baz"); // a second osmo_use_count_entry was allocated + * foo_get(foo, "baz"); // still two entries + * + * printf("use: %s\n", osmo_use_count_name_buf(namebuf, sizeof(namebuf), &foo->use_count)); + * // "use: 3 (bar,2*baz)" + * + * foo_put(foo, "bar"); // still two entries, one entry is idle ("bar"=0) + * foo_put(foo, "baz"); + * rx_stop_baz_request(foo); + * // Final "baz" was put(), foo_use_cb() deallocated object foo, as well as all use count entries. + * }; + */ +struct osmo_use_count { + /*! Context to talloc-allocate use count entries from (if at all necessary); back-pointer to the owning object + * for osmo_use_count_cb_t implementations. */ + void *talloc_object; + /*! If not NULL, this is invoked for each use count change. */ + osmo_use_count_cb_t use_cb; + /*! List of use tokens. No need to touch this, the llist is initialized implicitly. */ + struct llist_head use_counts; +}; + +/*! One named counter in the list managed by osmo_use_count. + * Gets created as necessary by osmo_use_count_get_put(). The total current use count of an object is the sum of all + * individual osmo_use_count_entry->count. + * + * object <--backpointer-+ + * t| .osmo_use_count | + * a| .talloc_object ------------+ + * l| .use_counts llist: use count + * l|-> - osmo_use_count_entry: "foo" 1 + * o|-> - osmo_use_count_entry: "bar" 3 + * c|-> - osmo_use_count_entry: "baz" 0 (currently unused entry) + */ +struct osmo_use_count_entry { + /*! Entry in osmo_use_count->use_counts. */ + struct llist_head entry; + /*! Parent use count and backpointer to the talloc_object. */ + struct osmo_use_count *use_count; + /*! Use token string that was passed to osmo_use_count_get_put(). */ + const char *use; + /*! Current use count amount for only this use token string. + * If zero, this entry is currently unused and kept around to avoid frequent de-/allocation. */ + int32_t count; +}; + +/*! Change the use count for a given use token. + * \param USE_LIST A struct osmo_use_count*, e.g. &my_obj->use_count. + * \param USE A use token: arbitrary string (const char*). This must remain valid memory, e.g. string constants. + * \param CHANGE Signed integer value to add to the use count: positive means get(), negative means put(). + * \return Negative on range violations or USE_LIST == NULL, the use_cb()'s return value, or 0 on success. + */ +#define osmo_use_count_get_put(USE_LIST, USE, CHANGE) \ + _osmo_use_count_get_put(USE_LIST, USE, CHANGE, __FILE__, __LINE__) + +int _osmo_use_count_get_put(struct osmo_use_count *uc, const char *use, int32_t change, + const char *file, int line); + +const char *osmo_use_count_name_buf(char *buf, size_t buf_len, const struct osmo_use_count *uc); + +int32_t osmo_use_count_total(const struct osmo_use_count *uc); +int32_t osmo_use_count_by(const struct osmo_use_count *uc, const char *use); + +struct osmo_use_count_entry *osmo_use_count_find(const struct osmo_use_count *uc, const char *use); +void osmo_use_count_free(struct osmo_use_count_entry *use_count_entry); + +void osmo_use_count_make_static_entries(struct osmo_use_count *uc, struct osmo_use_count_entry *buf, + size_t buf_n_entries); + +/*! @} */ diff --git a/src/Makefile.am b/src/Makefile.am index 1fae8b0ac..aaf2233ea 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -26,6 +26,7 @@ libosmocore_la_SOURCES = timer.c timer_gettimeofday.c timer_clockgettime.c \ isdnhdlc.c \ tdef.c \ sockaddr_str.c \ + use_count.c \ $(NULL) if HAVE_SSSE3 diff --git a/src/use_count.c b/src/use_count.c new file mode 100644 index 000000000..453d2ae8c --- /dev/null +++ b/src/use_count.c @@ -0,0 +1,279 @@ +/*! \file use_count.c + * Generic object usage counter Implementation (get, put and deallocate on zero count). + */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include + +#include +#include +#include + +/*! \addtogroup use_count + * + * Generic object usage counter (get, put and deallocate on zero count). + * + * For an example and a detailed description, see struct osmo_use_count. + * + * @{ + * \file use_count.c + */ + +/*! Add two int32_t but make sure to min- and max-clamp at INT32_MIN and INT32_MAX, respectively. */ +static inline bool count_safe(int32_t *val_p, int32_t add) +{ + int32_t val = *val_p; + + /* A simpler implementation would just let the integer overflow and compare with previous value afterwards, but + * that causes runtime errors in the address sanitizer. So let's just do this without tricks. */ + if (add < 0 && val < 0 && val - INT32_MIN < -add) { + *val_p = INT32_MIN; + return false; + } + + if (add > 0 && val > 0 && INT32_MAX - val < add) { + *val_p = INT32_MAX; + return false; + } + + *val_p = val + add; + return true; +} + +/*! Return the sum of all use counts, min- and max-clamped at INT32_MIN and INT32_MAX. + * \param[in] uc Use counts to sum up. + * \return Accumulated counts, or 0 if uc is NULL. + */ +int32_t osmo_use_count_total(const struct osmo_use_count *uc) +{ + struct osmo_use_count_entry *e; + int32_t total = 0; + + if (!uc || !uc->use_counts.next) + return 0; + + llist_for_each_entry(e, &uc->use_counts, entry) { + count_safe(&total, e->count); + } + return total; +} + +/*! Return use count by a single use token. + * \param[in] uc Use counts to look up in. + * \param[in] use Use token. + * \return Use count, or 0 if uc is NULL or use token is not present. + */ +int32_t osmo_use_count_by(const struct osmo_use_count *uc, const char *use) +{ + const struct osmo_use_count_entry *e; + if (!uc) + return 0; + e = osmo_use_count_find(uc, use); + if (!e) + return 0; + return e->count; +} + +/*! Write a comprehensive listing of use counts to a string buffer. + * Reads like "12 (3*barring,fighting,8*kungfoo)". + * \param[inout] buf Destination buffer. + * \param[in] buf_len sizeof(buf). + * \param[in] uc Use counts to print. + * \return buf, always nul-terminated (except when buf_len < 1). + */ +const char *osmo_use_count_name_buf(char *buf, size_t buf_len, const struct osmo_use_count *uc) +{ + int32_t count = osmo_use_count_total(uc); + struct osmo_strbuf sb = { .buf = buf, .len = buf_len }; + struct osmo_use_count_entry *e; + bool first; + + OSMO_STRBUF_PRINTF(sb, "%" PRId32 " (", count); + + first = true; + llist_for_each_entry(e, &uc->use_counts, entry) { + if (!e->count) + continue; + if (!first) + OSMO_STRBUF_PRINTF(sb, ","); + first = false; + if (e->count != 1) + OSMO_STRBUF_PRINTF(sb, "%" PRId32 "*", e->count); + OSMO_STRBUF_PRINTF(sb, "%s", e->use ? : "NULL"); + } + if (first) + OSMO_STRBUF_PRINTF(sb, "-"); + OSMO_STRBUF_PRINTF(sb, ")"); + return buf; +} + +/* Return a use token's use count entry -- probably you want osmo_use_count_by() instead. + * \param[in] uc Use counts to look up in. + * \param[in] use Use token. + * \return matching entry, or NULL if not present. + */ +struct osmo_use_count_entry *osmo_use_count_find(const struct osmo_use_count *uc, const char *use) +{ + struct osmo_use_count_entry *e; + if (!uc->use_counts.next) + return NULL; + llist_for_each_entry(e, &uc->use_counts, entry) { + if (e->use == use || (use && e->use && !strcmp(e->use, use))) + return e; + } + return NULL; +} + +/*! Find a use count entry that currently has zero count, and re-use that for this new use token. */ +static struct osmo_use_count_entry *osmo_use_count_repurpose_zero_entry(struct osmo_use_count *uc, const char *use) +{ + struct osmo_use_count_entry *e; + if (!uc->use_counts.next) + return NULL; + llist_for_each_entry(e, &uc->use_counts, entry) { + if (!e->count) { + e->use = use; + return e; + } + } + return NULL; +} + +/*! Allocate a new use count entry, happens implicitly in osmo_use_count_get_put(). */ +static struct osmo_use_count_entry *osmo_use_count_create(struct osmo_use_count *uc, const char *use) +{ + struct osmo_use_count_entry *e = talloc_zero(uc->talloc_object, struct osmo_use_count_entry); + if (!e) + return NULL; + *e = (struct osmo_use_count_entry){ + .use_count = uc, + .use = use, + }; + if (!uc->use_counts.next) + INIT_LLIST_HEAD(&uc->use_counts); + llist_add_tail(&e->entry, &uc->use_counts); + return e; +} + +/*! Deallocate a use count entry. + * Normally, this is not necessary -- it is ok and even desirable to leave use count entries around even when they reach + * a count of zero, until the use_count->talloc_object deallocates and removes all of them in one flush. This avoids + * repeated allocation and deallocation for use tokens, because use count entries that have reached zero count are + * repurposed for any other use tokens. A cleanup makes sense only if a very large number of differing use tokens surged + * at the same time, and the owning object will not be deallocated soon; if so, this should be done by the + * osmo_use_count_cb_t implementation. + * + * osmo_use_count_free() must *not* be called on use count entries that were added by + * osmo_use_count_make_static_entries(). This is the responsibility of the osmo_use_count_cb_t() implementation. + * + * \param[in] use_count_entry Use count entry to unlist and free. + */ +void osmo_use_count_free(struct osmo_use_count_entry *use_count_entry) +{ + if (!use_count_entry) + return; + llist_del(&use_count_entry->entry); + talloc_free(use_count_entry); +} + +/*! Implementation for osmo_use_count_get_put(), which can also be directly invoked to pass source file information. For + * arguments besides file and line, see osmo_use_count_get_put(). + * \param[in] file Source file path, as in __FILE__. + * \param[in] line Source file line, as in __LINE__. + */ +int _osmo_use_count_get_put(struct osmo_use_count *uc, const char *use, int32_t change, + const char *file, int line) +{ + struct osmo_use_count_entry *e; + int32_t old_use_count; + if (!change) + return 0; + + e = osmo_use_count_find(uc, use); + if (!e) + e = osmo_use_count_repurpose_zero_entry(uc, use); + if (!e) + e = osmo_use_count_create(uc, use); + if (!e) + return -ENOMEM; + + if (!e->count) { + /* move to end */ + llist_del(&e->entry); + llist_add_tail(&e->entry, &uc->use_counts); + } + + old_use_count = e->count; + if (!count_safe(&e->count, change)) { + e->count = old_use_count; + return -ERANGE; + } + + if (uc->use_cb) + return uc->use_cb(e, old_use_count, file, line); + return 0; +} + +/*! Add N static use token entries to avoid dynamic allocation of use count tokens. + * When not using this function, use count entries are talloc allocated from uc->talloc_object as talloc context. This + * means that there are small dynamic allocations for each use count token. osmo_use_count_get_put() normally leaves + * zero-count entries around and re-purposes them later, so the number of small allocations is at most the number of + * concurrent differently-named uses of the same object. If that is not enough, this function allows completely avoiding + * dynamic use count allocations, by adding N static entries with a zero count and a NULL use token. They will be used + * by osmo_use_count_get_put(), and, if the caller avoids using osmo_use_count_free(), the osmo_use_count implementation + * never deallocates them. The idea is that the entries are members of the uc->talloc_object, or that they will by other + * means be implicitly deallocated by the talloc_object. It is fine to call + * osmo_use_count_make_static_entries(buf_n_entries=N) and later have more than N concurrent uses, i.e. it is no problem + * to mix static and dynamic entries. To completely avoid dynamic use count entries, N has to >= the maximum number of + * concurrent differently-named uses that will occur in the lifetime of the talloc_object. + * + * struct my_object { + * struct osmo_use_count use_count; + * struct osmo_use_count_entry use_count_buf[3]; // planning for 3 concurrent users + * }; + * + * void example() { + * struct my_object *o = talloc_zero(ctx, struct my_object); + * osmo_use_count_make_static_entries(&o->use_count, o->use_count_buf, ARRAY_SIZE(o->use_count_buf)); + * } + */ +void osmo_use_count_make_static_entries(struct osmo_use_count *uc, struct osmo_use_count_entry *buf, + size_t buf_n_entries) +{ + size_t idx; + if (!uc->use_counts.next) + INIT_LLIST_HEAD(&uc->use_counts); + for (idx = 0; idx < buf_n_entries; idx++) { + struct osmo_use_count_entry *e = &buf[idx]; + *e = (struct osmo_use_count_entry){ + .use_count = uc, + }; + llist_add_tail(&e->entry, &uc->use_counts); + } +} + +/*! @} */ diff --git a/tests/Makefile.am b/tests/Makefile.am index 88bcd7e99..d123ee2a7 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -31,6 +31,7 @@ check_PROGRAMS = timer/timer_test sms/sms_test ussd/ussd_test \ tdef/tdef_vty_test_config_subnode \ tdef/tdef_vty_test_dynamic \ sockaddr_str/sockaddr_str_test \ + use_count/use_count_test \ $(NULL) if ENABLE_MSGFILE @@ -240,6 +241,9 @@ tdef_tdef_vty_test_dynamic_LDADD = $(LDADD) $(top_builddir)/src/vty/libosmovty.l sockaddr_str_sockaddr_str_test_SOURCES = sockaddr_str/sockaddr_str_test.c sockaddr_str_sockaddr_str_test_LDADD = $(LDADD) +use_count_use_count_test_SOURCES = use_count/use_count_test.c +use_count_use_count_test_LDADD = $(LDADD) + # The `:;' works around a Bash 3.2 bug when the output is not writeable. $(srcdir)/package.m4: $(top_srcdir)/configure.ac :;{ \ @@ -309,6 +313,7 @@ EXTRA_DIST = testsuite.at $(srcdir)/package.m4 $(TESTSUITE) \ tdef/tdef_vty_test_config_subnode.vty \ tdef/tdef_vty_test_dynamic.vty \ sockaddr_str/sockaddr_str_test.ok \ + use_count/use_count_test.ok use_count/use_count_test.err \ $(NULL) DISTCLEANFILES = atconfig atlocal conv/gsm0503_test_vectors.c diff --git a/tests/testsuite.at b/tests/testsuite.at index db2003f44..5a6e8020c 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -337,3 +337,10 @@ AT_KEYWORDS([sockaddr_str]) cat $abs_srcdir/sockaddr_str/sockaddr_str_test.ok > expout AT_CHECK([$abs_top_builddir/tests/sockaddr_str/sockaddr_str_test], [0], [expout], [ignore]) AT_CLEANUP + +AT_SETUP([use_count]) +AT_KEYWORDS([use_count]) +cat $abs_srcdir/use_count/use_count_test.ok > expout +cat $abs_srcdir/use_count/use_count_test.err > experr +AT_CHECK([$abs_top_builddir/tests/use_count/use_count_test], [0], [expout], [experr]) +AT_CLEANUP diff --git a/tests/use_count/use_count_test.c b/tests/use_count/use_count_test.c new file mode 100644 index 000000000..0b081c9cc --- /dev/null +++ b/tests/use_count/use_count_test.c @@ -0,0 +1,339 @@ +/* Test implementation for osmo_use_count API. */ +/* + * (C) 2019 by sysmocom s.f.m.c. GmbH + * + * All Rights Reserved + * + * Author: Neels Hofmeyr + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +static void *ctx = NULL; + +#define log(fmt, args...) fprintf(stderr, fmt, ##args) + +enum { + DFOO, +}; + +#define FOO_USE_BARRING "barring" +#define FOO_USE_FIGHTING "fighting" +#define FOO_USE_KUNG "kungfoo" +#define FOO_USE_RELEASING "releasing" + +LLIST_HEAD(all_foo); + +struct foo { + struct llist_head entry; + struct osmo_fsm_inst *fi; + struct osmo_use_count use_count; + struct osmo_use_count_entry use_count_buf[10]; +}; + +enum foo_fsm_events { + FOO_EV_UNUSED, +}; + +static char name_buf[1024]; +#define use_count_name(UL) osmo_use_count_name_buf(name_buf, sizeof(name_buf), UL) + +int foo_use_cb(struct osmo_use_count_entry *use_count_entry, int32_t old_use_count, + const char *file, int line) +{ + struct osmo_use_count *use_count = use_count_entry->use_count; + struct foo *foo = use_count->talloc_object; + const char *use = use_count_entry->use; + int32_t new_use_count = use_count_entry->count; + + if (use && (!strcmp(use, FOO_USE_BARRING) || !strcmp(use, FOO_USE_RELEASING)) + && new_use_count > 1) { + LOGPFSMLSRC(foo->fi, LOGL_ERROR, file, line, + "Attempt to get more than one %s\n", use); + /* Fix the use count */ + use_count_entry->count = 1; + return -ERANGE; + } + + LOGPFSMLSRC(foo->fi, LOGL_NOTICE, file, line, "%s %+d %s: now used by %s\n", + foo->fi->id, new_use_count - old_use_count, use ? : "NULL", use_count_name(use_count)); + + if (new_use_count < 0) { + LOGPFSMLSRC(foo->fi, LOGL_ERROR, file, line, "Negative use count on %s: %s\n", + use ? : "NULL", use_count_name(use_count)); + /* Let it pass for the sake of this test */ + } + + if (osmo_use_count_total(use_count) == 0) + osmo_fsm_inst_dispatch(foo->fi, FOO_EV_UNUSED, NULL); + return 0; +} + +#define foo_get_put(FOO, USE, CHANGE) do { \ + int rc = osmo_use_count_get_put(&(FOO)->use_count, USE, CHANGE); \ + if (rc) \ + log("osmo_use_count_get_put(%s, %s, %d) returned error: %d %s\n", \ + (FOO)->fi->id, USE ? : "NULL", CHANGE, rc, strerror(-rc)); \ + } while(0) + +#define foo_get(FOO, USE) foo_get_put(FOO, USE, 1) +#define foo_put(FOO, USE) foo_get_put(FOO, USE, -1) + +enum foo_fsm_states { + FOO_ST_IN_USE, + FOO_ST_IN_RELEASE, +}; + +void foo_fsm_in_use(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + OSMO_ASSERT(event == FOO_EV_UNUSED); + osmo_fsm_inst_state_chg(fi, FOO_ST_IN_RELEASE, 0, 0); +} + +void foo_fsm_in_release_onenter(struct osmo_fsm_inst *fi, uint32_t prev_state) +{ + struct foo *foo = fi->priv; + foo_get(foo, FOO_USE_RELEASING); +} + +void foo_fsm_in_release(struct osmo_fsm_inst *fi, uint32_t event, void *data) +{ + OSMO_ASSERT(event == FOO_EV_UNUSED); + osmo_fsm_inst_term(fi, OSMO_FSM_TERM_REGULAR, NULL); +} + + +#define S(x) (1 << (x)) + +static const struct osmo_fsm_state foo_fsm_states[] = { + [FOO_ST_IN_USE] = { + .name = "IN_USE", + .in_event_mask = 0 + | S(FOO_EV_UNUSED) + , + .out_state_mask = 0 + | S(FOO_ST_IN_RELEASE) + , + .action = foo_fsm_in_use, + }, + [FOO_ST_IN_RELEASE] = { + .name = "IN_RELEASE", + .in_event_mask = 0 + | S(FOO_EV_UNUSED) + , + .out_state_mask = 0 + , + .onenter = foo_fsm_in_release_onenter, + .action = foo_fsm_in_release, + }, +}; + +static const struct value_string foo_fsm_event_names[] = { + OSMO_VALUE_STRING(FOO_EV_UNUSED), + {} +}; + +void foo_cleanup(struct osmo_fsm_inst *fi, enum osmo_fsm_term_cause cause) +{ + struct foo *foo = fi->priv; + llist_del(&foo->entry); +} + +static struct osmo_fsm foo_fsm = { + .name = "foo", + .states = foo_fsm_states, + .event_names = foo_fsm_event_names, + .num_states = ARRAY_SIZE(foo_fsm_states), + .log_subsys = DFOO, + .cleanup = foo_cleanup, +}; + +static struct foo *foo_alloc(const char *name, size_t static_entries) +{ + struct foo *foo; + struct osmo_fsm_inst *fi = osmo_fsm_inst_alloc(&foo_fsm, ctx, NULL, LOGL_DEBUG, name); + OSMO_ASSERT(fi); + OSMO_ASSERT(static_entries <= ARRAY_SIZE(foo->use_count_buf)); + + foo = talloc_zero(fi, struct foo); + *foo = (struct foo){ + .fi = fi, + .use_count = { + .talloc_object = foo, + .use_cb = foo_use_cb, + }, + }; + fi->priv = foo; + + osmo_use_count_make_static_entries(&foo->use_count, foo->use_count_buf, static_entries); + + llist_add_tail(&foo->entry, &all_foo); + return foo; +} + +void print_foos() +{ + int count = 0; + struct foo *foo; + fprintf(stderr, "\nall use counts:\n"); + llist_for_each_entry(foo, &all_foo, entry) { + fprintf(stderr, "%s: %s\n", foo->fi->id, use_count_name(&foo->use_count)); + count++; + } + fprintf(stderr, "%d foos\n\n", count); +} + +static void test_use_count_fsm() +{ + struct foo *a, *b, *c; + log("\n%s()\n", __func__); + + a = foo_alloc("a", 0); + b = foo_alloc("b", 2); + c = foo_alloc("c", 10); + print_foos(); + + log("A few gets and puts, logging source file information\n"); + log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME); + foo_get(a, FOO_USE_BARRING); + + foo_get(b, FOO_USE_BARRING); + foo_get(b, FOO_USE_FIGHTING); + + print_foos(); + + log("Attempt to get more than one on limited 'barring' user:\n"); + foo_get(b, FOO_USE_BARRING); + print_foos(); + + log("Put away one user of b\n"); + foo_put(b, FOO_USE_BARRING); + print_foos(); + + log("(no longer log source file information)\n"); + log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE); + + log("Test null use token\n"); + foo_get(a, NULL); + print_foos(); + foo_put(a, NULL); + print_foos(); + + log("Put away last user of a, goes to RELEASING state and waits for a hypothetic async release process\n"); + foo_put(a, FOO_USE_BARRING); + print_foos(); + + log("Async releasing of a is done, will dealloc\n"); + foo_put(a, FOO_USE_RELEASING); + print_foos(); + + log("Use b multiple times\n"); + foo_get(b, FOO_USE_KUNG); + foo_get(b, FOO_USE_KUNG); + + foo_put(b, FOO_USE_KUNG); + foo_get(b, FOO_USE_KUNG); + + foo_get(b, FOO_USE_KUNG); + print_foos(); + + log("Test range: set kung-fu to INT32_MAX-1, then get three more; total count gets max-clamped to INT32_MAX\n"); + foo_get_put(b, FOO_USE_KUNG, INT32_MAX-1 - osmo_use_count_by(&b->use_count, FOO_USE_KUNG)); + print_foos(); + foo_get(b, FOO_USE_KUNG); + foo_get(b, FOO_USE_KUNG); + foo_get(b, FOO_USE_KUNG); + foo_get_put(b, FOO_USE_FIGHTING, 2); + foo_get_put(b, FOO_USE_KUNG, -3); + foo_put(b, FOO_USE_KUNG); + foo_put(b, FOO_USE_KUNG); + foo_get(b, FOO_USE_FIGHTING); + foo_get(b, FOO_USE_FIGHTING); + foo_get(b, FOO_USE_FIGHTING); + print_foos(); + + log("Release all uses of b\n"); + foo_get_put(b, FOO_USE_KUNG, - osmo_use_count_by(&b->use_count, FOO_USE_KUNG)); + foo_get_put(b, FOO_USE_FIGHTING, - osmo_use_count_by(&b->use_count, FOO_USE_FIGHTING)); + + log("Signal async release as done\n"); + foo_put(b, FOO_USE_RELEASING); + print_foos(); + + log("Release something not gotten before: a get/put bug goes into negative count\n"); + foo_put(c, FOO_USE_KUNG); + print_foos(); + log("More negative\n"); + foo_put(c, FOO_USE_KUNG); + foo_put(c, FOO_USE_KUNG); + print_foos(); + + log("Also release c\n"); + foo_get_put(c, FOO_USE_KUNG, 4); + foo_put(c, FOO_USE_KUNG); + log("Signal async release as done\n"); + foo_put(c, FOO_USE_RELEASING); + print_foos(); +} + +static const struct log_info_cat default_categories[] = { + [DFOO] = { + .name = "DFOO", + .description = "FOO", + .enabled = 1, .loglevel = LOGL_DEBUG, + }, +}; + +static const struct log_info log_info = { + .cat = default_categories, + .num_cat = ARRAY_SIZE(default_categories), +}; + + +int main(int argc, char **argv) +{ + ctx = talloc_named_const(NULL, 0, "use_count_test.c"); + + osmo_fsm_log_addr(false); + + osmo_init_logging2(ctx, &log_info); + + log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_BASENAME); + log_set_print_filename2(osmo_stderr_target, LOG_FILENAME_NONE); + log_set_print_filename_pos(osmo_stderr_target, LOG_FILENAME_POS_LINE_END); + log_set_print_category(osmo_stderr_target, 1); + log_set_print_category_hex(osmo_stderr_target, 0); + log_set_print_level(osmo_stderr_target, 1); + log_set_use_color(osmo_stderr_target, 0); + + osmo_fsm_register(&foo_fsm); + + test_use_count_fsm(); + + return EXIT_SUCCESS; +} diff --git a/tests/use_count/use_count_test.err b/tests/use_count/use_count_test.err new file mode 100644 index 000000000..97e74a510 --- /dev/null +++ b/tests/use_count/use_count_test.err @@ -0,0 +1,171 @@ + +test_use_count_fsm() +DFOO DEBUG foo(a){IN_USE}: Allocated +DFOO DEBUG foo(b){IN_USE}: Allocated +DFOO DEBUG foo(c){IN_USE}: Allocated + +all use counts: +a: 0 (-) +b: 0 (-) +c: 0 (-) +3 foos + +A few gets and puts, logging source file information +DFOO NOTICE foo(a){IN_USE}: a +1 barring: now used by 1 (barring) (use_count_test.c:223) +DFOO NOTICE foo(b){IN_USE}: b +1 barring: now used by 1 (barring) (use_count_test.c:225) +DFOO NOTICE foo(b){IN_USE}: b +1 fighting: now used by 2 (barring,fighting) (use_count_test.c:226) + +all use counts: +a: 1 (barring) +b: 2 (barring,fighting) +c: 0 (-) +3 foos + +Attempt to get more than one on limited 'barring' user: +DFOO ERROR foo(b){IN_USE}: Attempt to get more than one barring (use_count_test.c:231) +osmo_use_count_get_put(b, barring, 1) returned error: -34 Numerical result out of range + +all use counts: +a: 1 (barring) +b: 2 (barring,fighting) +c: 0 (-) +3 foos + +Put away one user of b +DFOO NOTICE foo(b){IN_USE}: b -1 barring: now used by 1 (fighting) (use_count_test.c:235) + +all use counts: +a: 1 (barring) +b: 1 (fighting) +c: 0 (-) +3 foos + +(no longer log source file information) +Test null use token +DFOO NOTICE foo(a){IN_USE}: a +1 NULL: now used by 2 (barring,NULL) + +all use counts: +a: 2 (barring,NULL) +b: 1 (fighting) +c: 0 (-) +3 foos + +DFOO NOTICE foo(a){IN_USE}: a -1 NULL: now used by 1 (barring) + +all use counts: +a: 1 (barring) +b: 1 (fighting) +c: 0 (-) +3 foos + +Put away last user of a, goes to RELEASING state and waits for a hypothetic async release process +DFOO NOTICE foo(a){IN_USE}: a -1 barring: now used by 0 (-) +DFOO DEBUG foo(a){IN_USE}: Received Event FOO_EV_UNUSED +DFOO DEBUG foo(a){IN_USE}: state_chg to IN_RELEASE +DFOO NOTICE foo(a){IN_RELEASE}: a +1 releasing: now used by 1 (releasing) + +all use counts: +a: 1 (releasing) +b: 1 (fighting) +c: 0 (-) +3 foos + +Async releasing of a is done, will dealloc +DFOO NOTICE foo(a){IN_RELEASE}: a -1 releasing: now used by 0 (-) +DFOO DEBUG foo(a){IN_RELEASE}: Received Event FOO_EV_UNUSED +DFOO DEBUG foo(a){IN_RELEASE}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DFOO DEBUG foo(a){IN_RELEASE}: Freeing instance +DFOO DEBUG foo(a){IN_RELEASE}: Deallocated + +all use counts: +b: 1 (fighting) +c: 0 (-) +2 foos + +Use b multiple times +DFOO NOTICE foo(b){IN_USE}: b +1 kungfoo: now used by 2 (fighting,kungfoo) +DFOO NOTICE foo(b){IN_USE}: b +1 kungfoo: now used by 3 (fighting,2*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b -1 kungfoo: now used by 2 (fighting,kungfoo) +DFOO NOTICE foo(b){IN_USE}: b +1 kungfoo: now used by 3 (fighting,2*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b +1 kungfoo: now used by 4 (fighting,3*kungfoo) + +all use counts: +b: 4 (fighting,3*kungfoo) +c: 0 (-) +2 foos + +Test range: set kung-fu to INT32_MAX-1, then get three more; total count gets max-clamped to INT32_MAX +DFOO NOTICE foo(b){IN_USE}: b +2147483643 kungfoo: now used by 2147483647 (fighting,2147483646*kungfoo) + +all use counts: +b: 2147483647 (fighting,2147483646*kungfoo) +c: 0 (-) +2 foos + +DFOO NOTICE foo(b){IN_USE}: b +1 kungfoo: now used by 2147483647 (fighting,2147483647*kungfoo) +osmo_use_count_get_put(b, kungfoo, 1) returned error: -34 Numerical result out of range +osmo_use_count_get_put(b, kungfoo, 1) returned error: -34 Numerical result out of range +DFOO NOTICE foo(b){IN_USE}: b +2 fighting: now used by 2147483647 (3*fighting,2147483647*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b -3 kungfoo: now used by 2147483647 (3*fighting,2147483644*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b -1 kungfoo: now used by 2147483646 (3*fighting,2147483643*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b -1 kungfoo: now used by 2147483645 (3*fighting,2147483642*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b +1 fighting: now used by 2147483646 (4*fighting,2147483642*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b +1 fighting: now used by 2147483647 (5*fighting,2147483642*kungfoo) +DFOO NOTICE foo(b){IN_USE}: b +1 fighting: now used by 2147483647 (6*fighting,2147483642*kungfoo) + +all use counts: +b: 2147483647 (6*fighting,2147483642*kungfoo) +c: 0 (-) +2 foos + +Release all uses of b +DFOO NOTICE foo(b){IN_USE}: b -2147483642 kungfoo: now used by 6 (6*fighting) +DFOO NOTICE foo(b){IN_USE}: b -6 fighting: now used by 0 (-) +DFOO DEBUG foo(b){IN_USE}: Received Event FOO_EV_UNUSED +DFOO DEBUG foo(b){IN_USE}: state_chg to IN_RELEASE +DFOO NOTICE foo(b){IN_RELEASE}: b +1 releasing: now used by 1 (releasing) +Signal async release as done +DFOO NOTICE foo(b){IN_RELEASE}: b -1 releasing: now used by 0 (-) +DFOO DEBUG foo(b){IN_RELEASE}: Received Event FOO_EV_UNUSED +DFOO DEBUG foo(b){IN_RELEASE}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DFOO DEBUG foo(b){IN_RELEASE}: Freeing instance +DFOO DEBUG foo(b){IN_RELEASE}: Deallocated + +all use counts: +c: 0 (-) +1 foos + +Release something not gotten before: a get/put bug goes into negative count +DFOO NOTICE foo(c){IN_USE}: c -1 kungfoo: now used by -1 (-1*kungfoo) +DFOO ERROR foo(c){IN_USE}: Negative use count on kungfoo: -1 (-1*kungfoo) + +all use counts: +c: -1 (-1*kungfoo) +1 foos + +More negative +DFOO NOTICE foo(c){IN_USE}: c -1 kungfoo: now used by -2 (-2*kungfoo) +DFOO ERROR foo(c){IN_USE}: Negative use count on kungfoo: -2 (-2*kungfoo) +DFOO NOTICE foo(c){IN_USE}: c -1 kungfoo: now used by -3 (-3*kungfoo) +DFOO ERROR foo(c){IN_USE}: Negative use count on kungfoo: -3 (-3*kungfoo) + +all use counts: +c: -3 (-3*kungfoo) +1 foos + +Also release c +DFOO NOTICE foo(c){IN_USE}: c +4 kungfoo: now used by 1 (kungfoo) +DFOO NOTICE foo(c){IN_USE}: c -1 kungfoo: now used by 0 (-) +DFOO DEBUG foo(c){IN_USE}: Received Event FOO_EV_UNUSED +DFOO DEBUG foo(c){IN_USE}: state_chg to IN_RELEASE +DFOO NOTICE foo(c){IN_RELEASE}: c +1 releasing: now used by 1 (releasing) +Signal async release as done +DFOO NOTICE foo(c){IN_RELEASE}: c -1 releasing: now used by 0 (-) +DFOO DEBUG foo(c){IN_RELEASE}: Received Event FOO_EV_UNUSED +DFOO DEBUG foo(c){IN_RELEASE}: Terminating (cause = OSMO_FSM_TERM_REGULAR) +DFOO DEBUG foo(c){IN_RELEASE}: Freeing instance +DFOO DEBUG foo(c){IN_RELEASE}: Deallocated + +all use counts: +0 foos + diff --git a/tests/use_count/use_count_test.ok b/tests/use_count/use_count_test.ok new file mode 100644 index 000000000..e69de29bb