Lifespan QoS Policy
The Lifespan policy specifies the maximum duration a data sample remains valid. Samples older than the lifespan are considered expired and are automatically discarded from the reader cache.
Purpose
Lifespan controls data freshness:
- Writers declare how long their samples remain meaningful
- Readers automatically discard expired samples
- Late joiners only receive samples that are still within their validity window
Deadline monitors the time between samples (publication rate). Lifespan controls the time for a sample (validity duration). They solve different problems and can be combined.
Configuration
use hdds::{Participant, QoS, TransportMode};
use std::time::Duration;
let participant = Participant::builder("sensor_app")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
// Writer: samples expire after 5 seconds
let writer = participant
.topic::<SensorData>("sensors/temperature")?
.writer()
.qos(QoS::reliable().transient_local().lifespan_secs(5))
.build()?;
// Reader: also configured with lifespan
let reader = participant
.topic::<SensorData>("sensors/temperature")?
.reader()
.qos(QoS::reliable().transient_local().lifespan_secs(5))
.build()?;
#include <hdds.h>
/* Create writer with 5-second lifespan */
struct HddsQoS* qos = hdds_qos_transient_local();
hdds_qos_set_lifespan_ns(qos, 5000000000ULL); /* 5s in nanoseconds */
struct HddsDataWriter* writer = hdds_writer_create_with_qos(
participant, "sensors/temperature", qos);
hdds_qos_destroy(qos);
Default Value
Default is infinite (samples never expire):
let qos = QoS::reliable();
// lifespan = Duration::MAX (infinite, no expiration)
Fluent Builder Methods
| Method | Description |
|---|---|
.lifespan_secs(n) | Set lifespan in seconds |
.lifespan_millis(n) | Set lifespan in milliseconds |
How Expiration Works
Lifespan: 2 seconds
Time: 0.0s 0.5s 1.0s 1.5s 2.0s 2.5s 3.0s
| | | | | | |
Write: [1] [2] [3] [4] [5] [6] [7]
| | | | | | |
| expires expires | | |
| at 2.5s at 3.0s | | |
DEAD DEAD DEAD DEAD alive alive alive
^
|
Late-joiner at t=3.0s only sees [5][6][7]
Expiration is enforced at multiple points:
- Writer cache: Expired samples are purged before sending to late joiners
- Reader cache: Expired samples are removed before delivery to the application
- On read/take: The
is_expired()check filters out stale data
Compatibility Rules
Writer lifespan must be greater than or equal to Reader lifespan:
| Writer | Reader | Match? |
|---|---|---|
| 10s | 5s | Yes |
| 5s | 5s | Yes |
| 5s | 10s | No |
| Infinite | 5s | Yes |
| 5s | Infinite | No |
| Infinite | Infinite | Yes |
Rule: Writer.lifespan >= Reader.lifespan
A writer with a shorter lifespan cannot satisfy a reader expecting longer validity. The reader would receive samples that expire before it can process them.
Use Cases
Sensor Data Aging
use hdds::{Participant, QoS, TransportMode};
let participant = Participant::builder("sensor_system")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
// IMU readings become stale after 100ms
let writer = participant
.topic::<ImuData>("sensors/imu")?
.writer()
.qos(QoS::reliable().transient_local().lifespan_millis(100))
.build()?;
Time-Sensitive Commands
use hdds::{Participant, QoS, TransportMode};
let participant = Participant::builder("command_system")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
// Commands expire after 1 second if not processed
let writer = participant
.topic::<Command>("robot/commands")?
.writer()
.qos(QoS::reliable().transient_local().lifespan_secs(1))
.build()?;
Cache Freshness for Late Joiners
use hdds::{Participant, QoS, TransportMode};
let participant = Participant::builder("cache_demo")
.domain_id(0)
.with_transport(TransportMode::UdpMulticast)
.build()?;
// Combined with TRANSIENT_LOCAL: late joiners get only fresh data
let writer = participant
.topic::<MarketData>("market/prices")?
.writer()
.qos(QoS::reliable().transient_local().lifespan_secs(10))
.build()?;
// Late-joining reader gets cached data that is still within 10s window
let reader = participant
.topic::<MarketData>("market/prices")?
.reader()
.qos(QoS::reliable().transient_local().lifespan_secs(10))
.build()?;
Interaction with Other Policies
Lifespan + Durability
| Lifespan | Durability | Behavior |
|---|---|---|
| Set | volatile() | Samples expire but no cache for late joiners |
| Set | transient_local() | Late joiners get only non-expired cached samples |
| Infinite | transient_local() | Late joiners get all cached samples |
Lifespan without transient_local() or persistent() has limited impact since samples are only delivered to currently matched readers anyway.
Lifespan + History
With keep_last(N), both history depth and lifespan work together. A sample can be evicted by either:
- Being pushed out by newer samples (history depth exceeded)
- Exceeding the lifespan duration
Lifespan + Deadline
| Policy | Purpose |
|---|---|
| Deadline | "I expect a new sample every N ms" |
| Lifespan | "Each sample is valid for N ms" |
These are complementary and often used together for periodic sensor data.
Common Pitfalls
-
Lifespan without durability: Setting lifespan on
volatile()writers has minimal effect since data is only sent to currently-matched readers. -
Too-short lifespan on slow networks: If the lifespan is shorter than the network latency, samples may expire before reaching the reader.
-
Clock synchronization: In distributed systems, ensure clocks are reasonably synchronized. Large clock skew can cause premature expiration or delayed expiration.
-
Forgetting reader-side lifespan: For late-joiner scenarios, set lifespan on both writer and reader QoS.
Performance Notes
- Lifespan checking adds minimal CPU overhead (timestamp comparison per sample)
- Expired samples are lazily purged, not on a timer
- Memory for expired samples is reclaimed when the cache is accessed
- With
transient_local(), the writer cache size is bounded by both history depth and lifespan
Next Steps
- Deadline - Periodic update requirements
- Durability - Data persistence for late joiners
- Overview - All QoS policies