Skip to content

Introduce FailFast utils to provide offensive programing #5373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.common.util.FailFast
import io.homeassistant.companion.android.settings.SettingsActivity
import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme

Expand All @@ -30,6 +32,7 @@ import io.homeassistant.companion.android.util.compose.HomeAssistantAppTheme
* This activity is not meant to be used in production that's why it is only accessible through the debug build type.
* To avoid any mistakes this activity is only accessible from a shortcut
*/
@AndroidEntryPoint
class DevPlaygroundActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -69,6 +72,13 @@ private fun DevPlayGroundScreen(context: Context? = null) {
}) {
Text("Start Settings")
}
Button(modifier = Modifier.padding(top = 16.dp), onClick = {
FailFast.failWhen(true) {
"This should stop the process."
}
}) {
Text("Fail fast")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import io.homeassistant.companion.android.common.R as commonR
import io.homeassistant.companion.android.common.util.CHANNEL_HIGH_ACCURACY
import io.homeassistant.companion.android.common.util.FailFast
import io.homeassistant.companion.android.sensors.LocationSensorManager
import io.homeassistant.companion.android.util.ForegroundServiceLauncher
import kotlin.math.abs
Expand Down Expand Up @@ -148,7 +149,12 @@ class HighAccuracyLocationService : Service() {
notification = notificationBuilder.build()

val type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) FOREGROUND_SERVICE_TYPE_LOCATION else 0
LAUNCHER.onServiceCreated(this, notificationId, notification, type)
FailFast.failOnCatch {
// Sometimes the service cannot be started as foreground due to the app being in a state where
// this is not allowed. We didn't identified yet how to avoid starting the service in this state.
// To avoid a crash, we catch it with FailFast, which will only crash in debug builds.
LAUNCHER.onServiceCreated(this, notificationId, notification, type)
}

Timber.d("High accuracy location service created -> onCreate")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.homeassistant.companion.android.common.util

val DefaultFailFastHandler = CrashFailFastHandler
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import io.homeassistant.companion.android.common.data.servers.ServerManager.Comp
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepositoryFactory
import io.homeassistant.companion.android.common.data.wifi.WifiHelper
import io.homeassistant.companion.android.common.util.FailFast
import io.homeassistant.companion.android.database.sensor.SensorDao
import io.homeassistant.companion.android.database.server.Server
import io.homeassistant.companion.android.database.server.ServerDao
Expand Down Expand Up @@ -89,7 +90,12 @@ class ServerManagerImpl @Inject constructor(
mutableServers.values.any {
it.type == ServerType.DEFAULT &&
it.connection.isRegistered() &&
authenticationRepository(it.id).getSessionState() == SessionState.CONNECTED
FailFast.failOnCatch(
message = {
"""Fail to get authenticationRepository for ${it.id} current authenticationRepos ids: ${authenticationRepos.keys} """
},
fallback = false,
) { authenticationRepository(it.id).getSessionState() == SessionState.CONNECTED }
}

override suspend fun addServer(server: Server): Int {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.homeassistant.companion.android.common.util

import io.homeassistant.companion.android.common.util.FailFast.setHandler

/**
* A handler for [FailFast] exceptions.
*
* Implement this interface to define a custom handler if you want to do something different than [DefaultFailFastHandler].
* Don't forget to register the handler in [FailFast.setHandler].
*/
interface FailFastHandler {
fun handleException(exception: Exception, additionalMessage: String? = null)
}

private class FailFastException : Exception {
constructor(message: String) : super(message)
constructor(exception: Throwable) : super(exception)

init {
// We remove any reference to FailFast from the stack trace to make it easier to find the root cause
stackTrace = stackTrace.filterNot { it.className == FailFast::class.java.name }.toTypedArray()
}
}

/**
* A utility object for implementing the "fail fast" [principle](https://en.wikipedia.org/wiki/Fail-fast_system).
*
* This object provides methods to check conditions and handle exceptions,
* allowing to identify and address issues sooner in the development lifecycle.
*
* By default, it uses [DefaultFailFastHandler] to log exceptions. This behavior can be
* customized by providing a different [FailFastHandler] implementation using [setHandler].
*
* [DefaultFailFastHandler] behavior is different based on the build target debug or release.
*/
object FailFast {
private var handler: FailFastHandler = DefaultFailFastHandler

fun setHandler(handler: FailFastHandler) {
this.handler = handler
}

fun failWhen(condition: Boolean, message: () -> String) {
if (condition) {
handler.handleException(FailFastException(message()))
}
}

fun failOnCatch(message: () -> String? = { null }, block: () -> Unit) {
failOnCatch<Unit>(message, Unit, block)
}

fun <T> failOnCatch(message: () -> String? = { null }, fallback: T, block: () -> T): T {
return try {
block()
} catch (e: Throwable) {
handler.handleException(FailFastException(e), message())
fallback
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.homeassistant.companion.android.common.util

import kotlin.system.exitProcess
import timber.log.Timber

private const val HEADER = """
████████████████████████████████████████████████████████████████
██ ██
██ !!! CRITICAL FAILURE: FAIL-FAST !!! ██
██ ██
████████████████████████████████████████████████████████████████
"""

private const val SEPARATOR = """----------------------------------------------------------------"""

object CrashFailFastHandler : FailFastHandler {
override fun handleException(exception: Exception, additionalMessage: String?) {
Timber.e(
exception,
buildString {
appendLine(HEADER.trimIndent())
appendLine()
appendLine(
"""
An unrecoverable error has occurred, and the FailFast mechanism
has been triggered. The application cannot continue and will now exit.

ACTION REQUIRED: This error must be investigated and resolved.
Review the accompanying stack trace for details.
""".trimIndent(),
)
appendLine(SEPARATOR)
additionalMessage?.let {
appendLine()
appendLine(it)
appendLine(SEPARATOR)
appendLine()
}
},
)
exitProcess(1)
}
}

object LogOnlyFailFastHandler : FailFastHandler {
override fun handleException(exception: Exception, additionalMessage: String?) {
Timber.e(
exception,
buildString {
appendLine(HEADER.trimIndent())
appendLine()
appendLine(
"""
The exception is ignored to avoid a crash but it should be handled
if you see this please open an issue on github https://github.com/home-assistant/android/issues/new/choose
""".trimIndent(),
)
additionalMessage?.let {
appendLine()
appendLine(it)
appendLine(SEPARATOR)
appendLine()
}
},
)
// no-op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.homeassistant.companion.android.common.util

val DefaultFailFastHandler = LogOnlyFailFastHandler
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.homeassistant.companion.android.common.util

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class FailFastTest {

@Test
fun `Given a throw when invoking failOnCatch then properly propagate the error with custom stacktrace`() {
var exceptionCaught: Exception? = null
FailFast.setHandler(object : FailFastHandler {
override fun handleException(exception: Exception, additionalMessage: String?) {
exceptionCaught = exception
}
})
FailFast.failOnCatch {
throw IllegalArgumentException("expected error")
}

assertEquals(exceptionCaught?.message, "java.lang.IllegalArgumentException: expected error")
assertEquals(FailFastTest::class.java.name, exceptionCaught?.stackTrace?.first()?.className)
}
}