I'm trying to figure out why this `first()` doesn'...
# coroutines
r
I'm trying to figure out why this
first()
doesn't get cancelled by the timeout
Copy code
private suspend fun executeProgram(
        program: TVControlProgram,
        targetCondition: StateCondition
    ) {
        Timber.tag("TVControlV2").d { " Executing ${program.commands.size} commands for program ${program.id} (non-interruptible)" }
        withContext(NonCancellable) {
            program.commands.forEachIndexed { index, command ->
                Timber.tag("TVControlV2").v { " Command ${index + 1}/${program.commands.size}: ${command.javaClass.simpleName}" }
                executeCommand(command)
            }
        }

        // Wait for stabilization - monitors TV state reactively (interruptible)
        // Must satisfy BOTH the program's target state AND the overall request target
        Timber.tag("TVControlV2").d { " Waiting for stabilization (timeout=${program.stabilizationTimeout}), program target: ${program.targetState}, request target: $targetCondition" }
        val startState = getCurrentState()
        Timber.tag("TVControlV2").d { " Stabilization starting from state: $startState" }

        withTimeout(program.stabilizationTimeout) {
            var lastLoggedState: TVStateV2? = null
            combine(
                cecFlows.tvState,
                cecFlows.activeSource
            ) { tvState, activeSource ->
                mapToTVStateV2(tvState, activeSource)
            }.first { state ->
                // Log state changes
                if (state != lastLoggedState) {
                    Timber.tag("TVControlV2").d { " Stabilization state change: $lastLoggedState -> $state" }
                    lastLoggedState = state
                }

                // Check if we've reached the program's immediate target OR the final request target
                // Use OR because programs may be intermediate steps toward the final goal
                val programTargetReached = program.targetState.matches(state)
                val finalTargetReached = targetCondition.matches(state)
                val matches = programTargetReached || finalTargetReached

                Timber.tag("TVControlV2").v { " Stabilization check: state=$state, programTarget=$programTargetReached, finalTarget=$finalTargetReached, matches=$matches" }

                if (matches) {
                    Timber.tag("TVControlV2").d { " Stabilization complete, state: $state (programTarget=$programTargetReached, finalTarget=$finalTargetReached)" }
                }
                matches
            }
        }
    }
There is no other suspend stuff around.
j
How do you know it doesn't?
r
This is the code around it:
Copy code
try {
                        Timber.tag("TVControlV2").d { " Executing program ${program.id} (${program.commands.size} commands)" }
                        executeProgram(program, activeRequest.targetCondition)
                        Timber.tag("TVControlV2").d { " Program ${program.id} completed successfully" }
                        // Success - clear failed programs
                        activeRequest = activeRequest.copy(failedPrograms = emptySet())
                    } catch (e: CancellationException) {
                        throw e
                    } catch (e: TimeoutCancellationException) {
                        // Program timed out - mark as failed and try next
                        Timber.tag("TVControlV2").w(e) { "Program ${program.id} timed out during stabilization" }
                        activeRequest = activeRequest.copy(
                            failedPrograms = activeRequest.failedPrograms + program.id
                        )
                    } catch (e: Exception) {
                        // Other error - mark as failed
                        Timber.tag("TVControlV2").e(e) { "Program ${program.id} failed with exception" }
                        activeRequest = activeRequest.copy(
                            failedPrograms = activeRequest.failedPrograms + program.id
                        )
                    }
But the log line in
TimeoutCancellationException
doesn't show up
j
Isn't
TimeoutCancellationException
a
CancellationException
? The first catch will prevent the second from running
r
Ah duh, reordered, thanks
Something higher up must have swallowed it though
j
I would personally avoid
withTimeout
and stick to
withTimeoutOrNull
. The latter checks whether the
TimeoutCancellationException
is actually its own, so it only returns
null
when its own timeout expired.
1
With
withTimeout
you cannot know whether the exception comes from the
withTimeout
that you call, or from another
withTimeout
higher up that your code is wrapped in
r
Ah nice, ok, will do