fix(sources): a dead custom URL no longer counts as a successful update

Found by an audit of the replace-semantics commit rather than by the change itself.

The success count added orbital and transceivers sources together, so one could stand in
for the other. With the built-in sources replaced there are two requests instead of 28: if
the operator's TLE URL was down and SatNOGS answered, the count was 1, no exception was
raised, and setUpdateSuccessful stamped a fresh timestamp for an update that refreshed no
orbital elements at all. That also suppressed the 48-hour auto-update retry, which keys off
that timestamp - so the operator was left with stale orbits, a screen saying the update
worked, and nothing scheduled to correct it.

The failure existed before this rebuild, but 26 other sources masked it. Narrowing the
source set made it easy to hit, which is why it belongs with these commits rather than in a
backlog.

Orbital sources are now counted on their own. A test covers the exact case: transceivers
answers, the custom TLE URL does not, and the update must raise rather than record success.

Also: an upload that found nothing waiting said "Uploaded 0 QSO". Accurate, but it reads
oddly when the queue was already clear, so that case has its own wording now.
This commit is contained in:
mckero committed 2026-08-26 04:56:44 +00:00
1 parent cd70e3654c
commit 6db10b5b72
5 files changed
+49 -4

No files matched your search

@@ -107,12 +107,17 @@ class DatabaseRepo(
// launch all network requests concurrently
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
// Count successful sources: zero successes = update failed (timestamp untouched, exception surfaced in the UI)
val tleResults = tleJobs.awaitAll()
val radioResults = radioJobs.awaitAll()
val successCount = tleResults.count { it.second != null } + radioResults.count { it.second != null }
if (successCount == 0) {
throw java.io.IOException("All data sources failed to download")
// Orbital elements are counted on their own. A combined count let a successful transceivers
// fetch stand in for a failed orbital one: with a custom TLE URL there are two requests
// rather than 28, so if that URL was down and SatNOGS answered, the total was 1, no
// exception was raised, and setUpdateSuccessful stamped a fresh timestamp for an update
// that refreshed no orbital data at all - which also suppressed the 48-hour auto-update
// retry that keys off that timestamp. The failure existed before but 26 other sources hid
// it; replacing them made it easy to hit.
if (tleResults.none { it.second != null }) {
throw java.io.IOException("No orbital data source could be downloaded")
}
// parse fetched data concurrently and associate with types
val importedEntries = tleResults.flatMap { (url, stream) ->
@@ -159,6 +159,42 @@ class DatabaseRepoTest {
)
}
/**
* A dead custom URL must fail the update even when the transceivers source answers.
*
* The counts used to be added together, so one transceivers success covered a total orbital
* failure: no exception, and a fresh "updated successfully" timestamp for an update that
* refreshed nothing. Replacing the built-in sources shrank the denominator from 28 to 2 and
* made that easy to hit.
*/
@Test
fun `a dead custom url fails the update even if transceivers succeed`() = runTest(dispatcher) {
val localSource = FakeLocalSource()
val remoteSource = FakeRemoteSource().apply {
Sources.transceiversDataUrls.values.filter { it.isNotBlank() }
.forEach { networkStreams[it] = { "[]".byteInputStream() } }
}
val settingsRepo = FakeSettingsRepo(
dataSources = DataSourcesSettings(
useCustomTLE = true,
useCustomTransceivers = false,
tleUrl = "https://example.com/dead.csv",
transceiversUrl = ""
)
)
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
var threw = false
try {
repository.updateFromRemote()
} catch (_: java.io.IOException) {
threw = true
}
assertTrue("a total orbital failure must raise", threw)
assertTrue("no entries may be inserted", localSource.insertedEntries.isEmpty())
}
private fun validCsvStream(): InputStream = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
@@ -263,6 +263,7 @@
<string name="wavelog_saved">已存入本地日志</string>
<string name="wavelog_upload_done">已上传 %1$d 条, %2$d 条仍在队列</string>
<string name="wavelog_upload_all_ok">已上传 %1$d 条</string>
<string name="wavelog_nothing_to_upload">没有待上传的联络</string>
<string name="wavelog_not_configured">未配置 WaveLog 服务器</string>
<string name="wavelog_no_station">读不到站点信息 - 请检查站点 ID 与密钥权限</string>
<string name="wavelog_upload_failed">上传失败</string>
@@ -294,6 +294,7 @@
<string name="wavelog_saved">Saved to local log</string>
<string name="wavelog_upload_done">Uploaded %1$d, %2$d still queued</string>
<string name="wavelog_upload_all_ok">Uploaded %1$d QSO</string>
<string name="wavelog_nothing_to_upload">Nothing waiting to upload</string>
<string name="wavelog_not_configured">WaveLog server not configured</string>
<string name="wavelog_no_station">Cannot read the station profile - check the station ID and key permissions</string>
<string name="wavelog_upload_failed">Upload failed</string>