fix(scene): preserve fixed gateway device identifiers, fix edit loading race condition and parallel abort strategy
This commit is contained in:
+42
-8
@@ -22,6 +22,7 @@ import com.flagship.abox.manager.domain.SocketGateway
|
||||
import com.flagship.abox.manager.domain.SocketPowerState
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@@ -227,15 +228,48 @@ class FakeSceneExecutor(
|
||||
|
||||
return try {
|
||||
val results: List<SceneActionExecutionResult> = if (scene.concurrencyMode == ConcurrencyMode.PARALLEL) {
|
||||
coroutineScope {
|
||||
scene.actions.map { action ->
|
||||
async {
|
||||
val actionStart = System.currentTimeMillis()
|
||||
val execResult = executeAction(scene.profileId, action)
|
||||
val duration = System.currentTimeMillis() - actionStart
|
||||
execResult.copy(executionDurationMillis = duration)
|
||||
if (scene.strategy == ExecutionStrategy.ABORT_ON_ERROR) {
|
||||
coroutineScope {
|
||||
lateinit var deferreds: List<Deferred<SceneActionExecutionResult>>
|
||||
deferreds = scene.actions.mapIndexed { index, action ->
|
||||
async {
|
||||
val actionStart = System.currentTimeMillis()
|
||||
val execResult = executeAction(scene.profileId, action)
|
||||
val duration = System.currentTimeMillis() - actionStart
|
||||
val enriched = execResult.copy(executionDurationMillis = duration)
|
||||
if (enriched.outcome == SceneActionOutcome.FAILURE) {
|
||||
deferreds.forEachIndexed { otherIndex, otherDeferred ->
|
||||
if (otherIndex != index) {
|
||||
otherDeferred.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
enriched
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
deferreds.mapIndexed { index, deferred ->
|
||||
try {
|
||||
deferred.await()
|
||||
} catch (e: CancellationException) {
|
||||
SceneActionExecutionResult(
|
||||
action = scene.actions[index],
|
||||
outcome = SceneActionOutcome.SKIPPED,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
coroutineScope {
|
||||
scene.actions.map { action ->
|
||||
async {
|
||||
val actionStart = System.currentTimeMillis()
|
||||
val execResult = executeAction(scene.profileId, action)
|
||||
val duration = System.currentTimeMillis() - actionStart
|
||||
execResult.copy(executionDurationMillis = duration)
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val sequentialResults = mutableListOf<SceneActionExecutionResult>()
|
||||
|
||||
+40
@@ -1,8 +1,10 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.ConcurrencyMode
|
||||
import com.flagship.abox.manager.domain.DeviceName
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.ExecutionStrategy
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Scene
|
||||
import com.flagship.abox.manager.domain.SceneAction
|
||||
@@ -156,6 +158,44 @@ class FakeSceneExecutorTest {
|
||||
assertEquals(listOf(notificationAction), executor.sentNotifications)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parallelAbortOnErrorCancelsPendingActions() = runTest {
|
||||
val socketGateway = FakeSocketGateway()
|
||||
socketGateway.setError(
|
||||
profileId,
|
||||
DeviceName("socket-1"),
|
||||
DomainError.DeviceOffline(detail = "offline"),
|
||||
)
|
||||
val executor = FakeSceneExecutor(
|
||||
socketGateway = socketGateway,
|
||||
infraredGateway = FakeInfraredGateway(),
|
||||
delayDurationProvider = { kotlinx.coroutines.delay(1000) },
|
||||
)
|
||||
|
||||
val action1 = socketAction("socket-1", SocketPowerState.ON)
|
||||
val action2 = SceneAction.Delay(
|
||||
id = SceneActionId("abox:delay-1"),
|
||||
profileId = profileId,
|
||||
durationSeconds = 10,
|
||||
)
|
||||
|
||||
val scene = Scene(
|
||||
id = SceneId("${profileId.value}:scene-par-abort"),
|
||||
profileId = profileId,
|
||||
name = "Parallel Abort Scene",
|
||||
concurrencyMode = ConcurrencyMode.PARALLEL,
|
||||
strategy = ExecutionStrategy.ABORT_ON_ERROR,
|
||||
actions = listOf(action1, action2),
|
||||
)
|
||||
|
||||
val result = executor.execute(scene)
|
||||
assertTrue(result is DomainResult.Success)
|
||||
val executionResult = (result as DomainResult.Success).value
|
||||
assertEquals(2, executionResult.results.size)
|
||||
assertEquals(SceneActionOutcome.FAILURE, executionResult.results[0].outcome)
|
||||
assertEquals(SceneActionOutcome.SKIPPED, executionResult.results[1].outcome)
|
||||
}
|
||||
|
||||
private fun scene(actions: List<SceneAction>): Scene = Scene(
|
||||
id = SceneId("${profileId.value}:scene-test"),
|
||||
profileId = profileId,
|
||||
|
||||
Reference in New Issue
Block a user