Skip to content

Commit c0263f8

Browse files
AhmedVargosstanis-k
authored andcommitted
Fix race condition in MapboxTripSession (#11778) (#11850)
* Fix race condition in MapboxTripSession * Add test * Add changelog * Register NavigationRoutesFinishedObserver as well * Fix test * Fix test * Add java doc * Fix test * Fix typos. Add comment. Co-authored-by: Stanislav <[email protected]> GitOrigin-RevId: b5de4d13cb1dc493ff8b25d83ea9c1fa0859bdb9
1 parent d1d3a01 commit c0263f8

File tree

9 files changed

+232
-113
lines changed

9 files changed

+232
-113
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Fix the race condition when canceling Active Guidance from a background thread that does not immediately cancel Route Progress updates.

navigation/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import com.mapbox.navigation.core.arrival.ArrivalObserver
5050
import com.mapbox.navigation.core.arrival.ArrivalProgressObserver
5151
import com.mapbox.navigation.core.arrival.AutoArrivalController
5252
import com.mapbox.navigation.core.directions.session.DirectionsSession
53+
import com.mapbox.navigation.core.directions.session.DirectionsSessionRoutes
5354
import com.mapbox.navigation.core.directions.session.RoutesExtra
5455
import com.mapbox.navigation.core.directions.session.RoutesObserver
5556
import com.mapbox.navigation.core.directions.session.RoutesSetStartedParams
@@ -596,8 +597,13 @@ class MapboxNavigation @VisibleForTesting internal constructor(
596597
tripSessionLocationEngine = NavigationComponentProvider.createTripSessionLocationEngine(
597598
locationOptions = navigationOptions.locationOptions,
598599
)
600+
601+
directionsSession = NavigationComponentProvider.createDirectionsSession(routerWrapper)
602+
directionsSession.registerSetNavigationRoutesFinishedObserver(navigationSession)
603+
599604
tripSession = NavigationComponentProvider.createTripSession(
600605
tripService = tripService,
606+
directionsSession = directionsSession,
601607
tripSessionLocationEngine = tripSessionLocationEngine,
602608
navigator = navigator,
603609
threadController,
@@ -608,8 +614,6 @@ class MapboxNavigation @VisibleForTesting internal constructor(
608614
tripSession.registerStateObserver(navigationSession)
609615
tripSession.registerStateObserver(historyRecordingStateHandler)
610616

611-
directionsSession = NavigationComponentProvider.createDirectionsSession(routerWrapper)
612-
directionsSession.registerSetNavigationRoutesFinishedObserver(navigationSession)
613617
if (reachabilityObserverId == null) {
614618
reachabilityObserverId = ReachabilityService.addReachabilityObserver(
615619
connectivityHandler,
@@ -1340,6 +1344,16 @@ class MapboxNavigation @VisibleForTesting internal constructor(
13401344
"to ${directionsSession.routes.firstOrNull()?.id}",
13411345
),
13421346
)
1347+
1348+
// Even though we are not setting new routes here,
1349+
// we need to inform that the operation (setNavigationRoutesStarted) is finished
1350+
directionsSession.setNavigationRoutesFinished(
1351+
DirectionsSessionRoutes(
1352+
acceptedRoutes = directionsSession.routes,
1353+
ignoredRoutes = directionsSession.ignoredRoutes,
1354+
setRoutesInfo = setRoutesInfo,
1355+
),
1356+
)
13431357
} else {
13441358
historyRecordingStateHandler.setRoutes(routes)
13451359
when (val processedRoutes = setRoutesToTripSession(routes, setRoutesInfo)) {

navigation/src/main/java/com/mapbox/navigation/core/NavigationComponentProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,15 @@ internal object NavigationComponentProvider {
107107

108108
fun createTripSession(
109109
tripService: TripService,
110+
directionsSession: DirectionsSession,
110111
tripSessionLocationEngine: TripSessionLocationEngine,
111112
navigator: MapboxNativeNavigator,
112113
threadController: ThreadController,
113114
repeatRerouteAfterOffRouteDelaySeconds: Int,
114115
): TripSession = PerformanceTracker.trackPerformanceSync("createTripSession") {
115116
MapboxTripSession(
116117
tripService,
118+
directionsSession,
117119
tripSessionLocationEngine,
118120
navigator = navigator,
119121
threadController,

navigation/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,19 @@ internal interface DirectionsSession : RouteRefresh {
2525

2626
val initialLegIndex: Int
2727

28+
/**
29+
* Notify that route setting process has started.
30+
* During the process of routes update, NavSDK skips all the [NavigationStatus] updates.
31+
*
32+
* Make sure to call [setNavigationRoutesFinished] when done to release the updates.
33+
* */
2834
fun setNavigationRoutesStarted(params: RoutesSetStartedParams)
35+
36+
/**
37+
* Notify that route setting process has finished.
38+
*
39+
* @param routes DirectionsSessionRoutes
40+
*/
2941
fun setNavigationRoutesFinished(routes: DirectionsSessionRoutes)
3042

3143
/**

navigation/src/main/java/com/mapbox/navigation/core/trip/session/MapboxTripSession.kt

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.mapbox.navigation.base.trip.model.RouteLegProgress
1717
import com.mapbox.navigation.base.trip.model.RouteProgress
1818
import com.mapbox.navigation.base.trip.model.roadobject.UpcomingRoadObject
1919
import com.mapbox.navigation.core.SetRoutes
20+
import com.mapbox.navigation.core.directions.session.DirectionsSession
2021
import com.mapbox.navigation.core.internal.utils.initialLegIndex
2122
import com.mapbox.navigation.core.internal.utils.mapToReason
2223
import com.mapbox.navigation.core.navigator.getCurrentBannerInstructions
@@ -57,6 +58,7 @@ import kotlinx.coroutines.cancelChildren
5758
import kotlinx.coroutines.launch
5859
import java.util.concurrent.CopyOnWriteArraySet
5960
import java.util.concurrent.TimeUnit
61+
import java.util.concurrent.atomic.AtomicBoolean
6062
import kotlin.time.Duration.Companion.seconds
6163
import kotlin.time.ExperimentalTime
6264
import kotlin.time.TimeSource
@@ -66,6 +68,7 @@ import kotlin.time.minus
6668
* Default implementation of [TripSession]
6769
*
6870
* @param tripService TripService
71+
* @param directionsSession Directions session
6972
* @param tripSessionLocationEngine the location engine
7073
* @param navigator Native navigator
7174
* @param threadController controller for main/io jobs
@@ -75,6 +78,7 @@ import kotlin.time.minus
7578
@MainThread
7679
internal class MapboxTripSession(
7780
override val tripService: TripService,
81+
private val directionsSession: DirectionsSession,
7882
private val tripSessionLocationEngine: TripSessionLocationEngine,
7983
private val navigator: MapboxNativeNavigator,
8084
private val threadController: ThreadController,
@@ -86,12 +90,18 @@ internal class MapboxTripSession(
8690
private const val LOG_CATEGORY = "MapboxTripSession"
8791
}
8892

89-
private var isUpdatingRoute = false
9093
private var updateLegIndexJob: Job? = null
9194

95+
@VisibleForTesting
96+
internal val isUpdatingRoute = AtomicBoolean(false)
97+
9298
@VisibleForTesting
9399
internal var primaryRoute: NavigationRoute? = null
94100

101+
init {
102+
registerSetRoutesObservers()
103+
}
104+
95105
override suspend fun setRoutes(
96106
routes: List<NavigationRoute>,
97107
setRoutes: SetRoutes,
@@ -466,7 +476,7 @@ internal class MapboxTripSession(
466476
"Error processing native status update: origin=$origin, status=$status.\n" +
467477
"Error: $error\n" +
468478
"MapboxTripSession state: " +
469-
"isUpdatingRoute=$isUpdatingRoute, primaryRoute=${primaryRoute?.id}"
479+
"isUpdatingRoute=${isUpdatingRoute.get()}, primaryRoute=${primaryRoute?.id}"
470480
}
471481
throw NativeStatusProcessingError(error)
472482
}
@@ -791,7 +801,7 @@ internal class MapboxTripSession(
791801

792802
// we should skip RouteProgress, BannerInstructions, isOffRoute state updates while
793803
// setting a new route
794-
if (isUpdatingRoute) {
804+
if (isUpdatingRoute.get()) {
795805
logD("route progress update dropped - updating routes", LOG_CATEGORY)
796806
return
797807
}
@@ -934,10 +944,33 @@ internal class MapboxTripSession(
934944
}
935945
}
936946

947+
/**
948+
* Executes a route update transaction, setting the `isUpdatingRoute` flag to true
949+
* Works alongside with the observers registered in [registerSetRoutesObservers].
950+
* This ensures that during the execution of the provided function, any incoming
951+
* route progress updates are ignored, preventing potential race conditions
952+
*/
937953
private inline fun <T> updateRouteTransaction(func: () -> T): T {
938-
isUpdatingRoute = true
939-
return func().also {
940-
isUpdatingRoute = false
954+
isUpdatingRoute.set(true)
955+
return try {
956+
func()
957+
} finally {
958+
isUpdatingRoute.set(false)
959+
}
960+
}
961+
962+
/**
963+
* Registers an observer to detect an upcoming routes change even before `setRoutes` is called.
964+
* This helps handle race conditions by setting `isUpdatingRoute` early, allowing the session to
965+
* ignore route progress updates that may arrive from the navigator during the route update process.
966+
*/
967+
private fun registerSetRoutesObservers() {
968+
directionsSession.registerSetNavigationRoutesStartedObserver {
969+
isUpdatingRoute.set(true)
970+
}
971+
972+
directionsSession.registerSetNavigationRoutesFinishedObserver {
973+
isUpdatingRoute.set(false)
941974
}
942975
}
943976
}

navigation/src/test/java/com/mapbox/navigation/core/MapboxNavigationBaseTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ internal open class MapboxNavigationBaseTest {
384384
every {
385385
NavigationComponentProvider.createTripSession(
386386
tripService = tripService,
387+
directionsSession = directionsSession,
387388
tripSessionLocationEngine = tripSessionLocationEngine,
388389
navigator = navigator,
389390
threadController,

navigation/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1851,13 +1851,15 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() {
18511851
every { id } returns "id3"
18521852
}
18531853
every { directionsSession.routes } returnsMany listOf(listOf(route1), listOf(route2))
1854+
every { directionsSession.ignoredRoutes } returns emptyList()
18541855

18551856
mapboxNavigation.setNavigationRoutes(listOf(route1, route3))
18561857

18571858
coVerify(exactly = 0) {
18581859
tripSession.setRoutes(any(), any())
18591860
}
1860-
verify(exactly = 0) {
1861+
1862+
verify(exactly = 1) {
18611863
directionsSession.setNavigationRoutesFinished(any())
18621864
}
18631865
}
@@ -2742,13 +2744,15 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() {
27422744
every {
27432745
NavigationComponentProvider.createTripSession(
27442746
tripService = any(),
2747+
directionsSession = any(),
27452748
tripSessionLocationEngine = any(),
27462749
navigator = any(),
27472750
any(),
27482751
any(),
27492752
)
27502753
} returns MapboxTripSession(
27512754
tripService,
2755+
directionsSession,
27522756
tripSessionLocationEngine,
27532757
navigator,
27542758
threadController,

navigation/src/test/java/com/mapbox/navigation/core/trip/session/MapboxTripSessionNoSetupTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.mapbox.navigation.base.internal.factory.RoadObjectFactory.toUpcomingR
1515
import com.mapbox.navigation.base.options.LocationOptions
1616
import com.mapbox.navigation.base.trip.model.roadobject.UpcomingRoadObject
1717
import com.mapbox.navigation.core.SetRoutes
18+
import com.mapbox.navigation.core.directions.session.DirectionsSession
1819
import com.mapbox.navigation.core.infra.TestLocationProvider
1920
import com.mapbox.navigation.core.infra.recorders.BannerInstructionsObserverRecorder
2021
import com.mapbox.navigation.core.infra.recorders.OffRouteObserverRecorder
@@ -577,6 +578,8 @@ private fun buildTripSession(
577578
every { hasServiceStarted() } returns false
578579
}
579580

581+
val directionsSession: DirectionsSession = mockk(relaxUnitFun = true)
582+
580583
val parentJob = SupervisorJob()
581584
val testScope = CoroutineScope(parentJob + TestCoroutineDispatcher())
582585
val threadController = spyk<ThreadController>()
@@ -592,6 +595,7 @@ private fun buildTripSession(
592595
}
593596
return MapboxTripSession(
594597
tripService,
598+
directionsSession,
595599
TripSessionLocationEngine(LocationOptions.Builder().build()),
596600
nativeNavigator,
597601
threadController,

0 commit comments

Comments
 (0)