On Fri, Jan 12, 2024 at 3:11 PM Ilya Maximets <[email protected]> wrote:
>
> On 1/12/24 12:16, Frode Nordahl wrote:
> > On Fri, Jan 12, 2024 at 12:26 AM Ilya Maximets <[email protected]> wrote:
> >>
> >> On 1/10/24 20:29, Frode Nordahl wrote:
> >>> One of the goals of Open vSwitch is to be as resource efficient as
> >>> possible.  Core parts of the program has been implemented as
> >>> asynchronous state machines, and when absolutely necessary
> >>> additional threads are used.
> >>>
> >>> Introduce cooperative multitasking module which allow us to
> >>> interleave important processing with long running tasks while
> >>> avoiding the additional resource consumption of threads and
> >>> complexity of asynchronous state machines.
> >>>
> >>> We will use this module to ensure long running processing in the
> >>> OVSDB server does not interfere with stable maintenance of the
> >>> RAFT cluster in subsequent patches.
> >>>
> >>> Suggested-by: Ilya Maximets <[email protected]>
> >>> Signed-off-by: Frode Nordahl <[email protected]>
> >>> ---
> >>>  lib/automake.mk                        |   3 +
> >>>  lib/cooperative-multitasking-private.h |  31 +++
> >>>  lib/cooperative-multitasking.c         | 195 +++++++++++++++++++
> >>>  lib/cooperative-multitasking.h         |  42 ++++
> >>>  tests/automake.mk                      |   1 +
> >>>  tests/library.at                       |  10 +
> >>>  tests/ovsdb-server.at                  |   1 +
> >>>  tests/test-cooperative-multitasking.c  | 259 +++++++++++++++++++++++++
> >>>  8 files changed, 542 insertions(+)
> >>>  create mode 100644 lib/cooperative-multitasking-private.h
> >>>  create mode 100644 lib/cooperative-multitasking.c
> >>>  create mode 100644 lib/cooperative-multitasking.h
> >>>  create mode 100644 tests/test-cooperative-multitasking.c
> >>
> >> Thanks for working on this!  See some comments inline.
> >
> > Thank you for providing the counter proposal for this approach, I
> > think it will turn out much better than the original approach.
> >
> >>>
> >>> diff --git a/lib/automake.mk b/lib/automake.mk
> >>> index 0dc8a35cc..8596171c6 100644
> >>> --- a/lib/automake.mk
> >>> +++ b/lib/automake.mk
> >>> @@ -94,6 +94,9 @@ lib_libopenvswitch_la_SOURCES = \
> >>>       lib/conntrack-other.c \
> >>>       lib/conntrack.c \
> >>>       lib/conntrack.h \
> >>> +     lib/cooperative-multitasking.c \
> >>> +     lib/cooperative-multitasking.h \
> >>> +     lib/cooperative-multitasking-private.h \
> >>>       lib/coverage.c \
> >>>       lib/coverage.h \
> >>>       lib/cpu.c \
> >>> diff --git a/lib/cooperative-multitasking-private.h 
> >>> b/lib/cooperative-multitasking-private.h
> >>> new file mode 100644
> >>> index 000000000..b2e4e7291
> >>> --- /dev/null
> >>> +++ b/lib/cooperative-multitasking-private.h
> >>> @@ -0,0 +1,31 @@
> >>> +/*
> >>> + * Copyright (c) 2024 Canonical Ltd.
> >>> + *
> >>> + * Licensed under the Apache License, Version 2.0 (the "License");
> >>> + * you may not use this file except in compliance with the License.
> >>> + * You may obtain a copy of the License at:
> >>> + *
> >>> + *     http://www.apache.org/licenses/LICENSE-2.0
> >>> + *
> >>> + * Unless required by applicable law or agreed to in writing, software
> >>> + * distributed under the License is distributed on an "AS IS" BASIS,
> >>> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
> >>> implied.
> >>> + * See the License for the specific language governing permissions and
> >>> + * limitations under the License.
> >>> + */
> >>> +
> >>> +#ifndef COOPERATIVE_MULTITASKING_PRIVATE_H
> >>> +#define COOPERATIVE_MULTITASKING_PRIVATE_H 1
> >>> +
> >>> +#include "openvswitch/hmap.h"
> >>> +
> >>> +struct cooperative_multitasking_callback {
> >>> +    struct hmap_node node;
> >>> +    void (*cb)(void *);
> >>> +    void *arg;
> >>> +    long long int time_threshold;
> >>> +    long long int last_run;
> >>> +    const char *msg;
> >>> +};
> >>> +
> >>> +#endif /* COOPERATIVE_MULTITASKING_PRIVATE_H */
> >>> diff --git a/lib/cooperative-multitasking.c 
> >>> b/lib/cooperative-multitasking.c
> >>> new file mode 100644
> >>> index 000000000..1b1205e8f
> >>> --- /dev/null
> >>> +++ b/lib/cooperative-multitasking.c
> >>> @@ -0,0 +1,195 @@
> >>> +/*
> >>> + * Copyright (c) 2023 Canonical Ltd.
> >>
> >> 2024 ? :)
> >>
> >> Same for other files.
> >
> > I started the development before the break, so the files were actually
> > created in 2023, and we are now indeed in 2024 so it makes sense to
> > update :)
> >
> >>> + *
> >>> + * Licensed under the Apache License, Version 2.0 (the "License");
> >>> + * you may not use this file except in compliance with the License.
> >>> + * You may obtain a copy of the License at:
> >>> + *
> >>> + *     http://www.apache.org/licenses/LICENSE-2.0
> >>> + *
> >>> + * Unless required by applicable law or agreed to in writing, software
> >>> + * distributed under the License is distributed on an "AS IS" BASIS,
> >>> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
> >>> implied.
> >>> + * See the License for the specific language governing permissions and
> >>> + * limitations under the License.
> >>> + */
> >>> +
> >>> +#include <config.h>
> >>> +
> >>> +#include "backtrace.h"
> >>> +#include "cooperative-multitasking-private.h"
> >>> +#include "cooperative-multitasking.h"
> >>> +#include "hash.h"
> >>> +#include "openvswitch/hmap.h"
> >>> +#include "openvswitch/vlog.h"
> >>> +#include "timeval.h"
> >>> +
> >>> +VLOG_DEFINE_THIS_MODULE(cooperative_multitasking);
> >>> +
> >>> +static struct hmap *cooperative_multitasking_callbacks = NULL;
> >>
> >> I wonder if we can use a shorter name.  You're using 'cm_callbacks'
> >> in the test, for example.  It is an internal hash map after all.
> >> Probably, no need to have an extra fancy name.
> >
> > I was mostly concerned with consistent namespacing since it is a
> > "global" variable, as-is it is contained to the module as you point
> > out.
> >
> > However, if we're going to share it with the unit test through extern
> > as proposed below I suggest we keep the long name to avoid potential
> > symbol clashes?
>
> I'm not concerned much about symbol clashes, since the header must
> not be included anywhere, except for the module itself and the test
> code.  The test already knows too much about internals of the module,
> so we can expect it to be aware of the internal naming.  But it is
> up to you.

