Show an unreachable origin in the web UI (step 19)
A watcher that cannot fetch leaves the server perfectly reachable and every branch row stale — a calm list that implies nothing changed. The state was already recorded and served at /api/watcher; only nothing rendered it. The banner lives in the shared nav fragment, so every view inherits it, and is fed from the view's own polling tick without being chained to it. It stays separate from live-indicator: that one reports whether the browser reaches the server, this one whether the server reaches origin. Also caps the log volume the outage exposed: a lasting fetch failure is logged when its message changes, not on every cycle, and the recovery is logged once. Same for an invalid atTimes slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
18a41e9ced
commit
0193386642
@@ -1,5 +1,8 @@
|
||||
package de.hoennig.gittally.watcher
|
||||
|
||||
import ch.qos.logback.classic.Logger
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import ch.qos.logback.core.read.ListAppender
|
||||
import de.hoennig.gittally.build.ArtifactKeys
|
||||
import de.hoennig.gittally.build.ArtifactStore
|
||||
import de.hoennig.gittally.build.BuildExecutor
|
||||
@@ -24,6 +27,7 @@ import io.kotest.matchers.booleans.shouldBeTrue
|
||||
import io.kotest.matchers.collections.shouldBeEmpty
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -32,6 +36,7 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Clock
|
||||
@@ -172,6 +177,28 @@ class WatcherTest : FunSpec() {
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
test("a lasting fetch failure is logged once, and so is the recovery") {
|
||||
val harness = Harness()
|
||||
val logged = captureWatcherLog()
|
||||
every { harness.gitService.fetchOrigin(any()) } throws RuntimeException("origin unreachable")
|
||||
|
||||
repeat(5) { harness.watcher.poll(harness.workingDir) }
|
||||
|
||||
// one wrong token used to write a warning every ten seconds, 297 of them in an hour
|
||||
logged().filter { it.contains("fetching origin failed") } shouldHaveSize 1
|
||||
|
||||
every { harness.gitService.fetchOrigin(any()) } returns Unit
|
||||
repeat(3) { harness.watcher.poll(harness.workingDir) }
|
||||
|
||||
logged().filter { it.contains("fetching origin succeeded again") } shouldHaveSize 1
|
||||
|
||||
// a different failure is a different message and is worth saying again
|
||||
every { harness.gitService.fetchOrigin(any()) } throws RuntimeException("host is down")
|
||||
harness.watcher.poll(harness.workingDir)
|
||||
|
||||
logged().filter { it.contains("fetching origin failed") } shouldHaveSize 2
|
||||
}
|
||||
|
||||
test("poll enqueues changed local branches before recent new origin branches") {
|
||||
val harness = Harness()
|
||||
every { harness.gitService.originBranches(any()) } returns listOf("main", "feature/new")
|
||||
@@ -745,3 +772,12 @@ class WatcherTest : FunSpec() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Collects this spec's [Watcher] log messages; the returned lambda reads them at any point. */
|
||||
private fun captureWatcherLog(): () -> List<String> {
|
||||
val logger = LoggerFactory.getLogger(Watcher::class.java) as Logger
|
||||
val appender = ListAppender<ILoggingEvent>()
|
||||
appender.start()
|
||||
logger.addAppender(appender)
|
||||
return { appender.list.map { it.formattedMessage } }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user