Skip to content

Commit

Permalink
#52 Return failureCause for runs via the API
Browse files Browse the repository at this point in the history
Still need to
- persist it to the DB for later retrieval after-the-fact
- possibly change the status code to a `4xx` error on the synchronous trigger API when the run is failed
  • Loading branch information
chadlwilson committed Dec 13, 2021
1 parent 8d1eb8b commit fe99bf9
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 12 deletions.
9 changes: 7 additions & 2 deletions src/main/kotlin/recce/server/api/RunApiModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
import io.micronaut.core.annotation.Introspected
import io.swagger.v3.oas.annotations.media.Schema
import recce.server.recrun.*
import recce.server.util.ThrowableUtils.extractFailureCause
import java.time.Duration
import java.time.Instant

Expand All @@ -29,7 +30,10 @@ data class RunApiModel(
val status: RunStatus,

@field:Schema(description = "Summary results from the run")
val summary: RunSummary?
val summary: RunSummary?,

@field:Schema(description = "Reason for failure of the run")
val failureCause: String? = null,
) {
@get:Schema(type = "number", format = "double", description = "How long the run took, in seconds")
@get:JsonProperty("completedDurationSeconds")
Expand All @@ -51,7 +55,8 @@ data class RunApiModel(
summary = summaryBuilder
.sourceMeta(run.sourceMeta)
.targetMeta(run.targetMeta)
.build()
.build(),
failureCause = run.failureCause?.run { extractFailureCause(this) }
)
}
}
Expand Down
16 changes: 16 additions & 0 deletions src/main/kotlin/recce/server/util/ThrowableUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package recce.server.util

import com.google.common.base.Throwables

object ThrowableUtils {
fun extractFailureCause(throwable: Throwable): String {
val rootCause = Throwables.getRootCause(throwable)
return if (rootCause.equals(throwable)) {
throwable.messageOrClassName()
} else {
"${throwable.messageOrClassName()}, rootCause=[${rootCause.messageOrClassName()}]"
}
}

private fun Throwable.messageOrClassName(): String = this.message ?: this.javaClass.simpleName
}
47 changes: 37 additions & 10 deletions src/test/kotlin/recce/server/api/DatasetRecRunControllerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ import org.junit.jupiter.params.provider.ValueSource
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import recce.server.dataset.DataLoadException
import recce.server.dataset.DatasetRecRunner
import recce.server.recrun.*
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter

private const val sampleKeysLimit = 3
private const val notFoundId = 0
private const val testDataset = "testDataset"
private val testCompletedDuration = Duration.ofMinutes(3).plusNanos(234)
Expand All @@ -47,21 +50,19 @@ private val testResults = RecRun(
summary = MatchStatus(1, 2, 3, 4)
}

