0.2.51 - update checker fixes
This commit is contained in:
@@ -165,6 +165,7 @@ import com.ponzischeme89.memby.ui.theme.MembyTheme
|
|||||||
import com.ponzischeme89.memby.ui.setup.SignInContent
|
import com.ponzischeme89.memby.ui.setup.SignInContent
|
||||||
import com.ponzischeme89.memby.update.InstallPermission
|
import com.ponzischeme89.memby.update.InstallPermission
|
||||||
import com.ponzischeme89.memby.update.RequiredUpdateSignal
|
import com.ponzischeme89.memby.update.RequiredUpdateSignal
|
||||||
|
import com.ponzischeme89.memby.update.nextUpdateCheckDelay
|
||||||
import com.ponzischeme89.memby.update.requiredUpdateSatisfied
|
import com.ponzischeme89.memby.update.requiredUpdateSatisfied
|
||||||
import com.ponzischeme89.memby.update.ServerUpdateService
|
import com.ponzischeme89.memby.update.ServerUpdateService
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
@@ -247,7 +248,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
*/
|
*/
|
||||||
private const val ONBOARDING_CHECK_TIMEOUT_MS = 2_500L
|
private const val ONBOARDING_CHECK_TIMEOUT_MS = 2_500L
|
||||||
private const val UPDATE_CHECK_TIMEOUT_MS = 2_500L
|
private const val UPDATE_CHECK_TIMEOUT_MS = 2_500L
|
||||||
private const val UPDATE_CHECK_INTERVAL_MS = 60L * 60L * 1_000L
|
// Update policy is live operator state, just like maintenance. The verdict is an in-memory
|
||||||
|
// gateway read, so keeping this close to the maintenance poll means a set that was already
|
||||||
|
// open sees a newly-required release promptly instead of as much as an hour later.
|
||||||
|
private const val UPDATE_CHECK_INTERVAL_MS = 30_000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long to wait before asking again while the gateway has this build retired.
|
* How long to wait before asking again while the gateway has this build retired.
|
||||||
@@ -347,6 +351,11 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
|||||||
// How many checks in a row have been made because this build is retired. Loop
|
// How many checks in a row have been made because this build is retired. Loop
|
||||||
// state rather than app state: nothing outside this effect decides when to ask.
|
// state rather than app state: nothing outside this effect decides when to ask.
|
||||||
var requiredAttempts = 0
|
var requiredAttempts = 0
|
||||||
|
// A launch timeout used to be treated like a successful "nothing to say" verdict,
|
||||||
|
// which put the next attempt an hour away. A television's first DNS/TLS connection
|
||||||
|
// is often the slowest one it makes, so failed or timed-out checks retry on the same
|
||||||
|
// bounded schedule as a retired build until the gateway gives a real answer.
|
||||||
|
var failedAttempts = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
// What the gateway has already said about this build, read from disk rather
|
// What the gateway has already said about this build, read from disk rather
|
||||||
// than from this loop's memory — the refusal is commonly one process old.
|
// than from this loop's memory — the refusal is commonly one process old.
|
||||||
@@ -379,6 +388,11 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
|||||||
initialUpdateCheckComplete = true
|
initialUpdateCheckComplete = true
|
||||||
firstCheck = false
|
firstCheck = false
|
||||||
}
|
}
|
||||||
|
failedAttempts = if (result == null || result.isFailure) {
|
||||||
|
failedAttempts + 1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
requiredAttempts = if (retired != null && appUpdate == null) {
|
requiredAttempts = if (retired != null && appUpdate == null) {
|
||||||
requiredAttempts + 1
|
requiredAttempts + 1
|
||||||
} else {
|
} else {
|
||||||
@@ -388,11 +402,14 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
|||||||
// now. While this build is retired and no verdict has arrived, that schedule is
|
// now. While this build is retired and no verdict has arrived, that schedule is
|
||||||
// seconds rather than an hour — the alternative is a television sitting on a
|
// seconds rather than an hour — the alternative is a television sitting on a
|
||||||
// screen it cannot leave, waiting on an answer nobody has asked for.
|
// screen it cannot leave, waiting on an answer nobody has asked for.
|
||||||
val wait = when {
|
val wait = nextUpdateCheckDelay(
|
||||||
requiredAttempts == 0 -> UPDATE_CHECK_INTERVAL_MS
|
requiredAttempts = requiredAttempts,
|
||||||
requiredAttempts <= UPDATE_REQUIRED_FAST_ATTEMPTS -> UPDATE_REQUIRED_RETRY_MS
|
failedAttempts = failedAttempts,
|
||||||
else -> UPDATE_REQUIRED_BACKOFF_MS
|
fastAttempts = UPDATE_REQUIRED_FAST_ATTEMPTS,
|
||||||
}
|
fastRetryMillis = UPDATE_REQUIRED_RETRY_MS,
|
||||||
|
backoffMillis = UPDATE_REQUIRED_BACKOFF_MS,
|
||||||
|
regularPollMillis = UPDATE_CHECK_INTERVAL_MS,
|
||||||
|
)
|
||||||
withTimeoutOrNull(wait) { updateWake.receive() }
|
withTimeoutOrNull(wait) { updateWake.receive() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,6 +527,10 @@ private fun AppRoot(onCloseSettings: () -> Unit) {
|
|||||||
// it a moment later, twice, during the one stretch of a launch that is busiest.
|
// it a moment later, twice, during the one stretch of a launch that is busiest.
|
||||||
// Hoisted to one call site, the screen and its clip survive the whole cold start.
|
// Hoisted to one call site, the screen and its clip survive the whole cold start.
|
||||||
val openingQuoteStyle = when {
|
val openingQuoteStyle = when {
|
||||||
|
// A required update is a service gate, not launcher content. Once the gateway
|
||||||
|
// answers, uncover it immediately instead of making the viewer finish the
|
||||||
|
// decorative opening clip first. Optional prompts may wait for the intro.
|
||||||
|
appUpdate?.isMandatory == true -> null
|
||||||
// Memby's own clip runs to its end before anything else is drawn. It is the one
|
// Memby's own clip runs to its end before anything else is drawn. It is the one
|
||||||
// thing the app owns and it was, in practice, never seen: the launcher uncovered
|
// thing the app owns and it was, in practice, never seen: the launcher uncovered
|
||||||
// it whenever it happened to be ready, which on a warm start was a fraction of a
|
// it whenever it happened to be ready, which on a warm start was a fraction of a
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ponzischeme89.memby.update
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses when the app should next ask for live update policy.
|
||||||
|
*
|
||||||
|
* A timeout and a positive "no update" response are deliberately different states. The
|
||||||
|
* latter may wait for the regular policy poll; the former has learned nothing and retries
|
||||||
|
* quickly before settling onto a bounded backoff.
|
||||||
|
*/
|
||||||
|
internal fun nextUpdateCheckDelay(
|
||||||
|
requiredAttempts: Int,
|
||||||
|
failedAttempts: Int,
|
||||||
|
fastAttempts: Int,
|
||||||
|
fastRetryMillis: Long,
|
||||||
|
backoffMillis: Long,
|
||||||
|
regularPollMillis: Long,
|
||||||
|
): Long = when {
|
||||||
|
requiredAttempts in 1..fastAttempts -> fastRetryMillis
|
||||||
|
requiredAttempts > fastAttempts -> backoffMillis
|
||||||
|
failedAttempts in 1..fastAttempts -> fastRetryMillis
|
||||||
|
failedAttempts > fastAttempts -> backoffMillis
|
||||||
|
else -> regularPollMillis
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ponzischeme89.memby.update
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class UpdateCheckScheduleTest {
|
||||||
|
@Test
|
||||||
|
fun `a real verdict uses the regular live policy poll`() {
|
||||||
|
assertEquals(30_000L, delay(requiredAttempts = 0, failedAttempts = 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a timed out launch check retries promptly`() {
|
||||||
|
assertEquals(5_000L, delay(requiredAttempts = 0, failedAttempts = 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `repeated failures settle onto bounded backoff`() {
|
||||||
|
assertEquals(60_000L, delay(requiredAttempts = 0, failedAttempts = 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a retired build keeps the urgent schedule even after a successful empty check`() {
|
||||||
|
assertEquals(5_000L, delay(requiredAttempts = 1, failedAttempts = 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delay(requiredAttempts: Int, failedAttempts: Int) = nextUpdateCheckDelay(
|
||||||
|
requiredAttempts = requiredAttempts,
|
||||||
|
failedAttempts = failedAttempts,
|
||||||
|
fastAttempts = 4,
|
||||||
|
fastRetryMillis = 5_000L,
|
||||||
|
backoffMillis = 60_000L,
|
||||||
|
regularPollMillis = 30_000L,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user