Expand description
Records that could not be delivered when they were raised: before any sink was installed, or while another thread was inside a sink (issue 2000).
§The problem this exists for
crate::init publishes the sink list. A record raised before that call
has nowhere to go, and for most of this crate’s life it was constructed,
dispatched and DROPPED — silently, and invisibly to its author, who cannot
know what the board did before reaching their code.
Issue 0708 answered by requiring every board boot funnel to call
init_default(), gated on funnels spelled pub fn run*. That is a SEARCH
for boot paths and it kept losing: NuttX’s funnel is pub extern "C" fn nsh_main, and three board crates did not link nros-log in the
configuration holding the funnel at all.
Issue 0710 answered by having dispatch install the platform sink itself.
That removed the search — but it put nros_platform_log_write on a path
EVERY binary executes, which turned a pluggable delivery into a LINK-TIME
requirement. Seven test targets in nros-rmw-cffi and most of nros-tests
stopped linking under check-workspace-features, and no Cargo feature can
fix that: nros-platform-cffi and nros-rmw-bridge enable
nros-node/rmw-cffi unconditionally, so feature unification turns any
forwarded gate back ON for every member of a workspace build. A feature is a
property of the BUILD; what the question needs is a property of the BINARY.
§A funnel is not always a board method — issues 1048 and 1123
The search above kept losing for one more reason, and it is worth naming
because it defeats the obvious grep: not every boot funnel lives in a
board crate. nros::main! emits a different entry shape per framework,
and two of them reach no BoardEntry method at all —
Framework::Zephyr(and its single-node siblingnros::zephyr_component_main!) emitsrust_maindirectly, because Zephyr owns the Cmainand a Rust staticlib cannot take it over. That is whynros-board-zephyrhas onlyrun_tiersto install from, and why a single-tier pure-Rust Zephyr image published no sink list for two phases (issue 1123) while itslog::info!s worked fine.- a
[[bridge]]system replaces the register/spin body withnros_bridge::run_from_config_str, so it reaches no board either.
Both now call nros_platform::log::init_default() — reached through
nros-platform (which re-exports nros_platform_cffi::log) because that is
the crate a generated Entry and the Zephyr example leaves already declare;
giving each leaf a nros-platform-cffi dep of its own is the link-time
requirement 0710 rejected.
So the enumeration is board crates PLUS the framework arms of
nros-macros’ main_macro.rs PLUS nros’ entry macros. Sweep it with:
grep -rn 'Framework::[A-Za-z]* => ' packages/core/nros-macros/src/main_macro.rs
grep -rn 'init_default()' packages/boards packages/core/nros-macros packages/api/nros§What this does instead
Hold the records. A record raised with no sinks installed is copied into a
bounded static ring here; crate::init drains it into whatever sinks the
board actually chose. Nothing is dropped, no board can forget, and this
crate touches no platform symbol to do it — the facade stays a facade.
It is also STRICTLY better than installing a default sink was: the early records land in the sink the board picked, rather than in whichever one dispatch guessed before the board had spoken.
§Cost, and how to decline it
EARLY_DEPTH * (format buffer + name + header) of static RAM, all of it in
.bss. The depth is the NROS_LOG_EARLY_RECORDS knob (issue 1037; builtin
4, resolved by build.rs on the RFC-0049 ladder — it replaced the
early-records-<N> features), because a 64 KB MCU and a Linux host do not
want the same number. 0 declines the buffer entirely and restores the
pre-0708 behaviour of dropping, with the count below still kept so the loss
is at least reportable.
Overflow is counted, never silently absorbed: overflowed returns how
many records did not fit, and the next delivery reports the count through
the sinks (nros_log logger, at warn).
§The same queue holds records that met a BUSY dispatcher — issue 2000
Dispatch runs one record at a time: a flag is held across every
sink.log(), so a sink that logs cannot recurse into itself, and two
threads never write to a sink at once. Until issue 2000 a record that found
the flag held was DROPPED, which was right for the recursion it was written
for and wrong for everything else: a record from ANOTHER thread vanished
whenever any thread was mid-delivery. nros-log cannot tell the two apart
without a thread identity, which no no_std target here gives it for free.
So such a record is held here instead, and the thread holding the flag delivers it before letting go. Nobody waits and nothing interleaves inside a sink. Two properties follow, both bounded on purpose:
- A sink that logs is not refused, it is postponed. The holder makes at
most
DRAIN_PASSESpasses, each over the records already held when the pass began, so a sink that logs on every write adds a bounded number of lines per dispatch and can never loop or grow the stack. What is left waits for the next dispatch orcrate::flush. - A record cannot be stranded by the hand-off. The raiser publishes the
record, then tries the flag once more; the holder releases the flag, then
looks for held records once more. Under
SeqCstone of the two always sees the other, so either the raiser delivers it or the holder does. The only record left behind is one raised during the holder’s LAST pass, which the next dispatch orflushdelivers.
The knob sizes both uses, because they are one question: how many records may be waiting for a sink at once.
Constants§
- DRAIN_
PASSES - Passes a dispatcher makes over the held records before it lets go. See the module docs: this is what bounds a sink that logs.
Functions§
- early_
depth - Records held at once. See the module docs for the trade.
- emit_
until_ delivered - Raise a record with
emituntildelivered()sees it, re-raising it wheneveroverflowedcounts a loss, for as long askeep_waiting()says to. Returns whetherdelivered()saw it. - overflowed
- How many records did not fit and were lost, since the program started.