OpenCyphal-Garage / libcyphal

Portable reference implementation of the Cyphal protocol stack in C++ for embedded systems and Linux.
http://opencyphal.org
MIT License
282 stars 503 forks source link

Extend IRunnable::run with a deadline #364

Open pavel-kirienko opened 1 week ago

pavel-kirienko commented 1 week ago

See https://github.com/OpenCyphal-Garage/libcyphal/pull/363#discussion_r1651705834

This should be a deadline rather than a timeout. The clock used for the deadline tracking is described in https://github.com/OpenCyphal-Garage/libcyphal/blob/main/docs/design/readme.md#time-primitives

serges147 commented 1 week ago

What if introduce ICancellable (or IInterruptable TBD) with bool isCancelled() method, pass this interface (optionally?) to whatever run as MaybeError run(const TimePoint now, ICancellable* cancellation_token), and whatever loop we have in libcyphal currently is supposed to periodically check (on each iteration, or maybe even in between of some breakable sub-tasks of a single iteration, or even pass it to a nested internal runXxx methods in necessary) ? User implementation of such ICancellable might implement whatever logic she wants, f.e. keep track of currently allowed time slot, and when it's gone start returning true - and whole run should quickly unwind. With this approach we don't need to bring the clock entity into lybcyphal, and so don't need compare time by ourselves. Also, it allows user to interrupt a run safely and early by maybe some other than time condition...

pavel-kirienko commented 1 week ago

Makes sense. But are we sure we need an interface for this instead of a simple cetl::function<bool() noexcept, 4*sizeof(void)>?

serges147 commented 1 week ago

IMHO both will work, I just think that interface pointer might be more light way... In both cases we still need to first check for "emptiness" (either != nullptr or bool operator of function) before calling isCancelled, like:

while (/*some currently existing loop condition*/)
{
    if ((cancellation_token != nullptr) && cancellation_token->isCancelled())
    {
        return cetl::nullopt; // or `break;` if it suits better for some cases
    }
    ...
    runXxx(cancellation_token); // b/c might have nested "loop" inside
    ...
}

UNLESS!!! We will require non-empty token (either by passing ICancellable&, or require function to be pre-assigned with some functor like dummy []() { return false; }). Then the above code will be a bit simpler and with less branches to cover.

@pavel-kirienko What benefits do you think will be in using function here? Thnx

pavel-kirienko commented 1 week ago

What benefits do you think will be in using function here?

706ec3ca-8360-4354-8aa9-2a337006f98a_text

thirtytwobits commented 1 week ago

A cancellable assumes multiple threads of execution. We should avoid requiring this.

serges147 commented 1 week ago

A cancellable assumes multiple threads of execution.

This is not what I meant. Here is an example of possible user-provided implementation of ICancellable:

bool TimeslotCancellable::isCancelled()
{
    return ::user_system_clock() > deadline_;
}

And somewhere else user calls our run like this in her super-loop:

TimeslotCancellable timeslot;
while (true)
{
    const TimePoint now = ::user_system_clock();
    timeslot.setDeadline(now + 20ms);

    auto maybe_error = libcyphal.run(now, &timeslot);

    // do some other user-specific stuff
    ...
} 

OR like this if we go with function:

...
    const TimePoint deadline = user_system_clock() + 20ms;
    auto maybe_error = libcyphal.run(now, [deadline]() { return ::user_system_clock() > deadline; });
...
pavel-kirienko commented 1 week ago

@thirtytwobits Are you saying we need a different name?

thirtytwobits commented 1 week ago

@thirtytwobits Are you saying we need a different name?

nope. I like what Serge is proposing.