Skip to content

Feature: discard undelivered (unselected) events on demand via Database #427

Description

@jadamcrain

Motivation

By default — and especially after #423 — an outstation retains unconfirmed events and re-reports them after a reconnect. That is the correct DNP3 behavior and what almost every integration wants.

A small number of bespoke, non-standard integrations want the opposite for a specific class of data: when a session ends, they want to drop their undelivered events of a given class rather than have them re-reported on the next connection. This comes up when the master treats values timestamped between connection windows as stale and rejects them, so re-delivering buffered events on reconnect is actively harmful in that deployment.

This is deliberately outside normal sequence-of-events semantics. It is lossy on purpose, it is not something a conformant outstation should do by default, and it should never be on automatically. The request is for an explicit, opt-in primitive that an application can call when it knows it wants to discard.

Proposed API

A single additive method on the public Database, reached through DatabaseHandle::transaction like every other database mutation:

impl Database {
    /// Discard undelivered events of the given classes from the event buffer.
    ///
    /// Only events that are NOT part of an in-flight response/confirm exchange are
    /// removed. Returns the number of events discarded.
    ///
    /// This is a lossy operation outside normal event-delivery semantics. Discarded
    /// events are gone; they are not re-reported and no confirmation callback fires
    /// for them. Intended only for specialized integrations that must drop undelivered
    /// data on disconnect. Standard outstations should not use it.
    pub fn discard_unselected_events(&mut self, classes: EventClasses) -> u32;
}

Generated into the C/C++/.NET/Java bindings as a normal additive method on the Database class (it does not change any existing interface).

1. Why it works, and why only unselected events

The event buffer tracks each event's delivery state. Some events are sitting idle ("unselected"); others have been written into a response that is on the wire and awaiting the master's confirmation ("in-flight"). The generic primitive only ever removes the idle ones, for two reasons:

  • Yanking in-flight events out from under a live exchange is unsafe. It would break delivery accounting — the confirmation callback would never fire for events the master actually received, so the application would re-send them and the master would see duplicates — and it can truncate a multi-fragment response mid-series. Restricting to unselected events makes the call safe to invoke at any time: if it's ever called while a response is in flight, it simply leaves those events alone. Worst case it is a no-op. There is no way to corrupt an active exchange with it, which is what lets us expose it generically rather than as a fragile, state-sensitive call.

  • "Unselected only" is not a practical limitation for the intended use case, because of how teardown now works (Outstation event buffer not reset on session teardown: unconfirmed events stranded (not re-reported on reconnect) + stale Selected events can leak into later responses #423). On session teardown the buffer is re-armed: every event — including ones that were in-flight when the link dropped — returns to the unselected state, and this happens before the application is notified of the disconnect. So at the moment you'd actually call this (your disconnect handler), all residual events are unselected and therefore eligible. "Unselected only" + "called on disconnect" discards everything stale, with zero risk to any live exchange.

It deliberately does not fire the event-cleared confirmation callback (that signal means "the master confirmed receipt"; these events are being dropped by the application, not confirmed), and it does not affect static data — only buffered events.

2. Where the application calls it

Through a transaction, from the application's disconnect notification:

handle.transaction(|db| {
    db.discard_unselected_events(EventClasses::class2());
});
  • Outstation-as-TCP-client: from the disconnect handler (the connection state listener's disconnected transition).
  • Outstation-as-TCP-server: from the ConnectionState::Disconnected listener.

In both modes the #423 teardown reset has already run by the time these fire, so the call sees a fully re-armed buffer and drops the complete residual backlog for the given classes.

Notes / scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions