Skip to content

Question / Proposal: Confirmed dialog snapshot & restore support #65

@FedorKiselev76

Description

@FedorKiselev76

DESCRIPTION
This is a proposal and a request for feedback on whether such a change would be acceptable as a PR.

The idea is to add support for persisting and restoring confirmed SIP dialogs, allowing dialogs to survive process restarts.

MOTIVATION
In real-world deployments such as supervisors, rolling restarts, or crashes, confirmed SIP dialogs may outlive the process handling them.
Without persistence, all in-memory dialogs are lost on restart, even though the remote peer still considers them active.

This makes it impossible to correctly send BYE or other in-dialog requests after restart.

CURRENT USAGE (CLIENT DIALOGS)
At the moment, this approach is used for client-side dialogs (ClientInviteDialog) only.

The reason is architectural.
The system currently acts as a gRPC to SIP gateway.
SIP dialogs are initiated and controlled from the client side.
Dialog persistence is required to correctly terminate or continue calls after restart.

PLANNED USAGE (SERVER DIALOGS)
In the near future, the system is planned to migrate from OpenSIPS to rsipstack for server-side call handling.

After this migration, the same snapshot and restore mechanism would be reused for server-side dialogs (ServerInviteDialog), allowing confirmed incoming calls to survive restarts in the same way.

For this reason, the proposed implementation is role-agnostic and designed to support both client and server dialogs at the core level.

WHAT IS PROPOSED
DialogSnapshot would provide a minimal representation of a confirmed dialog state.
snapshot_confirmed() would capture dialog state before shutdown.
DialogInner::from_confirmed_snapshot(...) would restore dialog state after restart.
Restored dialogs would be inserted directly into DialogLayer without INVITE replay.
A synthetic initial request would be generated internally to preserve in-dialog request behavior.

DESIGN CONSIDERATIONS
Only confirmed dialogs are considered.
No automatic re-INVITE or media renegotiation is attempted.
Transport and connection recovery are intentionally out of scope.
Dialog removal would continue to use the existing remove_dialog(&DialogId) API, without parallel lifecycle paths.

STORAGE
The snapshot format is intended to be storage-agnostic and suitable for Redis or any other key-value store.

FEEDBACK REQUEST
Before preparing a full PR, I would like to ask whether this approach and scope would be acceptable for inclusion in rsipstack, or if there are any architectural concerns or preferred alternatives.

EXAMPLE: PERSISTING A CONFIRMED DIALOG

This example shows how a confirmed dialog is snapshotted and stored when the dialog reaches the Confirmed state.

use rsipstack::dialog::dialog::{Dialog, DialogState};
use crate::dialog_snapshot_convert::to_wire_snapshot;
use crate::dialog_store::{DialogStore, now_ms};
 
 async fn on_dialog_state_confirmed(
 
   dialog_layer: std::sync::Arc<rsipstack::dialog::dialog_layer::DialogLayer>,
    store: std::sync::Arc<dyn DialogStore>,
    state: DialogState,
  ) {
 
   if let DialogState::Confirmed(id, resp) = state {
        let Some(dialog) = dialog_layer.get_dialog(&id) else {
            return;
        };
 
       let snapshot = match dialog {
            Dialog::ClientInvite(d) => d.snapshot_confirmed(),
            Dialog::ServerInvite(d) => d.snapshot_confirmed(),
        };
        let wire = to_wire_snapshot(&snapshot);
        let snapshot_json = serde_json::to_string(&wire).unwrap();

        let _ = store
            .persist_confirmed(&id, &resp, &snapshot_json, now_ms())
            .await;
    }
}

EXAMPLE: RESTORING CONFIRMED DIALOGS ON STARTUP

This example shows how confirmed dialogs are restored into DialogLayer during application startup.

use rsipstack::dialog::dialog::{Dialog, DialogInner, DialogSnapshot};
use rsipstack::dialog::dialog_layer::DialogLayer;
use rsipstack::transaction::transaction::transaction_event_sender_noop;
use crate::dialog_snapshot_convert::from_wire_snapshot;
use crate::dialog_store::DialogStore;

async fn restore_confirmed_dialogs_on_startup(
    dialog_layer: &std::sync::Arc<DialogLayer>,
    endpoint_inner: rsipstack::transaction::endpoint::EndpointInnerRef,
    state_tx: rsipstack::dialog::dialog::DialogStateSender,
    store: std::sync::Arc<dyn DialogStore>,
) {
    let ids = store.list_confirmed_ids(10_000).await.unwrap();

    for dialog_id_str in ids {
        let Some(json) = store.load_snapshot_json(&dialog_id_str).await.unwrap() else {
            continue;
        };

        let wire = serde_json::from_str(&json).unwrap();
        let snapshot: DialogSnapshot = from_wire_snapshot(wire).unwrap();

        let inner = DialogInner::from_confirmed_snapshot(
            snapshot.clone(),
            endpoint_inner.clone(),
            state_tx.clone(),
            transaction_event_sender_noop(),
        );

        let dialog = Dialog::from_inner(
            snapshot.role.clone(),
            std::sync::Arc::new(inner),
        );

        dialog_layer.insert_dialog_restored(dialog_id_str, dialog);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions