Commit 9623a69d49 for asterisk.org
commit 9623a69d499425c1c7752731b0247db10c1118ac
Author: rikrobson <richard@rikrobson.co.uk>
Date: Mon Sep 28 16:55:52 2026 +0000
res_pjsip_pubsub: Expire subscriptions recreated from persistence.
A subscription recreated from persistence after a restart is never
accepted by pjproject, so its server timeout never fires. The expiration
task scheduled in its place called pubsub_on_refresh_timeout(), which in
the normal state only sends another NOTIFY. If the subscriber had stopped
refreshing, the subscription therefore never ended: at expiry Asterisk
sent "Subscription-State: active;expires=0", then kept notifying with a
wrapped expires value on every state change until the next restart.
The expiration task now moves the subscription to the terminate pending
state first, as pubsub_on_server_timeout() does, so it ends with a final
"terminated" NOTIFY. A refresh still cancels the task, so subscriptions
that are refreshed are unaffected.
Assisted-by: Claude Opus 5.5
Resolves: #2187
diff --git a/res/res_pjsip_pubsub.c b/res/res_pjsip_pubsub.c
index 1852a5f877..9dd999284d 100644
--- a/res/res_pjsip_pubsub.c
+++ b/res/res_pjsip_pubsub.c
@@ -3204,7 +3204,7 @@ static int generate_initial_notify(struct ast_sip_subscription *sub)
return res;
}
-static int pubsub_on_refresh_timeout(void *userdata);
+static int pubsub_on_expiration_timeout(void *userdata);
static int initial_notify_task(void * obj)
{
@@ -3232,7 +3232,7 @@ static int initial_notify_task(void * obj)
ast_debug(3, "Scheduling timer: %s\n", name);
ind->sub_tree->expiration_task = ast_sip_schedule_task(ind->sub_tree->serializer,
- ind->expires * 1000, pubsub_on_refresh_timeout, name,
+ ind->expires * 1000, pubsub_on_expiration_timeout, name,
ind->sub_tree, AST_SIP_SCHED_TASK_FIXED | AST_SIP_SCHED_TASK_DATA_AO2);
if (!ind->sub_tree->expiration_task) {
ast_log(LOG_ERROR, "Unable to create expiration timer of %d seconds for %s\n",
@@ -4219,6 +4219,32 @@ static int pubsub_on_refresh_timeout(void *userdata)
return 0;
}
+/*!
+ * \brief Expire a subscription recreated from persistence
+ *
+ * A recreated subscription is never accepted by pjproject, so its server
+ * timeout never fires. Any refresh cancels this task, so if it runs the
+ * subscription has expired: terminate it as pubsub_on_server_timeout would.
+ */
+static int pubsub_on_expiration_timeout(void *userdata)
+{
+ struct sip_subscription_tree *sub_tree = userdata;
+ pjsip_dialog *dlg = sub_tree->dlg;
+
+ /* The dialog lock is held across the state change and the NOTIFY so they
+ * happen together. pubsub_on_refresh_timeout() takes the same lock again;
+ * that recursive lock is safe, as the dialog lock is a recursive mutex.
+ */
+ pjsip_dlg_inc_lock(dlg);
+ if (sub_tree->state == SIP_SUB_TREE_NORMAL) {
+ sub_tree->state = SIP_SUB_TREE_TERMINATE_PENDING;
+ }
+ pubsub_on_refresh_timeout(sub_tree);
+ pjsip_dlg_dec_lock(dlg);
+
+ return 0;
+}
+
static int serialized_pubsub_on_refresh_timeout(void *userdata)
{
struct sip_subscription_tree *sub_tree = userdata;