From 6ab402da51d5ca2574da7c4ecfced67339b6202a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=8B=D1=82=D0=BA=D0=BE=D0=B2=20=D0=A0=D0=BE=D0=BC?= =?UTF-8?q?=D0=B0=D0=BD?= Date: Fri, 22 May 2026 13:22:17 +0300 Subject: [PATCH] =?UTF-8?q?perf(yandex):=20=D1=81=D1=83=D0=B7=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B8=D0=BD=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BA=D1=8D=D1=88=D0=B0=20Disk=20API=20=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=D1=87=D1=91?= =?UTF-8?q?=D1=82=D1=87=D0=B8=D0=BA=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Инвалидирую list/get по префиксу пути вместо полной очистки, учитываю вызовы в cloudApiCallCount для замеров. --- .../repository/YandexDiskRepository.kt | 64 ++++++++++++++++--- .../repository/YandexDiskRepositoryTest.kt | 2 + 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepository.kt b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepository.kt index 1558540..2e0981f 100644 --- a/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepository.kt +++ b/domain-vault/src/main/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepository.kt @@ -25,6 +25,7 @@ import java.io.FilterInputStream import java.io.IOException import java.io.InputStream import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong class YandexDiskRepository( private val api: YandexDiskApi, @@ -38,6 +39,13 @@ class YandexDiskRepository( private val listCache = ConcurrentHashMap() private val getCache = ConcurrentHashMap() + private val cloudApiCallCount = AtomicLong(0) + + fun cloudApiCallCount(): Long = cloudApiCallCount.get() + + fun resetCloudApiCallCount() { + cloudApiCallCount.set(0) + } suspend fun diskInfo(): DiskInfoDto = withContext(ioDispatcher) { val now = System.currentTimeMillis() @@ -93,7 +101,7 @@ class YandexDiskRepository( suspend fun createFolder(path: String): Unit = withContext(ioDispatcher) { val resp = wrapAuth { api.createFolder(path) } when (resp.code()) { - 201, 409 -> invalidateDiskMetaCaches() + 201, 409 -> invalidateDiskMetaCaches(path) else -> throw failure("createFolder", resp) } } @@ -101,14 +109,14 @@ class YandexDiskRepository( suspend fun delete(path: String, permanently: Boolean = true): Unit = withContext(ioDispatcher) { val resp = wrapAuth { api.deleteResource(path, permanently) } when (resp.code()) { - 204 -> invalidateDiskMetaCaches() + 204 -> invalidateDiskMetaCaches(path) 202 -> { val link = resp.body()?.use { body -> parseLink(body) } ?: throw IOException("DELETE 202 without body") awaitOperation(link.href) - invalidateDiskMetaCaches() + invalidateDiskMetaCaches(path) } - 404 -> invalidateDiskMetaCaches() + 404 -> invalidateDiskMetaCaches(path) else -> throw failure("delete", resp) } } @@ -122,7 +130,7 @@ class YandexDiskRepository( throw failure("patch", resp) } resp.body()?.close() - invalidateDiskMetaCaches() + invalidateDiskMetaCaches(path) } suspend fun uploadBytes(path: String, bytes: ByteArray, overwrite: Boolean = true): Unit = @@ -134,10 +142,11 @@ class YandexDiskRepository( val body = bytes.toRequestBody(OCTET_STREAM) val req = Request.Builder().url(link.href).put(body).build() repeat(LOCKED_RETRY_MAX) { attempt -> + recordCloudApiCall() rawHttp.newCall(req).execute().use { resp -> when { resp.isSuccessful -> { - invalidateDiskMetaCaches() + invalidateDiskMetaCaches(path) return@withContext } resp.code == 423 && attempt < LOCKED_RETRY_MAX - 1 -> @@ -158,10 +167,11 @@ class YandexDiskRepository( val body = file.asRequestBody(OCTET_STREAM) val req = Request.Builder().url(link.href).put(body).build() repeat(LOCKED_RETRY_MAX) { attempt -> + recordCloudApiCall() rawHttp.newCall(req).execute().use { resp -> when { resp.isSuccessful -> { - invalidateDiskMetaCaches() + invalidateDiskMetaCaches(path) return@withContext } resp.code == 423 && attempt < LOCKED_RETRY_MAX - 1 -> @@ -186,6 +196,7 @@ class YandexDiskRepository( } val req = Request.Builder().url(link.href).get().build() repeat(LOCKED_RETRY_MAX) { attempt -> + recordCloudApiCall() val resp = rawHttp.newCall(req).execute() when { resp.isSuccessful -> { @@ -281,18 +292,47 @@ class YandexDiskRepository( getCache[path] = value } - private fun invalidateDiskMetaCaches() { + private fun invalidateDiskMetaCaches(changedDiskPath: String? = null) { synchronized(diskCacheLock) { diskInfoCached = null diskInfoCachedUntilMs = 0L } - listCache.clear() - getCache.clear() + if (changedDiskPath == null) { + listCache.clear() + getCache.clear() + return + } + val prefixes = cachePrefixesForPath(changedDiskPath) + listCache.keys.removeAll { key -> + prefixes.any { prefix -> + key.path.startsWith(prefix) || prefix.startsWith(key.path.trimEnd('/')) + } + } + getCache.keys.removeAll { cachedPath -> + prefixes.any { prefix -> + cachedPath.startsWith(prefix) || prefix.startsWith(cachedPath.trimEnd('/')) + } + } + } + + private fun cachePrefixesForPath(diskPath: String): List { + val normalized = diskPath.trimEnd('/') + val out = mutableListOf() + var current = normalized + while (current.isNotEmpty()) { + out.add(current) + out.add("$current/") + val slash = current.lastIndexOf('/') + if (slash <= 0) break + current = current.substring(0, slash) + } + return out } private suspend inline fun wrapAuth(crossinline block: suspend () -> T): T { repeat(LOCKED_RETRY_MAX) { attempt -> try { + recordCloudApiCall() return block() } catch (e: HttpException) { when (e.code()) { @@ -313,6 +353,10 @@ class YandexDiskRepository( error("unreachable") } + private fun recordCloudApiCall() { + cloudApiCallCount.incrementAndGet() + } + private fun failure(op: String, resp: Response): IOException { val msg = resp.errorBody()?.string() ?: resp.message() return IOException("$op failed: HTTP ${resp.code()} $msg") diff --git a/domain-vault/src/test/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepositoryTest.kt b/domain-vault/src/test/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepositoryTest.kt index d86cce9..ceb080e 100644 --- a/domain-vault/src/test/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepositoryTest.kt +++ b/domain-vault/src/test/java/com/github/nullptroma/wallenc/domain/vault/network/yandexdisk/repository/YandexDiskRepositoryTest.kt @@ -33,6 +33,7 @@ class YandexDiskRepositoryTest { @Test fun diskInfoParsesResponse() = runBlocking { + repository.resetCloudApiCallCount() server.enqueue( MockResponse() .setResponseCode(200) @@ -42,6 +43,7 @@ class YandexDiskRepositoryTest { val info = repository.diskInfo() assertEquals(1000L, info.totalSpace) assertEquals(200L, info.usedSpace) + assertEquals(1L, repository.cloudApiCallCount()) } @Test