Skip to content

Commit 2f620cc

Browse files
committed
use a reentrant lock
1 parent f6457a8 commit 2f620cc

File tree

2 files changed

+58
-11
lines changed

2 files changed

+58
-11
lines changed

features/dd-sdk-android-flags/src/main/kotlin/com/datadog/android/flags/internal/FlagsStateManager.kt

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import com.datadog.android.flags.StateObservable
1313
import com.datadog.android.flags.model.FlagsClientState
1414
import com.datadog.android.internal.utils.DDCoreSubscription
1515
import java.util.concurrent.ExecutorService
16+
import java.util.concurrent.locks.ReentrantLock
17+
import kotlin.concurrent.withLock
1618

1719
/**
1820
* Manages state transitions and notifications for a [com.datadog.android.flags.FlagsClient].
1921
*
2022
* This class handles state change notifications to registered listeners. All notification
2123
* methods are thread-safe and guarantee ordered delivery to listeners by using a
22-
* single-threaded executor service.
24+
* single-threaded executor service and a fair ReentrantLock.
2325
*
2426
* The current state is stored and emitted to new listeners immediately upon registration,
2527
* ensuring every listener receives the current state.
@@ -33,19 +35,26 @@ internal class FlagsStateManager(
3335
private val executorService: ExecutorService,
3436
private val internalLogger: InternalLogger
3537
) : StateObservable {
38+
/**
39+
* Fair lock to ensure FIFO ordering of state mutations and listener operations.
40+
* The fair parameter ensures that threads acquire the lock in the order they requested it.
41+
*/
42+
private val stateLock = ReentrantLock(true)
43+
3644
/**
3745
* The current state of the client.
38-
* Thread-safe: uses volatile for visibility across threads.
46+
* Thread-safe: access is protected by stateLock.
3947
*/
40-
@Volatile
4148
private var currentState: FlagsClientState = FlagsClientState.NotReady
4249

4350
/**
4451
* Returns the current state synchronously.
4552
*
4653
* @return The current [FlagsClientState].
4754
*/
48-
override fun getCurrentState(): FlagsClientState = currentState
55+
override fun getCurrentState(): FlagsClientState = stateLock.withLock {
56+
currentState
57+
}
4958

5059
/**
5160
* Updates the state and notifies all listeners.
@@ -56,8 +65,8 @@ internal class FlagsStateManager(
5665
* @param newState The new state to transition to.
5766
*/
5867
internal fun updateState(newState: FlagsClientState) {
59-
// lock the currentState until listener notifications are queued to prevent new listeners during state change.
60-
synchronized(currentState) {
68+
// Lock to ensure atomic state update and listener notification queueing
69+
stateLock.withLock {
6170
currentState = newState
6271
subscription.notifyListeners {
6372
executorService.executeSafe(
@@ -71,7 +80,7 @@ internal class FlagsStateManager(
7180
}
7281

7382
override fun addListener(listener: FlagsStateListener) {
74-
synchronized(currentState) {
83+
stateLock.withLock {
7584
subscription.addListener(listener)
7685

7786
val stateToEmit = currentState
@@ -85,7 +94,7 @@ internal class FlagsStateManager(
8594
}
8695

8796
override fun removeListener(listener: FlagsStateListener) {
88-
synchronized(currentState) {
97+
stateLock.withLock {
8998
subscription.removeListener(listener)
9099
}
91100
}

features/dd-sdk-android-flags/src/test/kotlin/com/datadog/android/flags/internal/FlagsStateManagerTest.kt

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ internal class FlagsStateManagerTest {
169169
)
170170

171171
val executionOrder = mutableListOf<String>()
172+
val executionOrderLock = Any()
172173

173174
// Creates a listener that adds start/end markers to the execution order and calls the additional block.
174175
fun createListener(
@@ -178,7 +179,7 @@ internal class FlagsStateManagerTest {
178179
): FlagsStateListener = object : FlagsStateListener {
179180
override fun onStateChanged(newState: FlagsClientState) {
180181
if (newState is FlagsClientState.Ready) {
181-
synchronized(executionOrder) {
182+
synchronized(executionOrderLock) {
182183
executionOrder.add(name)
183184
additionalBlock()
184185
executionOrder.add("$name ended")
@@ -202,7 +203,7 @@ internal class FlagsStateManagerTest {
202203
managerWithRealExecutor.addListener(listener4)
203204

204205
// When
205-
synchronized(executionOrder) {
206+
synchronized(executionOrderLock) {
206207
managerWithRealExecutor.updateState(FlagsClientState.Ready)
207208
executionOrder.add("updateState")
208209
}
@@ -215,7 +216,7 @@ internal class FlagsStateManagerTest {
215216
realExecutorService.awaitTermination(2, TimeUnit.SECONDS)
216217

217218
// Then - all listeners should have been called in order, despite listener3 throwing
218-
synchronized(executionOrder) {
219+
synchronized(executionOrderLock) {
219220
assertThat(executionOrder).containsExactly(
220221
"updateState",
221222
"listener1",
@@ -229,6 +230,43 @@ internal class FlagsStateManagerTest {
229230
}
230231
}
231232

233+
@Test
234+
fun `M maintain FIFO ordering W concurrent operations on fair lock`() {
235+
// Given
236+
realExecutorService = Executors.newSingleThreadExecutor()
237+
val managerWithRealExecutor = FlagsStateManager(
238+
DDCoreSubscription.create(),
239+
realExecutorService,
240+
mockInternalLogger
241+
)
242+
243+
val notificationCount = java.util.concurrent.atomic.AtomicInteger(0)
244+
val listeners = mutableListOf<FlagsStateListener>()
245+
246+
// When
247+
repeat(10) {
248+
val listener = object : FlagsStateListener {
249+
override fun onStateChanged(newState: FlagsClientState) {
250+
if (newState is FlagsClientState.Ready) {
251+
notificationCount.incrementAndGet()
252+
}
253+
}
254+
}
255+
listeners.add(listener)
256+
managerWithRealExecutor.addListener(listener)
257+
}
258+
259+
// Trigger state change
260+
managerWithRealExecutor.updateState(FlagsClientState.Ready)
261+
262+
realExecutorService.shutdown()
263+
realExecutorService.awaitTermination(2, TimeUnit.SECONDS)
264+
265+
// Then
266+
assertThat(notificationCount.get()).isEqualTo(10)
267+
assertThat(managerWithRealExecutor.getCurrentState()).isEqualTo(FlagsClientState.Ready)
268+
}
269+
232270
// endregion
233271

234272
companion object {

0 commit comments

Comments
 (0)