aboutsummaryrefslogtreecommitdiffstats
path: root/src/tdef.c
diff options
context:
space:
mode:
authorNeels Hofmeyr <neels@hofmeyr.de>2019-01-26 20:36:12 +0100
committerNeels Hofmeyr <neels@hofmeyr.de>2019-02-04 18:52:16 +0100
commit0fd615fd7b701fc0b3fd7f96429691e3eaa5a877 (patch)
tree7c2885199ce93394c308e3399b4d76d843416505 /src/tdef.c
parent44c15b7a8fadff949e79be0b843ec9caca61901c (diff)
add osmo_tdef API, originally adopted from osmo-bsc T_def
Move T_def from osmo-bsc to libosmocore as osmo_tdef. Adjust naming to be more consistent. Upgrade to first class API: - add timer grouping - add generic vty support - add mising API doc - add C test - add VTY transcript tests, also as examples for using the API From osmo_fsm_inst_state_chg() API doc, cross reference to osmo_tdef API. The root reason for moving to libosmocore is that I want to use the mgw_endpoint_fsm in osmo-msc for inter-MSC handover, and hence want to move the FSM to libosmo-mgcp-client. This FSM uses the T_def from osmo-bsc. Though the mgw_endpoint_fsm's use of T_def is minimal, I intend to use the osmo_tdef API in osmo-msc (and probably elsewhere) as well. libosmocore is the most sensible place for this. osmo_tdef provides: - a list of Tnnnn (GSM) timers with description, unit and default value. - vty UI to allow users to configure non-default timeouts. - API to tie T timers to osmo_fsm states and set them on state transitions. - a few standard units (minute, second, millisecond) as well as a custom unit (which relies on the timer's human readable description to indicate the meaning of the value). - conversion for standard units: for example, some GSM timers are defined in minutes, while our FSM definitions need timeouts in seconds. Conversion is for convenience only and can be easily avoided via the custom unit. By keeping separate osmo_tdef arrays, several groups of timers can be kept separately. The VTY tests in tests/tdef/ showcase different schemes: - tests/vty/tdef_vty_test_config_root.c: Keep several timer definitions in separately named groups: showcase the osmo_tdef_vty_groups*() API. Each timer group exists exactly once. - tests/vty/tdef_vty_test_config_subnode.c: Keep a single list of timers without separate grouping. Put this list on a specific subnode below the CONFIG_NODE. There could be several separate subnodes with timers like this, i.e. continuing from this example, sets timers could be separated by placing timers in specific config subnodes instead of using the global group name. - tests/vty/tdef_vty_test_dynamic.c: Dynamically allocate timer definitions per each new created object. Thus there can be an arbitrary number of independent timer definitions, one per allocated object. T_def was introduced during the recent osmo-bsc refactoring for inter-BSC handover, and has proven useful: - without osmo_tdef, each invocation of osmo_fsm_inst_state_chg() needs to be programmed with the right timeout value, for all code paths that invoke this state change. It is a likely source of errors to get one of them wrong. By defining a T timer exactly for an FSM state, the caller can merely invoke the state change and trust on the original state definition to apply the correct timeout. - it is helpful to have a standardized config file UI to provide user configurable timeouts, instead of inventing new VTY commands for each separate application of T timer numbers. Change-Id: Ibd6b1ed7f1bd6e1f2e0fde53352055a4468f23e5
Diffstat (limited to 'src/tdef.c')
-rw-r--r--src/tdef.c282
1 files changed, 282 insertions, 0 deletions
diff --git a/src/tdef.c b/src/tdef.c
new file mode 100644
index 00000000..7e79d680
--- /dev/null
+++ b/src/tdef.c
@@ -0,0 +1,282 @@
+/*! \file tdef.c
+ * Implementation to define Tnnn timers globally and use for FSM state changes.
+ */
+/*
+ * (C) 2018-2019 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
+ *
+ * All Rights Reserved
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ * Author: Neels Hofmeyr <neels@hofmeyr.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation; either version 3 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <limits.h>
+
+#include <osmocom/core/fsm.h>
+#include <osmocom/core/tdef.h>
+
+/*! \addtogroup Tdef
+ *
+ * Implementation to define Tnnn timers globally and use for FSM state changes.
+ *
+ * See also \ref Tdef_VTY
+ *
+ * osmo_tdef provides:
+ *
+ * - a list of Tnnnn (GSM) timers with description, unit and default value.
+ * - vty UI to allow users to configure non-default timeouts.
+ * - API to tie T timers to osmo_fsm states and set them on state transitions.
+ *
+ * - a few standard units (minute, second, millisecond) as well as a custom unit
+ * (which relies on the timer's human readable description to indicate the
+ * meaning of the value).
+ * - conversion for standard units: for example, some GSM timers are defined in
+ * minutes, while our FSM definitions need timeouts in seconds. Conversion is
+ * for convenience only and can be easily avoided via the custom unit.
+ *
+ * By keeping separate osmo_tdef arrays, several groups of timers can be kept
+ * separately. The VTY tests in tests/tdef/ showcase different schemes:
+ *
+ * - \ref tests/vty/tdef_vty_test_config_root.c:
+ * Keep several timer definitions in separately named groups: showcase the
+ * osmo_tdef_vty_groups*() API. Each timer group exists exactly once.
+ *
+ * - \ref tests/vty/tdef_vty_test_config_subnode.c:
+ * Keep a single list of timers without separate grouping.
+ * Put this list on a specific subnode below the CONFIG_NODE.
+ * There could be several separate subnodes with timers like this, i.e.
+ * continuing from this example, sets of timers could be separated by placing
+ * timers in specific config subnodes instead of using the global group name.
+ *
+ * - \ref tests/vty/tdef_vty_test_dynamic.c:
+ * Dynamically allocate timer definitions per each new created object.
+ * Thus there can be an arbitrary number of independent timer definitions, one
+ * per allocated object.
+ *
+ * osmo_tdef was introduced because:
+ *
+ * - without osmo_tdef, each invocation of osmo_fsm_inst_state_chg() needs to be
+ * programmed with the right timeout value, for all code paths that invoke this
+ * state change. It is a likely source of errors to get one of them wrong. By
+ * defining a T timer exactly for an FSM state, the caller can merely invoke the
+ * state change and trust on the original state definition to apply the correct
+ * timeout.
+ *
+ * - it is helpful to have a standardized config file UI to provide user
+ * configurable timeouts, instead of inventing new VTY commands for each
+ * separate application of T timer numbers. See \ref tdef_vty.h.
+ *
+ * @{
+ * \file tdef.c
+ */
+
+/*! a = return_val * b. \return 0 if factor is below 1. */
+static unsigned long osmo_tdef_factor(enum osmo_tdef_unit a, enum osmo_tdef_unit b)
+{
+ if (b == a
+ || b == OSMO_TDEF_CUSTOM || a == OSMO_TDEF_CUSTOM)
+ return 1;
+
+ switch (b) {
+ case OSMO_TDEF_MS:
+ switch (a) {
+ case OSMO_TDEF_S:
+ return 1000;
+ case OSMO_TDEF_M:
+ return 60*1000;
+ default:
+ return 0;
+ }
+ case OSMO_TDEF_S:
+ switch (a) {
+ case OSMO_TDEF_M:
+ return 60;
+ default:
+ return 0;
+ }
+ default:
+ return 0;
+ }
+}
+
+/*! \return val in unit to_unit, rounded up to the next integer value and clamped to ULONG_MAX, or 0 if val == 0. */
+static unsigned long osmo_tdef_round(unsigned long val, enum osmo_tdef_unit from_unit, enum osmo_tdef_unit to_unit)
+{
+ unsigned long f;
+ if (!val)
+ return 0;
+
+ f = osmo_tdef_factor(from_unit, to_unit);
+ if (f == 1)
+ return val;
+ if (f < 1) {
+ f = osmo_tdef_factor(to_unit, from_unit);
+ return (val / f) + (val % f? 1 : 0);
+ }
+ /* range checking */
+ if (f > (ULONG_MAX / val))
+ return ULONG_MAX;
+ return val * f;
+}
+
+/*! Set all osmo_tdef values to the default_val.
+ * It is convenient to define a tdefs array by setting only the default_val, and calling osmo_tdefs_reset() once for
+ * program startup. (See also osmo_tdef_vty_init())
+ * \param[in] tdefs Array of timer definitions, last entry being fully zero.
+ */
+void osmo_tdefs_reset(struct osmo_tdef *tdefs)
+{
+ struct osmo_tdef *t;
+ osmo_tdef_for_each(t, tdefs)
+ t->val = t->default_val;
+}
+
+/*! Return the value of a T timer from a list of osmo_tdef, in the given unit.
+ * If no such timer is defined, return the default value passed, or abort the program if default < 0.
+ *
+ * Round up any value match as_unit: 1100 ms as OSMO_TDEF_S becomes 2 seconds, as OSMO_TDEF_M becomes one minute.
+ * However, always return a value of zero as zero (0 ms as OSMO_TDEF_M still is 0 m).
+ *
+ * Range: even though the value range is unsigned long here, in practice, using ULONG_MAX as value for a timeout in
+ * seconds may actually wrap to negative or low timeout values (e.g. in struct timeval). It is recommended to stay below
+ * INT_MAX seconds. See also osmo_fsm_inst_state_chg().
+ *
+ * Usage example:
+ *
+ * struct osmo_tdef global_T_defs[] = {
+ * { .T=7, .default_val=50, .desc="Water Boiling Timeout" }, // default is .unit=OSMO_TDEF_S == 0
+ * { .T=8, .default_val=300, .desc="Tea brewing" },
+ * { .T=9, .default_val=5, .unit=OSMO_TDEF_M, .desc="Let tea cool down before drinking" },
+ * { .T=10, .default_val=20, .unit=OSMO_TDEF_M, .desc="Forgot to drink tea while it's warm" },
+ * {} // <-- important! last entry shall be zero
+ * };
+ * osmo_tdefs_reset(global_T_defs); // make all values the default
+ * osmo_tdef_vty_init(global_T_defs, CONFIG_NODE);
+ *
+ * val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_S, -1); // -> 50
+ * sleep(val);
+ *
+ * val = osmo_tdef_get(global_T_defs, 7, OSMO_TDEF_M, -1); // 50 seconds becomes 1 minute -> 1
+ * sleep_minutes(val);
+ *
+ * val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, 3); // not defined, returns 3
+ *
+ * val = osmo_tdef_get(global_T_defs, 99, OSMO_TDEF_S, -1); // not defined, program aborts!
+ *
+ * \param[in] tdefs Array of timer definitions, last entry must be fully zero initialized.
+ * \param[in] T Timer number to get the value for.
+ * \param[in] as_unit Return timeout value in this unit.
+ * \param[in] val_if_not_present Fallback value to return if no timeout is defined.
+ * \return Timeout value in the unit given by as_unit, rounded up if necessary, or val_if_not_present.
+ */
+unsigned long osmo_tdef_get(const struct osmo_tdef *tdefs, int T, enum osmo_tdef_unit as_unit, unsigned long val_if_not_present)
+{
+ const struct osmo_tdef *t = osmo_tdef_get_entry((struct osmo_tdef*)tdefs, T);
+ if (!t) {
+ OSMO_ASSERT(val_if_not_present >= 0);
+ return val_if_not_present;
+ }
+ return osmo_tdef_round(t->val, t->unit, as_unit);
+}
+
+/*! Find tdef entry matching T.
+ * This is useful for manipulation, which is usually limited to the VTY configuration. To retrieve a timeout value,
+ * most callers probably should use osmo_tdef_get() instead.
+ * \param[in] tdefs Array of timer definitions, last entry being fully zero.
+ * \param[in] T Timer number to get the entry for.
+ * \return osmo_tdef entry matching T in given array, or NULL if no match is found.
+ */
+struct osmo_tdef *osmo_tdef_get_entry(struct osmo_tdef *tdefs, int T)
+{
+ struct osmo_tdef *t;
+ osmo_tdef_for_each(t, tdefs) {
+ if (t->T == T)
+ return t;
+ }
+ return NULL;
+}
+
+/*! Using osmo_tdef for osmo_fsm_inst: find a given state's osmo_tdef_state_timeout entry.
+ *
+ * The timeouts_array shall contain exactly 32 elements, regardless whether only some of them are actually populated
+ * with nonzero values. 32 corresponds to the number of states allowed by the osmo_fsm_* API. Lookup is by array index.
+ * Not populated entries imply a state change invocation without timeout.
+ *
+ * For example:
+ *
+ * struct osmo_tdef_state_timeout my_fsm_timeouts[32] = {
+ * [MY_FSM_STATE_3] = { .T = 423 }, // look up timeout configured for T423
+ * [MY_FSM_STATE_7] = { .T = 235 },
+ * [MY_FSM_STATE_8] = { .keep_timer = true }, // keep previous state's T number, continue timeout.
+ * // any state that is omitted will remain zero == no timeout
+ * };
+ * osmo_tdef_get_state_timeout(MY_FSM_STATE_0, &my_fsm_timeouts) -> NULL,
+ * osmo_tdef_get_state_timeout(MY_FSM_STATE_7, &my_fsm_timeouts) -> { .T = 235 }
+ *
+ * The intention is then to obtain the timer like osmo_tdef_get(global_T_defs, T=235); see also
+ * fsm_inst_state_chg_T() below.
+ *
+ * \param[in] state State constant to look up.
+ * \param[in] timeouts_array Array[32] of struct osmo_tdef_state_timeout defining which timer number to use per state.
+ * \return A struct osmo_tdef_state_timeout entry, or NULL if that entry is zero initialized.
+ */
+const struct osmo_tdef_state_timeout *osmo_tdef_get_state_timeout(uint32_t state, const struct osmo_tdef_state_timeout *timeouts_array)
+{
+ const struct osmo_tdef_state_timeout *t;
+ OSMO_ASSERT(state < 32);
+ t = &timeouts_array[state];
+ if (!t->keep_timer && !t->T)
+ return NULL;
+ return t;
+}
+
+/*! See invocation macro osmo_tdef_fsm_inst_state_chg() instead.
+ * \param[in] file Source file name, like __FILE__.
+ * \param[in] line Source file line number, like __LINE__.
+ */
+int _osmo_tdef_fsm_inst_state_chg(struct osmo_fsm_inst *fi, uint32_t state,
+ const struct osmo_tdef_state_timeout *timeouts_array,
+ const struct osmo_tdef *tdefs, unsigned long default_timeout,
+ const char *file, int line)
+{
+ const struct osmo_tdef_state_timeout *t = osmo_tdef_get_state_timeout(state, timeouts_array);
+ unsigned long val;
+
+ /* No timeout defined for this state? */
+ if (!t)
+ return _osmo_fsm_inst_state_chg(fi, state, 0, 0, file, line);
+
+ if (t->keep_timer) {
+ int rc = _osmo_fsm_inst_state_chg_keep_timer(fi, state, file, line);
+ if (t->T && !rc)
+ fi->T = t->T;
+ return rc;
+ }
+
+ val = osmo_tdef_get(tdefs, t->T, OSMO_TDEF_S, default_timeout);
+ return _osmo_fsm_inst_state_chg(fi, state, val, t->T, file, line);
+}
+
+const struct value_string osmo_tdef_unit_names[] = {
+ { OSMO_TDEF_S, "s" },
+ { OSMO_TDEF_MS, "ms" },
+ { OSMO_TDEF_M, "m" },
+ { OSMO_TDEF_CUSTOM, "custom-unit" },
+ {}
+};
+
+/*! @} */