Use fresher manifest between cached and embedded (#1789)

This commit is contained in:
Derek Ellis
2026-03-04 12:49:51 -05:00
committed by GitHub
parent 728fd42caa
commit ba987470cc
5 changed files with 50 additions and 6 deletions

View File

@@ -4,7 +4,7 @@
* Fix: Prevent StackOverflow when serializing recursive class definitions. The serial name
description now ends when encountering a recursive descriptor.
* Fix: Embedded code will now be used first if it is fresher than cached network code.
## [1.25.0] - 2026-01-12
[1.25.0]: https://github.com/cashapp/zipline/releases/tag/1.25.0

View File

@@ -22,6 +22,7 @@ import app.cash.zipline.loader.internal.fetcher.FsCachingFetcher
import app.cash.zipline.loader.internal.fetcher.FsEmbeddedFetcher
import app.cash.zipline.loader.internal.fetcher.HttpFetcher
import app.cash.zipline.loader.internal.fetcher.LoadedManifest
import app.cash.zipline.loader.internal.fetcher.LoadedManifestComparator
import app.cash.zipline.loader.internal.fetcher.fetch
import app.cash.zipline.loader.internal.getApplicationManifestFileName
import app.cash.zipline.loader.internal.receiver.FsSaveReceiver
@@ -664,9 +665,15 @@ class ZiplineLoader internal constructor(
eventListener: EventListener,
nowEpochMs: Long,
): LoadedManifest? {
val result = cachingFetcher?.loadPinnedManifest(applicationName, nowEpochMs)
?: embeddedFetcher?.loadEmbeddedManifest(applicationName)
?: return null
val pinnedManifest = cachingFetcher?.loadPinnedManifest(applicationName, nowEpochMs)
val embeddedManifest = embeddedFetcher?.loadEmbeddedManifest(applicationName)
// If both are available, pick the freshest of the two
val result = if (pinnedManifest != null && embeddedManifest != null) {
maxOf(pinnedManifest, embeddedManifest, LoadedManifestComparator)
} else {
pinnedManifest ?: embeddedManifest ?: return null
}
// Defend against changes to the locally-cached copy.
val verifiedKey = manifestVerifier.verify(result.manifestBytes, result.manifest)

View File

@@ -49,3 +49,12 @@ internal fun LoadedManifest(manifestBytes: ByteString): LoadedManifest {
?: error("freshAtEpochMs is required for loaded manifests, but was null")
return LoadedManifest(manifestBytes, manifest, freshAtEpochMs)
}
internal object LoadedManifestComparator : Comparator<LoadedManifest> {
override fun compare(
a: LoadedManifest,
b: LoadedManifest,
): Int {
return a.freshAtEpochMs.compareTo(b.freshAtEpochMs)
}
}

View File

@@ -94,7 +94,7 @@ class LoaderTester(
cache.close()
}
fun seedEmbedded(applicationName: String, seed: String) {
fun seedEmbedded(applicationName: String, seed: String, freshAtEpochMs: Long = 5L) {
embeddedFileSystem.createDirectories(embeddedDir)
val ziplineFileByteString =
testFixtures.createZiplineFile(LoaderTestFixtures.createJs(seed), "$seed.js")
@@ -102,7 +102,7 @@ class LoaderTester(
val embeddedManifest = LoaderTestFixtures.createRelativeEmbeddedManifest(
seed = seed,
seedFileSha256 = sha256,
seedFreshAtEpochMs = 5L,
seedFreshAtEpochMs = freshAtEpochMs,
includeUnknownFieldInJson = includeUnknownFieldInJson,
)
embeddedFileSystem.write(embeddedDir / sha256.hex()) {

View File

@@ -53,6 +53,7 @@ class ZiplineLoaderTest {
private lateinit var httpClient: FakeZiplineHttpClient
private lateinit var embeddedFileSystem: FileSystem
private lateinit var embeddedDir: Path
private lateinit var cache: ZiplineCache
private val testFixtures = LoaderTestFixtures()
@@ -63,6 +64,7 @@ class ZiplineLoaderTest {
httpClient = tester.httpClient
embeddedFileSystem = tester.embeddedFileSystem
embeddedDir = tester.embeddedDir
cache = tester.cache
}
@AfterTest
@@ -410,6 +412,32 @@ class ZiplineLoaderTest {
}
}
@Test
fun freshestCachedOrEmbeddedManifestTakesPrecedence() = runBlocking {
cache.pinManifest("test", LoadedManifest(testFixtures.manifestByteString, freshAtEpochMs = 25L), tester.nowMillis)
// Embedded code is fresher than cached code
tester.seedEmbedded("test", "test", freshAtEpochMs = 100L)
val zipline = (
loader.loadOnce(
applicationName = "test",
freshnessChecker = FakeFreshnessCheckerFresh,
manifestUrl = MANIFEST_URL,
) as LoadResult.Success
).zipline
// Loaded the embedded "test" code, not the cached "alpha/bravo" code
assertEquals(
"""
|test loaded
|
""".trimMargin(),
zipline.getLog(),
)
zipline.close()
}
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") // Access :zipline-loader internals.
private fun Zipline.getLog(): String? = app.cash.zipline.internal.getLog(quickJs)