Skip to content

vehicle_time: Deliver VehicleClock callbacks from a worker thread - #215

Merged
umaucher merged 1 commit into
eclipse-score:mainfrom
florianfueller:feature/59-vehicle-clock-callback-delivery
Oct 13, 2026
Merged

umaucher merged 1 commit into
eclipse-score:mainfrom
florianfueller:feature/59-vehicle-clock-callback-delivery

Conversation

@florianfueller

@florianfueller florianfueller commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

The Set/Unset callback methods of VehicleClockBackendImpl were no-ops.
Callbacks are now delivered by a worker thread that polls the
TimeDaemon shared memory every 50 ms:

  • The thread starts with the first registered callback after Init()
    and runs until the backend is destroyed. While no callback is
    registered it sleeps.
  • A callback receives the first snapshot polled after its registration
    and afterwards only changes of its part of the snapshot:
    TimeSlaveSyncData and PDelayMeasurementData on any change,
    VehicleTimeStatus only when the flags change (rate deviation
    excluded).
  • Set/Unset are safe against in-flight invocations, also from inside a
    callback; Unset returns only after a running invocation finished.

Add unit tests, update the docs and remove the "not yet delivered"
warnings.

closes #59 (improvement ticket)

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

License Check Results

🚀 The license check job ran with the Bazel command:

bazel run //:license-check

Status: ⚠️ Needs Review

Click to expand output
[License Check Output]
Extracting Bazel installation...
Starting local Bazel server (8.6.0) and connecting to it...
INFO: Invocation ID: 1ad62f5b-ca6f-4164-b6bd-dda3ec582210
Computing main repo mapping: 
Computing main repo mapping: 
WARNING: For repository 'platforms', the root module requires module version platforms@1.0.0, but got platforms@1.1.0 in the resolved dependency graph. Please update the version in your MODULE.bazel or set --check_direct_dependencies=off
WARNING: For repository 'score_tooling', the root module requires module version score_tooling@2.2.1, but got score_tooling@2.2.2 in the resolved dependency graph. Please update the version in your MODULE.bazel or set --check_direct_dependencies=off
WARNING: For repository 'rules_oci', the root module requires module version rules_oci@2.2.7, but got rules_oci@2.3.0 in the resolved dependency graph. Please update the version in your MODULE.bazel or set --check_direct_dependencies=off
WARNING: For repository 'score_devcontainer', the root module requires module version score_devcontainer@1.8.0, but got score_devcontainer@1.11.1 in the resolved dependency graph. Please update the version in your MODULE.bazel or set --check_direct_dependencies=off
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Computing main repo mapping: 
Loading: 
Loading: 3 packages loaded
WARNING: Target pattern parsing failed.
ERROR: Skipping '//:license-check': no such target '//:license-check': target 'license-check' not declared in package '' defined by /home/runner/work/time/time/BUILD
ERROR: no such target '//:license-check': target 'license-check' not declared in package '' defined by /home/runner/work/time/time/BUILD
INFO: Elapsed time: 16.820s
INFO: 0 processes.
ERROR: Build did NOT complete successfully
ERROR: Build failed. Not running target

@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from d4f4e67 to 143d1fb Compare September 9, 2026 07:57
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from 143d1fb to d447876 Compare September 9, 2026 11:18
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from d447876 to a816147 Compare September 9, 2026 12:37
@florianfueller
florianfueller marked this pull request as ready for review September 9, 2026 12:38
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from c811e27 to 041c9b8 Compare September 23, 2026 06:56
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from 041c9b8 to 4448f49 Compare September 23, 2026 07:42
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from 4448f49 to 756eaad Compare September 23, 2026 08:06
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from 756eaad to 28b62ff Compare September 23, 2026 08:32
@florianfueller
florianfueller force-pushed the feature/59-vehicle-clock-callback-delivery branch from e5731ec to 150c89a Compare October 7, 2026 13:03
const auto status_flags = ConvertPtpStatus(snapshot.value().status);
score::cpp::ignore =
status_slot_.InvokeIfChanged(status_flags, VehicleTimeStatus{status_flags, snapshot.value().rate_deviation});
score::cpp::ignore = sync_data_slot_.TryDeliverChangedData(ConvertSyncData(snapshot.value().sync_fup_data));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need still to ignore the result?
regarding the returning result.
I think, the point, the callback was executed or nto is indeed not interesting.
but if the callback will return some result and we can react on it - that might be interesting.
but do we need this thing?
one of the use case - the callback could return if the registration shall still be hold.
so, if it will return true - continue calling it, false - drop the subscription.

