VLink  2.1.0
A high-performance communication middleware
trigger_recorder.h File Reference

Event-data-recorder engine: a rolling in-memory ring of every live topic, dumped to a bag on demand. More...

#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "../base/message_loop.h"
#include "../subscriber.h"
#include "./discovery_viewer.h"
Include dependency graph for trigger_recorder.h:

Go to the source code of this file.

Classes

class  vlink::TriggerRecorder
 MessageLoop-based rolling in-memory recorder that dumps a pre/post window to a bag on trigger. More...
 
struct  vlink::TriggerRecorder::UrlConfig
 Per-URL overrides; any field left negative falls back to the matching Config default. More...
 
struct  vlink::TriggerRecorder::Config
 Recorder-wide configuration; passed once to the constructor and read-only afterwards. More...
 
struct  vlink::TriggerRecorder::TriggerParams
 Per-trigger parameters; a pure data struct with no RPC or protobuf dependency. More...
 

Namespaces

 

Detailed Description

Event-data-recorder engine: a rolling in-memory ring of every live topic, dumped to a bag on demand.

TriggerRecorder is a self-contained data-plane engine (no RPC, no config parsing – those belong to the caller). It discovers every topic on the bus, subscribes to the raw Bytes of each through a caller-supplied RawSubFactory, and keeps a rolling per-URL ring holding the most recent pre-trigger window. dump() persists the pre + post window around the trigger instant to a bag file, rotating old files. This is the dashcam / EDR pattern: the ring is always recording, the trigger decides what to persist.

*   discovery --new URL--> RawSubFactory (caller TU) --> RawSub --Bytes--> [per-URL rolling rings]
*                                                                                   |
*                                     dump(): serialized, runs on the recorder loop |
*                                                                                   v
*   trigger plugin on_dump_finished() <-- dump_dir bag (.vdb|.vcap) <-- BagWriter <-- [bag plugin? reorder]
* 
Life cycle
The recorder is a MessageLoop: the constructor validates the Config and acquires every fallible resource (creates Config::dump_dir and constructs the discovery viewer), throwing on failure. async_run() starts discovery + buffering; wait for on_begin() to complete (e.g. invoke_task([](){}).wait()) before calling dump(). quit() stops the loop and abandons a dump still waiting for its post window; wait for is_dumping() to become false first when a dump must be preserved.
Two distinct plugin roles (never conflate them)
  • A bag plugin (BagPluginInterface, supplied via bind_bag_interface()) sits inside the write path: its on_write() re-emits frames reordered by the true data-plane time parsed from each payload. Without it frames are written in capture-time (arrival) order.
  • A trigger plugin (TriggerPluginInterface, via bind_trigger_interface()) observes the recorder life cycle – on_dump_finished() is the upload / archive hook. It never rewrites frames.
Per-URL windows
Each URL may override the global default pre / post window (milliseconds before / after the trigger), plus a max packet size, a per-URL byte cap and only_front / only_back restrictions. A global URL whitelist / blacklist selects which topics participate.
Retention model (constant retention)
Every enabled URL retains pre_u + max_post_all + 2*retention_guard of history (max_post_all = the largest post across all enabled URLs), so the ingest hot path needs no "is a trigger active" branch:
*                     pre_u              post_u
*             |<---------------->|<---------------->|         dump(T) accepted at T, written after the
*   ring   [==:==================T==================:==]      largest effective post of the selected URLs
*          T-pre_u-guard                     T+post_u+guard    plus retention_guard (immediately when that
*             selected at acceptance; frames sliced at write    post is zero); the guard cushions both
*             time from the guard-padded ring coverage          window boundaries
* 
Note
The subscriber callback (data into the ring) is the hot path: it runs on the transport dispatch thread(s), takes only a per-URL lock, copies the payload once with Bytes::deep_copy and is amortized O(1); one callback may evict multiple expired or over-limit entries.
Warning
A single URL with a large post raises the retention – and memory – of every URL.
A reordering bag plugin still copies and buffers part of the window until flush(); frames emitted downstream are synchronous and do not accumulate in an additional bag-writer queue.
busy_skip_data drops data while the bag writer is active, leaving time holes for later triggers.
With destroy_on_offline, offline buffers kept for an in-flight dump can push peak memory above max_cache_size.
Usage
config.dump_dir = "/data/edr";
config.default_pre_ms = 15'000;
config.default_post_ms = 0;
camera.pre_ms = 15'000; // pre=15s
camera.post_ms = 0; // post=0
config.url_overrides["dds://camera/front"] = camera;
vlink::TriggerRecorder recorder(config, [](const std::string& url, vlink::InitType type) {
});
recorder.async_run();
recorder.invoke_task([]() {}).wait(); // wait for on_begin() so dump() is accepted
// ... later, on an external event ...
params.reason = "hard-brake";
recorder.dump(params);