private val service = mock<DatasetRecRunner> {
private fun mockService() = mock<DatasetRecRunner> {
on { runFor(eq(testDataset)) } doReturn Mono.just(testResults)
}

private val runRepository = mock<RecRunRepository> {
private fun mockRunRepository() = mock<RecRunRepository> {
on { findById(testResults.id!!) } doReturn Mono.just(testResults)
on { existsById(testResults.id!!) } doReturn Mono.just(true)
on { findById(notFoundId) } doReturn Mono.empty()
on { existsById(notFoundId) } doReturn Mono.just(false)
on { findTop10ByDatasetIdOrderByCompletedTimeDesc(testDataset) } doReturn Flux.just(testResults, testResults)
}

private const val sampleKeysLimit = 3

private fun recordRepository(sampleRecords: List<RecRecord>) = mock<RecRecordRepository> {
private fun mockRecordRepository(sampleRecords: List<RecRecord>) = mock<RecRecordRepository> {
on { findFirstByRecRunIdSplitByMatchStatus(testResults.id!!, sampleKeysLimit) } doReturn Flux.fromIterable(sampleRecords)
on { findFirstByRecRunIdSplitByMatchStatus(notFoundId, sampleKeysLimit) } doReturn Flux.empty()
}
Expand All @@ -72,7 +73,9 @@ internal class DatasetRecRunControllerTest {
List(2) { RecRecord(RecRecordKey(testResults.id!!, "target-$it"), targetData = "set") } +
List(3) { RecRecord(RecRecordKey(testResults.id!!, "both-$it"), sourceData = "set", targetData = "set2") }

private val controller = DatasetRecRunController(service, runRepository, recordRepository(sampleRows))
private val service = mockService()
private val runRepository = mockRunRepository()
private val controller = DatasetRecRunController(service, runRepository, mockRecordRepository(sampleRows))

@Test
fun `can get run by id`() {
Expand Down Expand Up @@ -126,10 +129,34 @@ internal class DatasetRecRunControllerTest {

@Test
fun `trigger should delegate to service`() {
StepVerifier.create(controller.triggerRun(DatasetRecRunController.RunCreationParams(eq(testDataset))))
StepVerifier.create(controller.triggerRun(DatasetRecRunController.RunCreationParams(testDataset)))
.assertNext(::assertThatModelMatchesTestResults)
.verifyComplete()
}

@Test
fun `failed run should return with cause`() {
val failureCause = DataLoadException("Could not load data", IllegalArgumentException("Root Cause"))
val failedRun = RecRun(
id = testResults.id,
datasetId = testDataset,
createdTime = testResults.createdTime,
).asFailed(failureCause)

whenever(service.runFor(testDataset)).doReturn(Mono.just(failedRun))

StepVerifier.create(controller.triggerRun(DatasetRecRunController.RunCreationParams(testDataset)))
.assertNext { apiModel ->
SoftAssertions.assertSoftly { softly ->
softly.assertThat(apiModel.id).isEqualTo(testResults.id)
softly.assertThat(apiModel.datasetId).isEqualTo(testResults.datasetId)
softly.assertThat(apiModel.createdTime).isEqualTo(testResults.createdTime)
softly.assertThat(apiModel.completedTime).isNotNull
softly.assertThat(apiModel.status).isEqualTo(RunStatus.Failed)
softly.assertThat(apiModel.failureCause).isEqualTo("Could not load data, rootCause=[Root Cause]")
}
}.verifyComplete()
}
}

@MicronautTest
Expand Down Expand Up @@ -255,18 +282,18 @@ internal class DatasetRecRunControllerApiTest {

@MockBean(DatasetRecRunner::class)
fun reconciliationService(): DatasetRecRunner {
return service
return mockService()
}

@Replaces(H2RecRunRepository::class)
@Singleton
fun runRepository(): RecRunRepository {
return runRepository
return mockRunRepository()
}

@Replaces(H2RecRecordRepository::class)
@Singleton
fun recordRepository(): RecRecordRepository {
return recordRepository(sampleRows)
return mockRecordRepository(sampleRows)
}
}
48 changes: 48 additions & 0 deletions src/test/kotlin/recce/server/util/ThrowableUtilsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package recce.server.util

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class ThrowableUtilsTest {

private val rootCause = IllegalArgumentException("root")
private val causedByRoot = IllegalArgumentException("causedByRoot", rootCause)
private val causedByCausedBy = IllegalArgumentException(
"causedByCausedByRoot" +
"",
causedByRoot
)

@Test
fun `should return just message for throwable with no cause`() {
assertThat(ThrowableUtils.extractFailureCause(rootCause))
.isEqualTo("root")
}

@Test
fun `should return message with root cause for throwable with cause`() {
assertThat(ThrowableUtils.extractFailureCause(causedByRoot))
.isEqualTo("causedByRoot, rootCause=[root]")
}

@Test
fun `should ignore intermediary exceptions`() {
assertThat(ThrowableUtils.extractFailureCause(causedByCausedBy))
.isEqualTo("causedByCausedByRoot, rootCause=[root]")
}

@Test
fun `should return exception type if there is no message`() {
val noMessageRoot = IllegalArgumentException()
assertThat(ThrowableUtils.extractFailureCause(noMessageRoot))
.isEqualTo("IllegalArgumentException")
}

@Test
fun `should return exception type if there is no message for throwable with cause`() {
val noMessageRoot = IllegalArgumentException()
val bad = IllegalCallerException("bad", noMessageRoot)
assertThat(ThrowableUtils.extractFailureCause(bad))
.isEqualTo("bad, rootCause=[IllegalArgumentException]")
}
}

0 comments on commit fe99bf9

Please sign in to comment.