if not, I would propose to drop the return at all.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return dropped.


void SvtCallbackDispatcher::PollAndDispatch() noexcept
{
const auto snapshot = svt_receiver_->Receive();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then we need to investigate before we will merge it

template <typename Timebase>
bool IsSameForDelivery(const TimeSlaveSyncData<Timebase>& first, const TimeSlaveSyncData<Timebase>& second) noexcept
{
const bool same_precise_origin_timestamp = (first.precise_origin_timestamp == second.precise_origin_timestamp);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the default instantiation doesn't fit here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented operator==

bool IsSameForDelivery(const PDelayMeasurementData<Timebase>& first,
const PDelayMeasurementData<Timebase>& second) noexcept
{
const bool same_request_origin_timestamp = (first.request_origin_timestamp == second.request_origin_timestamp);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the default instantiation doesn't fit here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented operator==

/// @brief Thread-safe holder for a single move-only callback that is invoked from a dedicated worker thread
/// @brief Delivery comparison for types that already provide @c operator==.
template <typename Value>
bool IsSameForDelivery(const Value& first, const Value& second) noexcept

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, I can understand the function name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to IsSame

/// @return @c true if the callback was invoked, @c false if none is installed or @p data is unchanged.
template <typename Argument>
bool InvokeIfChanged(const Data& data, const Argument& argument) noexcept
bool TryDeliverChangedData(const Data& data) noexcept

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it a delivery?
Maybe TryToEnvoke() or somehting?
also the user of this function doesn't know, what the "new" data is.
he jsut has the "data"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to TryToEnvoke

std::recursive_mutex mutex_;
std::shared_ptr<Callback> callback_{};
std::optional<Data> last_data_{};
// Mirrors callback_ != nullptr. Written only under mutex_, read lock-free by IsSet().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think, we need this comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Documentation preview for this pull request is available at:
pr-215: https://eclipse-score.github.io/time/pr-215/

// No write was in progress when the copy started and none started since
if (exit_cnt_before_read == entry_cnt_after_read)
// Even: no write in progress
if ((seq_before_read & 1U) == 0U)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a very small, tiny but self-explaining function.
you will not need the comment then.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

{
return read_data;
// Copy the payload
read_data = shared_memory_data_->data_;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we set something another atomic variable to block the writer for moment here?
or it wil l be anyway compensated by +1 and +2 done by writer?

@florianfueller florianfueller Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The counter does the job: An in-progress write makes it odd and a complete write makes it even again. So the reader's final check fails and it retries. A lock would make the writer wait on slow readers.

What still remeins is a data race. The reader and the writer could still access the same memory location at the same time. This is undefined behavior. But we should solve this issue in a dedicated ticket.

shared_memory_data_->seq_.store(seq + 1U, std::memory_order_relaxed);

// Copy the non-atomic payload
// Copy the payload

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove it

wakeup_requested_ = true;
StartWorkerIfReadyLocked();
// Also delivers the first snapshot promptly to a fresh registration instead of after a full poll interval.
worker_wakeup_.notify_all();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the thread doesn't wait for itright now, the even will be missed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes notify_all() is lost when nothing is waiting, but the callback registration already happened in SetCallback and with the next poll the value will be delivered. The notify_all() here is for waking up the thread if it is currently waiting.

The Set/Unset callback methods of VehicleClockBackendImpl were no-ops.
Callbacks are now delivered by a worker thread that polls the
TimeDaemon shared memory every 50 ms:

- The thread starts with the first registered callback after Init()
  and runs until the backend is destroyed. While no callback is
  registered it sleeps.
- A callback receives the first snapshot polled after its registration
  and afterwards only changes of its part of the snapshot:
  TimeSlaveSyncData and PDelayMeasurementData on any change,
  VehicleTimeStatus only when the flags change (rate deviation
  excluded).
- Set/Unset are safe against in-flight invocations, also from inside a
  callback; Unset returns only after a running invocation finished.

Add unit tests, update the docs and remove the "not yet delivered"
warnings.

Refs eclipse-score#59

Signed-off-by: Florian Füller <5454068+florianfueller@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
workflow-approval — 41e5868f Deployed Oct 13, 2026 by umaucher via qnx-build (arm64-qnx) / approval #975
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Improvement: Implement the callbacks envoking

3 participants