Hum, I'm sure I'm doing something different than what you intended
with your comment here then, with:
lib/cooperative-multitasking.c:
struct hmap cooperative_multitasking_callbacks = HMAP_INITIALIZER(
    &cooperative_multitasking_callbacks);

lib/cooperative-multitasking-private.h
extern struct hmap cooperative_multitasking_callbacks;

The linker will see `cooperative_multitasking_callbacks` as a global symbol:
$ nm ovsdb/ovsdb-server|grep callbacks
0000000000245598 D cooperative_multitasking_callbacks

and subsequently it will not be available for use in other modules,
regardless of which header is included.

-- 
Frode Nordahl

> >
> >>> +
> >>> +/* One time initialization for process that wants to make use of 
> >>> cooperative
> >>> + * multitasking module.  References to data is stored in 
> >>> 'hmap_container' and
> >>> + * will be referenced by all calls to this module.  The ownership of the
> >>> + * container itself remains with the caller while the data in the hmap 
> >>> is owned
> >>> + * by this module and must be freed with a call to
> >>> + * cooperative_multitasking_destroy().
> >>> + *
> >>> + * The purpose of having the caller own 'hmap_container' is:
> >>> + * 1) Allow runtime decision whether to use cooperative multitasking 
> >>> without
> >>> + *    having to pass data between loosely connected parts of a program.  
> >>> This
> >>> + *    is useful for the raft code which is consumed by both the 
> >>> ovsdb-server
> >>> + *    daemon and the ovsdb-tool CLI utility.
> >>> + * 2) Allow inspection of internal data by unit tests. */
> >>
> >> Can we instead define the hash map here and export it as extern in the
> >> internal header?  It looks a little strange to make a module to hold a
> >> static hash map.  At the same time no two modules can call init.
> >> It can also be statically initialized with HMAP_INITIALIZER(), so no
> >> init function will be necessary.
> >
> > I was on that path initially, but at the time I did not have the
> > private header so there was no comfortable way to share it, and I
> > somehow convinced myself that we needed to conditionally enable this
> > for the dual use of raft code from ovsdb-server / ovsdb-tool, but
> > there is no such need.
> >
> > Will drop _init() and initialize the container in the library instead
> > as suggested.
> >
> >>> +void
> >>> +cooperative_multitasking_init(struct hmap *hmap_container)
> >>> +{
> >>> +    cooperative_multitasking_callbacks = hmap_container;
> >>> +    hmap_init(cooperative_multitasking_callbacks);
> >>> +}
> >>> +
> >>> +/* Register callback 'cb' with argument 'arg' to be called when
> >>> + * cooperating long running functions yield and 'time_threshold' msec has
> >>> + * passed since the last call to the function.  If the optional 'msg' is 
> >>> not
> >>> + * NULL it will be used when logging time threshold overrun conditions.
> >>> + *
> >>> + * It is possible to register the same callback multiple times as long 
> >>> as 'arg'
> >>> + * is different for each registration.  It is up to the caller to ensure 
> >>> no
> >>> + * unwanted duplicates are registered.
> >>> + *
> >>> + * The callback is expected to update the timestamp for last run with a 
> >>> call to
> >>> + * cooperative_multitasking_update() using the same values for 'cb' and 
> >>> 'arg'.
> >>> + */
> >>> +void
> >>> +cooperative_multitasking_register(void (*cb)(void *), void *arg,
> >>> +                                  long long int time_threshold,
> >>> +                                  const char *msg)
> >>> +{
> >>> +    if (!cooperative_multitasking_callbacks) {
> >>> +        return;
> >>> +    }
> >>> +
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>
> >> The structure is named as 'callback', but you're always using
> >> 'entry' as a variable name.  Maybe rename the structure?
> >
> > Ack, I guess we could even shorten it to 'cm_entry' as it's only used
> > internally.
>
> Agreed.
>
> >
> >>> +
> >>> +    cm_entry = xzalloc(sizeof *cm_entry);
> >>> +    cm_entry->cb = cb;
> >>> +    cm_entry->arg = arg;
> >>> +    cm_entry->time_threshold = time_threshold;
> >>> +    cm_entry->last_run = time_msec();
> >>> +    cm_entry->msg = msg;
> >>> +
> >>> +    hmap_insert(cooperative_multitasking_callbacks,
> >>> +                &cm_entry->node,
> >>> +                hash_pointer(
> >>> +                    cm_entry->arg ? cm_entry->arg : (void *) 
> >>> cm_entry->cb, 0));
> >>
> >> Why hashing the arg instead of callback?  I suppose that makes
> >> sense in context that we'll have only one user with a single
> >> function and different arguments.  But it seems backwards from
> >> the point of view of a library.
> >>
> >> In practice, ovsdb-server usually serves only one clustered
> >> database, so there should be no difference either way.
> >> If it's 2 databases, should also not be a problem.  But hashing
> >> the callback seems more logical.
> >>
> >> What do you think?
> >
> > Ack, I guess you caught me in premature/unneeded optimization, it's
> > not like there will be thousands of entries for the same value of 'cb'
> > with different 'arg'.
>
> I certainly hope so. :)  We would need to hash both in this case,
> I guess.
>
> >
> >>
> >>> +}
> >>> +
> >>> +/* Free any data allocated by calls to 
> >>> cooperative_multitasking_register(). */
> >>> +void
> >>> +cooperative_multitasking_destroy(void)
> >>> +{
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>> +    HMAP_FOR_EACH_SAFE (cm_entry, node, 
> >>> cooperative_multitasking_callbacks) {
> >>> +        hmap_remove(cooperative_multitasking_callbacks, &cm_entry->node);
> >>> +        free(cm_entry);
> >>> +    }
> >>> +}
> >>> +
> >>> +/* Update data for already registered callback identified by 'cb' and 
> >>> 'arg'.
> >>> + *
> >>> + * The value for 'last_run' must at a minimal be updated each time the 
> >>> callback
> >>> + * is run.  It can also be useful to update for multiple entry points to 
> >>> the
> >>> + * part serviced by the callback to avoid unnecessary callbacks on next 
> >>> call to
> >>> + * cooperative_multitasking_yield().
> >>
> >> I'm not sure I understand this last sentence.
> >
> > It's indeed a bit convoluted, so I'll reword it.
>
> Thanks!
>
> >
> >>> + *
> >>> + * Updating the value for 'time_threshold' may be necessary as a 
> >>> consequence of
> >>> + * the change in runtime configuration or requirements of the serviced
> >>> + * callback.
> >>> + *
> >>> + * Providing a value of 0 for 'last_run' or 'time_threshold' will result 
> >>> in
> >>> + * the respective stored value left untouched. */
> >>> +void
> >>> +cooperative_multitasking_update(void (*cb)(void *), void *arg,
> >>> +                                long long int last_run,
> >>> +                                long long int time_threshold)
> >>> +{
> >>> +    if (!cooperative_multitasking_callbacks) {
> >>> +        return;
> >>> +    }
> >>> +
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>> +
> >>> +    HMAP_FOR_EACH_WITH_HASH (cm_entry, node,
> >>> +                             hash_pointer(arg ? arg : (void *) cb, 0),
> >>> +                             cooperative_multitasking_callbacks)
> >>> +    {
> >>> +        if (cm_entry->cb == cb && cm_entry->arg == arg) {
> >>> +            if (last_run) {
> >>> +                cm_entry->last_run = last_run;
> >>> +            }
> >>> +
> >>> +            if (time_threshold) {
> >>> +                cm_entry->time_threshold = time_threshold;
> >>> +            }
> >>> +            return;
> >>
> >> The register() function doesn't check for duplicates, so there
> >> can be duplicates if used inaccurately and they will not be
> >> updated.
> >>
> >> Should we just have a single function like _set() that would
> >> find and update or create if not found?  Instead of relying on
> >> users to not add the same thing twice.
> >
> > Yes, that makes sense. The fact that no duplicate check is done was
> > documented, but condensing this to a single _set() will most likely
> > make that behavior even more clear to the caller.
> >
> >>> +        }
> >>> +    }
> >>> +}
> >>> +
> >>> +static void
> >>> +cooperative_multitasking_yield_at__(const char *source_location)
> >>> +{
> >>> +    long long int now = time_msec();
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>
> >> Reverse x-mass tree.
> >
> > Ack.
> >
> >>> +
> >>> +    HMAP_FOR_EACH (cm_entry, node, cooperative_multitasking_callbacks) {
> >>> +        long long int elapsed = now - cm_entry->last_run;
> >>> +
> >>> +        if (elapsed >= cm_entry->time_threshold) {
> >>> +            VLOG_DBG("yield called from %s: "
> >>> +                     "%lld: %lld >= %lld, executing %p(%p)",
> >>> +                     source_location, now, elapsed, 
> >>> cm_entry->time_threshold,
> >>> +                     cm_entry->cb, cm_entry->arg);
> >>
> >> Maybe an empty line here.
> >> Also, the pointers are not very informative.  Should something
> >> like 'name' be added to the structure?  So, it will be %s(%p).
> >>
> >> Not sure why logging 'now'.  Maybe re-organize things a little:
> >>
> >> "%{source}: yield for %{name}: elapsed(%lld) >= threshold(%lld), overrun: 
> >> %lld"
> >>
> >> What do you think?
> >>
> >>> +            (*cm_entry->cb)(cm_entry->arg);
> >>> +            if (elapsed - cm_entry->time_threshold >
> >>> +                cm_entry->time_threshold / 8)
> >>
> >> Callback updates thresholds, so the check should be done before
> >> calling it.   Maybe 'time_' part of the structure filed is
> >> redundant.  Without it the lines will be shorter.
> >
> > Thank you for pointing out that the callback can update the threshold,
> > that fact eluded me in some iteration.
> >
> > My original thought for splitting this across two calls was to avoid
> > delaying the callback even further by spending time on producing the
> > backtrace. But I guess it's not that expensive and it is only
> > performed during debugging.  Doing it before also avoids polluting the
> > backtrace with the call to the callback.
> >
> >>> +            {
> >>
> >> Should be on a previous line.
> >>
> >>> +                VLOG_WARN("yield threshold overrun with %lld msec. %s",
> >>> +                          elapsed - cm_entry->time_threshold,
> >>> +                          cm_entry->msg ? cm_entry->msg : "");
> >>
> >> If we add a name, we should print the callback name here as well.
> >> And we may even drop the 'msg', because it will be more or less
> >> clear what went wrong if we know that it is a raft_run, for example.
> >>
> >> Also, we could combine two log calls into one by using base VLOG()
> >> macro and choosing the log level conditionally, e.g.:
> >>
> >>     bool warn = elapsed - entry->threshold > entry->threshold / 8;
> >>
> >>     VLOG(warn ? VLL_WARN : VLL_DBG, ... );
> >>     if (warn && VLOG_IS_DBG_ENABLED()) {
> >>         backtrace
> >>     }
> >>     entry->cb();
> >
> > Yes, that is indeed much nicer, thx!
> >
> >>> +                if (VLOG_IS_DBG_ENABLED()) {
> >>> +                    /* log_backtrace() logs at ERROR level but we only 
> >>> want to
> >>> +                     * log a backtrace when DEBUG is enabled */
> >>> +                    log_backtrace();
> >>> +                }
> >>> +            }
> >>
> >> Should we also update 'now'?  The callback itself might have taken
> >> some time.
> >
> > Yes, good point.
> >
> >>> +        }
> >>> +    }
> >>
> >> Maybe log a warning if overall yield run took more than, let's say, a 
> >> second?
> >
> > Ack.
> >
> >>> +}
> >>> +
> >>> +/* Iterate over registered callbacks and execute callbacks as demanded 
> >>> by the
> >>> + * recorded time threshold. */
> >>> +void
> >>> +cooperative_multitasking_yield_at(const char *source_location)
> >>> +{
> >>> +    static bool yield_in_progress = false;
> >>> +
> >>> +    if (!cooperative_multitasking_callbacks) {
> >>> +        return;
> >>> +    }
> >>> +
> >>> +    if (yield_in_progress) {
> >>> +        VLOG_ERR_ONCE("nested yield avoided, this is a bug! "
> >>> +                      "enable debug logging for more details.");
> >>
> >> Might be better to capitalize the first letters in sentences.
> >> The second one is bothering me :) , but if we capitalize the
> >> second, then we also need to do the first.
> >
> > Ack.
> >
> >>> +        if (VLOG_IS_DBG_ENABLED()) {
> >>> +            VLOG_DBG("nested yield, called from %s", source_location);
> >>> +            log_backtrace();
> >>> +        }
> >>> +        return;
> >>> +    }
> >>> +    yield_in_progress = true;
> >>> +
> >>> +    cooperative_multitasking_yield_at__(source_location);
> >>> +
> >>> +    yield_in_progress = false;
> >>> +}
> >>> diff --git a/lib/cooperative-multitasking.h 
> >>> b/lib/cooperative-multitasking.h
> >>> new file mode 100644
> >>> index 000000000..6286bfbf5
> >>> --- /dev/null
> >>> +++ b/lib/cooperative-multitasking.h
> >>> @@ -0,0 +1,42 @@
> >>> +/*
> >>> + * Copyright (c) 2023 Canonical Ltd.
> >>> + *
> >>> + * Licensed under the Apache License, Version 2.0 (the "License");
> >>> + * you may not use this file except in compliance with the License.
> >>> + * You may obtain a copy of the License at:
> >>> + *
> >>> + *     http://www.apache.org/licenses/LICENSE-2.0
> >>> + *
> >>> + * Unless required by applicable law or agreed to in writing, software
> >>> + * distributed under the License is distributed on an "AS IS" BASIS,
> >>> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
> >>> implied.
> >>> + * See the License for the specific language governing permissions and
> >>> + * limitations under the License.
> >>> + */
> >>> +
> >>> +#ifndef COOPERATIVE_MULTITASKING_H
> >>> +#define COOPERATIVE_MULTITASKING_H 1
> >>> +
> >>
> >> I think, we need a comment here describing the library and some use cases.
> >> At least a little bit.  It should also have a thread safety section
> >> clearly stating that the library is not thread safe can only be used from
> >> one thread.
> >
> > Ack.
> >
> >>> +struct hmap;
> >>> +
> >>> +void cooperative_multitasking_init(struct hmap *);
> >>> +
> >>> +void cooperative_multitasking_register(void (*)(void *), void *,
> >>> +                                       long long int, const char *);
> >>> +#define COOPERATIVE_MULTITASKING_REGISTER(CB, ARG, TIME_THRESHOLD, MSG)  
> >>>      \
> >>> +    cooperative_multitasking_register((void (*)(void *)) CB, (void *) 
> >>> ARG,    \
> >>> +                                      TIME_THRESHOLD, MSG)
> >>> +
> >>> +void cooperative_multitasking_destroy(void);
> >>
> >> We probably need an unregister/remove-style function as well.
> >> For example, if we remove clustered database in runtime, we need
> >> to make sure the callback will not be called for a destroyed
> >> raft structure.
> >
> > Yes, that is a good point. I'll add that.
> >
> >>> +
> >>> +void cooperative_multitasking_update(void (*)(void *), void *, long long 
> >>> int,
> >>> +                                     long long int);
> >>> +#define COOPERATIVE_MULTITASKING_UPDATE(CB, ARG, LAST_RUN, 
> >>> TIME_THRESHOLD)    \
> >>> +    cooperative_multitasking_update((void (*) (void *)) CB, (void *) 
> >>> ARG,     \
> >>> +                                    LAST_RUN, TIME_THRESHOLD)
> >>
> >> I don't think we should have these macros.  I see that their purpose
> >> is to hide pointer conversions, but unfortunately we can't really do that.
> >> We need to make sure that all the callbacks actually have void (*) (void 
> >> *))
> >> prototypes and the arguments of these callbacks are actually void pointers.
> >>
> >> The problem is that llvm devs decided that it is an undefined behavior
> >> to call functions via pointer with a different type:
> >>   http://reviews.llvm.org/D148827#4379764
> >> And now UBSAN in clang-17 is complaining all over the place for multiple
> >> projects:
> >>   https://github.com/systemd/systemd/issues/29972
> >>   https://github.com/openssl/openssl/issues/22896
> >>
> >> This also affects OVS in rcu callbacks, but we didn't get around fixing
> >> this yet, as release right now is much more important.
> >> However, it'll be better to not introduce more issues like this.
> >>
> >> Unfortunately we'll likely have to have a wrapper function like:
> >>
> >>   void raft_run_wrap(void *arg)
> >>   {
> >>       struct raft *raft = (struct raft *) arg;
> >>
> >>       raft_run(raft);
> >>   }
> >>
> >> And use that as a callback...  It is ugly and looks very unnecessary,
> >> but llvm folks are not backing down from this and it looks like the only
> >> option for generic libraries like RCU or coop-multitasking.
> >
> > Thank you for elaborating on this, I was unaware of this looming
> > compiler change on the horizon.
> >
> >>> +
> >>> +void cooperative_multitasking_yield_at(const char *);
> >>> +#define cooperative_multitasking_yield() \
> >>> +    cooperative_multitasking_yield_at(OVS_SOURCE_LOCATOR)
> >>> +
> >>> +#endif /* COOPERATIVE_MULTITASKING_H */
> >>> diff --git a/tests/automake.mk b/tests/automake.mk
> >>> index 10c9fbb01..08c9b74d4 100644
> >>> --- a/tests/automake.mk
> >>> +++ b/tests/automake.mk
> >>> @@ -456,6 +456,7 @@ tests_ovstest_SOURCES = \
> >>>       tests/test-ccmap.c \
> >>>       tests/test-cmap.c \
> >>>       tests/test-conntrack.c \
> >>> +     tests/test-cooperative-multitasking.c \
> >>>       tests/test-csum.c \
> >>>       tests/test-flows.c \
> >>>       tests/test-hash.c \
> >>> diff --git a/tests/library.at b/tests/library.at
> >>> index 3f9df2f87..77d5abb01 100644
> >>> --- a/tests/library.at
> >>> +++ b/tests/library.at
> >>> @@ -296,3 +296,13 @@ AT_CLEANUP
> >>>  AT_SETUP([uuidset module])
> >>>  AT_CHECK([ovstest test-uuidset], [0], [], [ignore])
> >>>  AT_CLEANUP
> >>> +
> >>> +AT_SETUP([cooperative-multitasking module])
> >>> +AT_CHECK([ovstest test-cooperative-multitasking], [0], [])
> >>> +AT_CLEANUP
> >>> +
> >>> +AT_SETUP([cooperative-multitasking module nested yield detection])
> >>> +AT_CHECK([ovstest test-cooperative-multitasking-nested-yield], [0], [], 
> >>> [dnl
> >>> +cooperative_multitasking|ERR|nested yield avoided, this is a bug! enable 
> >>> debug logging for more details.
> >>> +])
> >>> +AT_CLEANUP
> >>> diff --git a/tests/ovsdb-server.at b/tests/ovsdb-server.at
> >>> index 6eb758e22..88a9a9a27 100644
> >>> --- a/tests/ovsdb-server.at
> >>> +++ b/tests/ovsdb-server.at
> >>> @@ -2387,6 +2387,7 @@ m4_define([CLEAN_LOG_FILE],
> >>>    [sed 's/[[0-9\-]]*T[[0-9:\.]]*Z|[[0-9]]*\(|.*$\)/\1/g' $1 | dnl
> >>>     sed '/|poll_loop|/d' |   dnl
> >>>     sed '/|socket_util|/d' | dnl
> >>> +   sed '/|cooperative_multitasking|DBG|/d' | dnl
> >>>     sed 's/[[0-9]]*\.ctl/<cleared>\.ctl/g'> $2])
> >>>
> >>>  CLEAN_LOG_FILE([1.log], [1.log.clear])
> >>> diff --git a/tests/test-cooperative-multitasking.c 
> >>> b/tests/test-cooperative-multitasking.c
> >>> new file mode 100644
> >>> index 000000000..ec6be865f
> >>> --- /dev/null
> >>> +++ b/tests/test-cooperative-multitasking.c
> >>> @@ -0,0 +1,259 @@
> >>> +/*
> >>> + * Copyright (c) 2023 Canonical Ltd.
> >>> + *
> >>> + * Licensed under the Apache License, Version 2.0 (the "License");
> >>> + * you may not use this file except in compliance with the License.
> >>> + * You may obtain a copy of the License at:
> >>> + *
> >>> + *     http://www.apache.org/licenses/LICENSE-2.0
> >>> + *
> >>> + * Unless required by applicable law or agreed to in writing, software
> >>> + * distributed under the License is distributed on an "AS IS" BASIS,
> >>> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 
> >>> implied.
> >>> + * See the License for the specific language governing permissions and
> >>> + * limitations under the License.
> >>> + */
> >>> +
> >>> +#include <config.h>
> >>> +#undef NDEBUG
> >>> +#include "cooperative-multitasking.h"
> >>> +#include "cooperative-multitasking-private.h"
> >>> +#include "openvswitch/hmap.h"
> >>> +#include "ovstest.h"
> >>> +#include "timeval.h"
> >>> +#include "util.h"
> >>> +#include "openvswitch/vlog.h"
> >>> +
> >>> +static struct hmap cm_callbacks;
> >>> +
> >>> +struct fixture_arg {
> >>> +    bool called;
> >>> +};
> >>> +
> >>> +static void
> >>> +fixture_run(struct fixture_arg *arg)
> >>
> >> These will have to have a void * arguments...
> >
> > I'll add the wrap functions to the test as well.
> >
> >>> +{
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_run, arg, time_msec(), 0);
> >>> +    if (arg) {
> >>> +        arg->called = true;
> >>> +    }
> >>> +}
> >>> +
> >>> +static void
> >>> +fixture_other_run(struct fixture_arg *arg)
> >>> +{
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_other_run, arg, 
> >>> time_msec(), 0);
> >>> +    if (arg) {
> >>> +        arg->called = true;
> >>> +    }
> >>> +}
> >>> +
> >>> +static void
> >>> +test_cm_register(void)
> >>> +{
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>> +    struct fixture_arg arg1 = {
> >>> +        .called = false,
> >>> +    };
> >>> +    struct fixture_arg arg2 = {
> >>> +        .called = false,
> >>> +    };
> >>> +
> >>> +    timeval_stop();
> >>> +    long long int now = time_msec();
> >>> +
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_run, &arg1, 1000, NULL);
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_run, &arg2, 2000, NULL);
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_other_run, NULL, 3000, 
> >>> NULL);
> >>> +
> >>> +    ovs_assert(hmap_count(&cm_callbacks) == 3);
> >>> +
> >>> +    HMAP_FOR_EACH (cm_entry, node, &cm_callbacks) {
> >>> +        if (cm_entry->arg == (void *)&arg1) {
> >>> +            ovs_assert (cm_entry->cb == (void (*)(void *)) &fixture_run);
> >>> +            ovs_assert (cm_entry->time_threshold == 1000);
> >>> +            ovs_assert (cm_entry->last_run == now);
> >>> +        } else if (cm_entry->arg == (void *)&arg2) {
> >>> +            ovs_assert (cm_entry->cb == (void (*)(void *)) &fixture_run);
> >>> +            ovs_assert (cm_entry->time_threshold == 2000);
> >>> +            ovs_assert (cm_entry->last_run == now);
> >>> +        } else if (cm_entry->cb == (void (*)(void *)) 
> >>> &fixture_other_run) {
> >>> +            ovs_assert (cm_entry->arg == NULL);
> >>> +            ovs_assert (cm_entry->time_threshold == 3000);
> >>> +            ovs_assert (cm_entry->last_run == now);
> >>
> >> Nit: no space after assert, but a space between the type and a
> >> variable in a cast.  Same for other similar parts of the test.
> >
> > Ack.
> >
> >>> +        } else {
> >>> +            OVS_NOT_REACHED();
> >>> +        }
> >>> +    }
> >>> +
> >>> +    cooperative_multitasking_destroy();
> >>> +}
> >>> +
> >>> +static void
> >>> +test_cm_update(void)
> >>> +{
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>> +    struct fixture_arg arg1 = {
> >>> +        .called = false,
> >>> +    };
> >>> +    struct fixture_arg arg2 = {
> >>> +        .called = false,
> >>> +    };
> >>> +
> >>> +    timeval_stop();
> >>> +    long long int now = time_msec();
> >>> +
> >>> +    /* first register a couple of callbacks. */
> >>
> >> Should start with a capital letter.  Same for all other comments in a file.
> >
> > Ack.
> >
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_run, &arg1, 0, NULL);
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_run, &arg2, 0, NULL);
> >>> +
> >>> +    ovs_assert(hmap_count(&cm_callbacks) == 2);
> >>> +
> >>> +    HMAP_FOR_EACH (cm_entry, node, &cm_callbacks) {
> >>> +        if (cm_entry->arg == (void *)&arg1) {
> >>> +            ovs_assert (cm_entry->time_threshold == 0);
> >>> +            ovs_assert (cm_entry->last_run == now);
> >>> +        } else if (cm_entry->arg == (void *)&arg2) {
> >>> +            ovs_assert (cm_entry->time_threshold == 0);
> >>> +            ovs_assert (cm_entry->last_run == now);
> >>> +        } else {
> >>> +            OVS_NOT_REACHED();
> >>> +        }
> >>> +    }
> >>> +
> >>> +    /* update 'last_run' and 'time_threshold' for each callback and 
> >>> validate
> >>> +     * that the correct entry was actually updated. */
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_run, &arg1, 1, 2);
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_run, &arg2, 3, 4);
> >>> +
> >>> +    HMAP_FOR_EACH (cm_entry, node, &cm_callbacks) {
> >>> +        if (cm_entry->arg == (void *)&arg1) {
> >>> +            ovs_assert (cm_entry->time_threshold == 2);
> >>> +            ovs_assert (cm_entry->last_run == 1);
> >>> +        } else if (cm_entry->arg == (void *)&arg2) {
> >>> +            ovs_assert (cm_entry->time_threshold == 4);
> >>> +            ovs_assert (cm_entry->last_run == 3);
> >>> +        } else {
> >>> +            OVS_NOT_REACHED();
> >>> +        }
> >>> +    }
> >>> +
> >>> +    /* confirm that providing 0 for 'last_run' or 'time_threshold' 
> >>> leaves the
> >>> +     * existing value untouched. */
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_run, &arg1, 0, 5);
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_run, &arg2, 6, 0);
> >>> +
> >>> +    HMAP_FOR_EACH (cm_entry, node, &cm_callbacks) {
> >>> +        if (cm_entry->arg == (void *)&arg1) {
> >>> +            ovs_assert (cm_entry->time_threshold == 5);
> >>> +            ovs_assert (cm_entry->last_run == 1);
> >>> +        } else if (cm_entry->arg == (void *)&arg2) {
> >>> +            ovs_assert (cm_entry->time_threshold == 4);
> >>> +            ovs_assert (cm_entry->last_run == 6);
> >>> +        } else {
> >>> +            OVS_NOT_REACHED();
> >>> +        }
> >>> +    }
> >>> +
> >>> +    cooperative_multitasking_destroy();
> >>> +}
> >>> +
> >>> +static void
> >>> +test_cm_yield(void)
> >>> +{
> >>> +    struct cooperative_multitasking_callback *cm_entry;
> >>> +    struct fixture_arg arg1 = {
> >>> +        .called = false,
> >>> +    };
> >>> +    struct fixture_arg arg2 = {
> >>> +        .called = false,
> >>> +    };
> >>> +
> >>> +    timeval_stop();
> >>> +    long long int now = time_msec();
> >>> +
> >>> +    /* first register a couple of callbacks. */
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_run, &arg1, 1000, NULL);
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_run, &arg2, 2000, NULL);
> >>> +
> >>> +    ovs_assert(hmap_count(&cm_callbacks) == 2);
> >>> +
> >>> +    /* call to yield should not execute callbacks until time threshold. 
> >>> */
> >>> +    cooperative_multitasking_yield();
> >>> +    ovs_assert(arg1.called == false);
> >>> +    ovs_assert(arg2.called == false);
> >>> +
> >>> +    HMAP_FOR_EACH (cm_entry, node, &cm_callbacks) {
> >>> +        ovs_assert(cm_entry->last_run == now);
> >>> +    }
> >>> +
> >>> +    /* move clock forward and confirm the expected callbacks to be 
> >>> executed. */
> >>> +    timeval_warp(0, 1000);
> >>> +    timeval_stop();
> >>> +    cooperative_multitasking_yield();
> >>> +    ovs_assert(arg1.called == true);
> >>> +    ovs_assert(arg2.called == false);
> >>> +
> >>> +    /* move clock forward and confirm the expected callbacks to be 
> >>> executed. */
> >>> +    arg1.called = arg2.called = false;
> >>> +    timeval_warp(0, 1000);
> >>> +    timeval_stop();
> >>> +    cooperative_multitasking_yield();
> >>> +    ovs_assert(arg1.called == true);
> >>> +    ovs_assert(arg2.called == true);
> >>> +
> >>> +    timeval_warp(0, 1);
> >>> +    cooperative_multitasking_destroy();
> >>> +}
> >>> +
> >>> +static void
> >>> +fixture_buggy_run(struct fixture_arg *arg)
> >>> +{
> >>> +    COOPERATIVE_MULTITASKING_UPDATE(&fixture_buggy_run, arg, 
> >>> time_msec(), 0);
> >>> +    if (arg) {
> >>> +        arg->called = true;
> >>> +    }
> >>> +    /* A real run function MUST NOT directly or indirectly call yield, 
> >>> this is
> >>> +     * here to test the detection of such a programming error. */
> >>> +    cooperative_multitasking_yield();
> >>> +}
> >>> +
> >>> +static void
> >>> +test_cooperative_multitasking_nested_yield(int argc OVS_UNUSED, char 
> >>> *argv[])
> >>> +{
> >>> +    struct fixture_arg arg1 = {
> >>> +        .called = false,
> >>> +    };
> >>> +
> >>> +    set_program_name(argv[0]);
> >>> +    vlog_set_pattern(VLF_CONSOLE, "%c|%p|%m");
> >>> +    vlog_set_levels(NULL, VLF_SYSLOG, VLL_OFF);
> >>> +
> >>> +    time_msec(); /* ensure timeval is initialized */
> >>
> >> Capital letter and a period at the end.
> >
> > Ack.
> >
> >>> +    timeval_timewarp_enable();
> >>> +
> >>> +    cooperative_multitasking_init(&cm_callbacks);
> >>> +
> >>> +    COOPERATIVE_MULTITASKING_REGISTER(&fixture_buggy_run, &arg1, 1000, 
> >>> NULL);
> >>> +    timeval_warp(0, 1000);
> >>> +    cooperative_multitasking_yield();
> >>> +    cooperative_multitasking_destroy();
> >>> +}
> >>> +
> >>> +static void
> >>> +test_cooperative_multitasking(int argc OVS_UNUSED, char *argv[] 
> >>> OVS_UNUSED)
> >>> +{
> >>> +    time_msec(); /* ensure timeval is initialized */
> >>
> >> Ditto.
> >
> > Ack.
> >
> >>> +    timeval_timewarp_enable();
> >>> +
> >>> +    cooperative_multitasking_init(&cm_callbacks);
> >>> +
> >>> +    test_cm_register();
> >>> +    test_cm_update();
> >>> +    test_cm_yield();
> >>> +}
> >>> +
> >>> +OVSTEST_REGISTER("test-cooperative-multitasking",
> >>> +                 test_cooperative_multitasking);
> >>> +OVSTEST_REGISTER("test-cooperative-multitasking-nested-yield",
> >>> +                 test_cooperative_multitasking_nested_yield);
> >>
> >
> >
>

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to