From 1b284a519baab0ce995adcb47b6eb2e027ef2212 Mon Sep 17 00:00:00 2001
From: Andrea Gianarda <andrea.gianarda@belledonne-communications.com>
Date: Mon, 24 Mar 2025 11:43:39 +0100
Subject: [PATCH] Do not send further reINVITEs or UPDATEs until the retry
 function has been executed

---
 src/conference/client-conference.cpp     | 32 +++++++++++++-----------
 src/conference/session/call-session.cpp  | 28 ++++++++++++++++-----
 src/conference/session/media-session.cpp |  4 +++
 src/sal/call-op.cpp                      |  3 ++-
 4 files changed, 45 insertions(+), 22 deletions(-)

diff --git a/src/conference/client-conference.cpp b/src/conference/client-conference.cpp
index 0b1169d2fd..1ec72d9c17 100644
--- a/src/conference/client-conference.cpp
+++ b/src/conference/client-conference.cpp
@@ -896,13 +896,12 @@ void ClientConference::onFocusCallStateChanged(CallSession::State state, BCTBX_U
 					if (requestStreams() != 0) {
 						lInfo() << "Delaying re-INVITE in order to get streams after joining " << *this
 						        << " because the dialog is not available yet to accept this transaction";
-						mScheduleUpdate = true;
-					} else {
-						// An update has been successfully sent therefore clear the flag mScheduleUpdate to avoid
-						// sending it twice.
-						mScheduleUpdate = false;
-						mFullStateUpdate = false;
+						session->addPendingAction(requestStreams);
 					}
+					// An update has been successfully sent therefore clear the flag mScheduleUpdate to avoid sending it
+					// twice.
+					mScheduleUpdate = false;
+					mFullStateUpdate = false;
 				}
 			}
 				BCTBX_NO_BREAK; /* Intentional no break */
@@ -1796,13 +1795,6 @@ void ClientConference::onFullStateReceived() {
 		}
 #endif // HAVE_ADVANCED_IM
 
-		auto requestStreams = [this]() -> LinphoneStatus {
-			lInfo() << "Sending re-INVITE in order to get streams after receiving a NOTIFY full state for " << *this;
-			setState(ConferenceInterface::State::Created);
-			auto ret = updateMainSession(false);
-			return ret;
-		};
-
 		auto session = mFocus ? dynamic_pointer_cast<MediaSession>(mFocus->getSession()) : nullptr;
 		// Notify local participant that the microphone is muted when receiving the full state as participants are added
 		// as soon as possible
