kuzaxak created an issue (kamailio/kamailio#4804)
### Description
With `db_mode=1` (`DB_MODE_REALTIME`), the dialog row INSERTed when a dialog
reaches state 3 (`DLG_STATE_CONFIRMED_NA`, 200 OK received, ACK pending)
always carries a `timeout` column value in the past.
In `dlg_onreply()` the DB write runs **before** the dialog timer is armed
(`src/modules/dialog/dlg_handlers.c:545-552` on branch 6.0, same on master).
The stored column is computed as
`(unsigned int)time(0) + cell->tl.timeout - get_ticks()`
(`dlg_db_handler.c:900-901`), and `tl.timeout` is only assigned inside
`insert_dlg_timer()` (`tl->timeout = get_ticks() + interval`, `dlg_timer.c`).
At the moment of the write `tl.timeout` is still `0`, so the stored value is
`time(0) - get_ticks()` — always `<= now` (observed from hours to years in the
past depending on the internal tick counter).
The row only becomes valid when the **same instance** later processes the ACK
(the 3→4 transition rewrites state and timeout with the timer armed). Until
then the row looks expired to everyone else. Two reproduced consequences:
1. **Multi-instance shared dialog DB** (active-active, in-dialog takeover via
`dlg_db_load_callid()`): if the ACK — or any in-dialog request — lands on
an instance that did not create the dialog, that instance loads the row;
`load_dialog_info_from_db()` sees `timeout <= time(0)`, sets
`lifetime = 0` (`dlg_db_handler.c:438-447`) and arms a 0-second timer, so
the dialog expires on the next `dlg_timer_routine` tick (1 Hz,
`dialog.c:715`): `dlg_ontimeout()` (`dlg_handlers.c:1679`) → state DELETED
→ `destroy_dlg()` → `remove_dialog_from_db()` (`dlg_hash.c:370-371`)
**deletes the shared row while the call is established**. Runtime-loaded
dialogs have `dflags = 0`, so nothing guards the DB delete. Every later
in-dialog request that needs DB recovery (re-INVITE, BYE) then fails on
every instance — the call can no longer be managed.
Additionally, if the ACK is processed by a non-creating instance, the
creating instance never runs the 3→4 repair, so the row stays expired for
the whole call even if it survives.
2. **Single instance**: a restart in the 200 OK → ACK window loads the dialog
at startup as already expired; it is destroyed and its row deleted instead
of recovered.
Expected behavior: the row written at `CONFIRMED_NA` carries
`timeout = now + dlg->lifetime`.
### Troubleshooting
#### Reproduction
Two kamailio instances **A** and **B** sharing one PostgreSQL dialog DB, both
with:
```
loadmodule "dialog.so"
modparam("dialog", "db_url", "postgres://kamailio:kamailiorw@pg:5432/kamailio")
modparam("dialog", "db_mode", 1)
```
A is a plain proxy doing `dlg_manage()` + `record_route()` on the initial
INVITE. B only handles in-dialog requests and recovers unknown dialogs:
```
if (!is_known_dlg()) {
dlg_db_load_callid("$ci");
if (!is_known_dlg()) {
sl_send_reply("500", "Dialog state lost");
exit;
}
}
```
Steps:
1. Send INVITE through A; UAS answers 200 OK. Do **not** ACK yet.
2. Probe the row:
`SELECT state, timeout, timeout - extract(epoch from now())::int AS delta
FROM dialog WHERE callid='...';`
→ `state=3`, `delta` **negative** (already expired).
3. Send the ACK (or any in-dialog request) to **B**. B logs that the dialog
was loaded, then within ≤1 s `dlg_ontimeout()` fires and the row is
**deleted** (the SELECT returns nothing, while the call is still up and
instance A still holds the dialog in memory).
4. A subsequent BYE to B cannot be recovered (`dlg_db_load_callid()` finds
nothing).
Control experiment: sending the ACK to **A** instead repairs the row
(`state=4`, `delta=+43199` = `default_timeout`), confirming the creating
instance's ACK is the only repair path.
I have a self-contained docker-compose reproduction (2× kamailio + postgres +
a python UAC/UAS driver with 7 assertions) and can attach it or push it to a
repo on request. With the one-line reorder below applied, all 7 assertions
pass; unpatched, 3 fail exactly as described.
#### Debugging Data
DB observations from the reproduction (fresh containers, kamailio built from
branch 6.0, git `5709418e6a`):
```
-- between 200 OK and ACK (state CONFIRMED_NA):
state=3 timeout=1728115743 now=1782980522 delta=-54864779s (in the past)
-- after the creating instance processes the ACK (3->4 repair):
state=4 timeout=1783023722 now=1782980523 delta=+43199s (=
default_timeout)
-- 0.5s after an in-dialog request was handled by the OTHER instance:
row deleted (call still established, creating instance still has the dialog in
memory)
```
#### Log Messages
Instance B (the non-creating instance), unpatched — dialog loaded, then
destroyed by the next timer tick, then recovery of the follow-up BYE fails:
```
NOTICE: <script>: B: ACK 599e748deee540e5b76baa41835bcf71@repro not in memory -
calling dlg_db_load_callid
NOTICE: <script>: B: dialog loaded from DB for ACK
599e748deee540e5b76baa41835bcf71@repro - is_known_dlg() now true
WARNING: dialog [dlg_handlers.c:1679]: dlg_ontimeout(): dlg timeout - callid:
'599e748deee540e5b76baa41835bcf71@repro' tags: 'uac-4ecf4ed5' 'uas-98b61cec'
ostate: 3
NOTICE: <script>: B: BYE 599e748deee540e5b76baa41835bcf71@repro not in memory -
calling dlg_db_load_callid
ERROR: <script>: B: RECOVERY FAILED - no dialog in DB for BYE
599e748deee540e5b76baa41835bcf71@repro
```
#### SIP Traffic
Attached `kamailio-dlg-db-mode1-pcaps.zip` with two captures of the identical
scenario (captured on the docker bridge, so all legs are visible):
- `sip-unpatched.pcap` — kamailio built from 6.0 `5709418e6a` unpatched
- `sip-patched.pcap` — same build plus the reorder from Possible Solutions
Address legend: `172.28.0.11` = instance A (creates the dialog),
`172.28.0.12` = instance B (loads it from DB), `151.101.2.132` = test UAC on
the docker host (NATed onto the bridge), `192.168.65.254` = test UAS on the
docker host.
`sip-unpatched.pcap` (tshark summary, annotated):
```
# call 1 (control) - single instance, everything through A:
1 0.000000 151.101.2.132 → 172.28.0.11 SIP/SDP Request: INVITE
sip:[email protected]
5 0.005517 172.28.0.11 → 151.101.2.132 SIP/SDP Status: 200 OK (INVITE)
6 0.438799 151.101.2.132 → 172.28.0.11 SIP Request: ACK <- A
repairs the row (3->4)
# call 2 (incident) - same INVITE path through A:
8 1.076994 151.101.2.132 → 172.28.0.11 SIP/SDP Request: INVITE
sip:[email protected]
12 1.078743 172.28.0.11 → 151.101.2.132 SIP/SDP Status: 200 OK (INVITE)
# DB row now: state=3, timeout in the past (see Debugging Data)
13 1.501259 151.101.2.132 → 172.28.0.12 SIP Request: ACK <- to B:
dlg_db_load_callid(),
0s
timer armed, row DELETED
within
1s (no SIP visible)
14 5.271345 151.101.2.132 → 172.28.0.12 SIP Request: BYE <- to B:
nothing left to load
15 5.272427 172.28.0.12 → 151.101.2.132 SIP Status: 500 Dialog state lost
# control: A still holds the dialog in memory, BYE through A works:
16 5.272759 151.101.2.132 → 172.28.0.11 SIP Request: BYE
19 5.273336 172.28.0.11 → 151.101.2.132 SIP Status: 200 OK (BYE)
```
`sip-patched.pcap` is identical up to frame 14; with the fix B keeps the
loaded dialog and frame 15 becomes `200 OK` instead of
`500 Dialog state lost`, and the shared DB row survives.
[kamailio-dlg-db-mode1-pcaps.zip](https://github.com/user-attachments/files/29589960/kamailio-dlg-db-mode1-pcaps.zip)
### Possible Solutions
Arm the dialog timer before the realtime DB write in the `CONFIRMED_NA` block
of `dlg_onreply()`, so the persisted timeout is `time(0) + lifetime`.
`dlg->lifetime` is already set on the INVITE path (`dlg_new_dialog()`), so the
reorder is safe. Verified against the reproduction above (stored timeout
becomes `now + default_timeout`; a second instance loading the dialog mid-call
keeps it; no row deletion; in-dialog requests recover fine).
```diff
--- a/src/modules/dialog/dlg_handlers.c
+++ b/src/modules/dialog/dlg_handlers.c
@@ -542,13 +542,6 @@ static void dlg_onreply(struct cell *t, int type, struct
tmcb_params *param)
/* set start time */
dlg->start_ts = (unsigned int)(time(0));
- /* save the settings to the database,
- * if realtime saving mode configured- save dialog now
- * else: the next time the timer will fire the update*/
- dlg->dflags |= DLG_FLAG_NEW;
- if(dlg_db_mode == DB_MODE_REALTIME)
- update_dialog_dbinfo(dlg);
-
if(0 != insert_dlg_timer(&dlg->tl, dlg->lifetime)) {
LM_CRIT("Unable to insert dlg %p [%u:%u] on event %d
[%d->%d] "
"with clid '%.*s' and tags '%.*s'
'%.*s'\n",
@@ -561,6 +554,18 @@ static void dlg_onreply(struct cell *t, int type, struct
tmcb_params *param)
dlg_ref(dlg, 1);
}
+ /* save the settings to the database,
+ * if realtime saving mode configured- save dialog now
+ * else: the next time the timer will fire the update.
+ * must run after insert_dlg_timer() - the stored timeout is
+ * computed from dlg->tl.timeout, which is 0 until the timer is
+ * armed, so writing earlier persists a timeout in the past and
+ * another instance loading the row (e.g. dlg_db_load_callid())
+ * expires the dialog immediately and deletes the shared row */
+ dlg->dflags |= DLG_FLAG_NEW;
+ if(dlg_db_mode == DB_MODE_REALTIME)
+ update_dialog_dbinfo(dlg);
+
/* dialog confirmed (ACK pending) */
run_dlg_callbacks(
DLGCB_CONFIRMED_NA, dlg, req, rpl,
DLG_DIR_UPSTREAM, 0);
```
I can open a pull request with this patch.
### Additional Information
- [x] LLM/AI Assistants were involved in discovery, analysis or submitting of
the issue
- [x] It happened in a production deployment
- [ ] It happened in a testing or development deployment
- [ ] Testing or fuzzing tools were used to generate SIP traffic when it
happened
* **Kamailio Version** - output of `kamailio -v`
```
version: kamailio 6.0.7 (aarch64/linux)
flags: USE_TCP, USE_TLS, USE_SCTP, TLS_HOOKS, USE_RAW_SOCKS, DISABLE_NAGLE,
USE_MCAST, DNS_IP_HACK, SHM_MMAP, PKG_MALLOC, MEM_JOIN_FREE, Q_MALLOC,
F_MALLOC, TLSF_MALLOC, DBG_SR_MEMORY, FAST_LOCK-ADAPTIVE_WAIT-NOSMP,
USE_DNS_CACHE, USE_DNS_FAILOVER, USE_NAPTR, USE_DST_BLOCKLIST, HAVE_RESOLV_RES,
TLS_PTHREAD_MUTEX_SHARED
ADAPTIVE_WAIT_LOOPS 1024, MAX_RECV_BUFFER_SIZE 262144, MAX_SEND_BUFFER_SIZE
262144, MAX_URI_SIZE 1024, BUF_SIZE 65535, DEFAULT PKG_SIZE 8MB
poll method support: poll, epoll_lt, epoll_et, sigio_rt, select.
compiled on 08:20:43 Jul 2 2026 with gcc 14.2.0
```
* **Operating System**:
<!--
Details about the operating system, the type: Linux (e.g.,: Debian 8.4, Ubuntu
16.04, CentOS 7.1, ...), MacOS, xBSD, Solaris, ...;
Kernel details (output of `lsb_release -a` and `uname -a`)
-->
```
Debian 13.5 (trixie) containers (docker compose), PostgreSQL 17
Linux 6.10.14-linuxkit aarch64 GNU/Linux
```
--
Reply to this email directly or view it on GitHub:
https://github.com/kamailio/kamailio/issues/4804
You are receiving this because you are subscribed to this thread.
Message ID: <kamailio/kamailio/issues/[email protected]>_______________________________________________
Kamailio - Development Mailing List -- [email protected]
To unsubscribe send an email to [email protected]
Important: keep the mailing list in the recipients, do not reply only to the
sender!