@@ -1812,12 +1804,22 @@ void ClientConference::onFullStateReceived() {
 
 		if (!getCore()->getCCore()->sal->mediaDisabled()) {
 			if (session && (!session->mediaInProgress() || !session->getPrivate()->isUpdateSentWhenIceCompleted())) {
+
+				auto requestStreams = [this]() -> LinphoneStatus {
+					lInfo() << "Sending re-INVITE in order to get streams after receiving a NOTIFY full state for "
+					        << *this;
+					setState(ConferenceInterface::State::Created);
+					auto ret = updateMainSession(false);
+					return ret;
+				};
+
 				if (requestStreams() != 0) {
 					lInfo() << "Delaying re-INVITE in order to get streams after receiving a NOTIFY full state for "
 					        << *this << " because it cannot be sent right now";
-					mScheduleUpdate = true;
-					mFullStateUpdate = true;
+					session->addPendingAction(requestStreams);
 				}
+				mScheduleUpdate = false;
+				mFullStateUpdate = false;
 			} else {
 				lInfo() << "Delaying re-INVITE in order to get streams after receiving a NOTIFY full state for "
 				        << *this << " because ICE negotiations didn't end yet";
diff --git a/src/conference/session/call-session.cpp b/src/conference/session/call-session.cpp
index d08b8cb6cf..adfb24324b 100644
--- a/src/conference/session/call-session.cpp
+++ b/src/conference/session/call-session.cpp
@@ -443,7 +443,6 @@ bool CallSessionPrivate::failure() {
 					        << Utils::toString(lastStableState);
 					setState(lastStableState, "Restore stable state because no retry function has been set");
 				}
-
 				return true;
 			}
 			if (ei->reason != SalReasonNoMatch) {
@@ -801,6 +800,15 @@ bool CallSessionPrivate::isReadyForInvite() const {
 }
 
 bool CallSessionPrivate::isUpdateAllowed(CallSession::State &nextState) const {
+	L_Q();
+	if (op->hasRetryFunction()) {
+		lWarning() << "Unable to send reINVITE or UPDATE request right now because " << *q << " (local address "
+		           << *q->getLocalAddress() << " remote address "
+		           << (q->getRemoteAddress() ? q->getRemoteAddress()->toString() : "Unknown")
+		           << ") needs to execute the request which was replied by a 491 Request Pending first.";
+		return false;
+	}
+
 	switch (state) {
 		case CallSession::State::IncomingReceived:
 		case CallSession::State::PushIncomingReceived:
@@ -826,7 +834,9 @@ bool CallSessionPrivate::isUpdateAllowed(CallSession::State &nextState) const {
 			nextState = state;
 			break;
 		default:
-			lError() << "Update is not allowed in [" << Utils::toString(state) << "] state";
+			lError() << *q << " (local address " << *q->getLocalAddress() << " remote address "
+			         << (q->getRemoteAddress() ? q->getRemoteAddress()->toString() : "Unknown")
+			         << "): Update is not allowed in [" << Utils::toString(state) << "] state";
 			return false;
 	}
 	return true;
@@ -1170,15 +1180,21 @@ std::shared_ptr<Address> CallSessionPrivate::getFixedContact() const {
 
 void CallSessionPrivate::reinviteToRecoverFromConnectionLoss() {
 	L_Q();
-	lInfo() << "CallSession [" << q << "] is going to be updated (reINVITE) in order to recover from lost connectivity";
+	lInfo() << *q << " is going to be updated (reINVITE) in order to recover from lost connectivity";
+	if (op) {
+		// Reset retry function as we need to recover from a network loss
+		op->resetRetryFunction();
+	}
 	q->update(params, CallSession::UpdateMethod::Invite);
 }
 
 void CallSessionPrivate::repairByNewInvite(bool withReplaces) {
 	L_Q();
-	lInfo() << "CallSession [" << q
-	        << "] is going to have a new INVITE one in order to recover from lost connectivity; with Replaces header:"
-	        << (withReplaces ? "yes" : "no");
+	lInfo() << *q << " is going to have a new INVITE one in order to recover from lost connectivity; "
+	        << (withReplaces ? "with" : "without") << " Replaces header";
+
+	// Reset retry function as we need to repair the INVITE session
+	op->resetRetryFunction();
 
 	// FIXME: startInvite shall() accept a list of bodies.
 	// Since it is not the case, we can only re-use the first one.
diff --git a/src/conference/session/media-session.cpp b/src/conference/session/media-session.cpp
index e0c7f8025c..e70087504a 100644
--- a/src/conference/session/media-session.cpp
+++ b/src/conference/session/media-session.cpp
@@ -4258,6 +4258,10 @@ void MediaSessionPrivate::reinviteToRecoverFromConnectionLoss() {
 	lInfo() << "MediaSession [" << q
 	        << "] is going to be updated (reINVITE) in order to recover from lost connectivity";
 	getStreamsGroup().getIceService().resetSession();
+	if (op) {
+		// Reset retry function as we need to recover from a network loss
+		op->resetRetryFunction();
+	}
 	MediaSessionParams newParams(*getParams());
 	q->update(&newParams, CallSession::UpdateMethod::Invite, q->isCapabilityNegotiationEnabled());
 }
diff --git a/src/sal/call-op.cpp b/src/sal/call-op.cpp
index 3f2aaadd67..9ab54ebadd 100644
--- a/src/sal/call-op.cpp
+++ b/src/sal/call-op.cpp
@@ -501,7 +501,8 @@ void SalCallOp::processResponseCb(void *userCtx, const belle_sip_response_event_
 	auto dialogState = dialog ? belle_sip_dialog_get_state(dialog) : BELLE_SIP_DIALOG_NULL;
 	lInfo() << "Op [" << op << "] receiving call response [" << code << "], dialog is [" << dialog << "] in state ["
 	        << belle_sip_dialog_state_to_string(dialogState) << "]";
-	op->ref(); // To make sure no cb will destroy op
+	op->ref();                // To make sure no cb will destroy op
+	op->resetRetryFunction(); // Retry function has been either executed or not needed anymore
 
 	auto request = belle_sip_transaction_get_request(BELLE_SIP_TRANSACTION(clientTransaction));
 	string method = belle_sip_request_get_method(request);
-- 
GitLab