Этап 1: единый OkHttpClient (переиспользование пула/кэша) — ч.1
Раньше NetworkFactory создавал новый OkHttpClient на КАЖДЫЙ запрос (~32 места): TLS-handshake + новый пул/диспатчер на запрос + утечка ExecutorService. Теперь: один базовый клиент (общий ConnectionPool/Dispatcher + диск-кэш 20МБ), авторизованные варианты через base.newBuilder() (шарят пул/кэш), кэшируются по кредам+таймаутам. Публичный API НЕ менялся — 32 вызова не тронуты. Init диск-кэша в F7MobileApp.onCreate. +3 unit-теста (переиспользование клиента, общий пул). 401→interceptor НЕ делал (меняет семантику исключений — отдельный шаг). Проверено: 15 тестов зелёные + assembleRelease BUILD SUCCESSFUL, APK 53 МБ.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск`
|
||||||
|
|
||||||
|
- 2026-07-07 | v0.5.114 (122) | Этап 1 (единый OkHttpClient, ч.1): NetworkFactory переписан — раньше создавал НОВЫЙ OkHttpClient на КАЖДЫЙ запрос (~32 места) → TLS-handshake + новый пул/диспатчер на запрос + утечка ExecutorService. Теперь один базовый клиент (общий ConnectionPool/Dispatcher + диск-кэш 20МБ), авторизованные варианты через base.newBuilder() (шарят пул/кэш), кэшируются по кредам+таймаутам. Публичный API НЕ менялся — 32 вызова не тронуты. Init диск-кэша — F7MobileApp.onCreate. +3 unit-теста (переиспользование клиента, общий пул). 401→interceptor НЕ делал (меняет семантику исключений — отдельный шаг) | контракты не менялись | средний: затрагивает ВСЕ сетевые вызовы (переиспользование соединений); поведение запросов то же, проверено 15 тестами + assembleRelease
|
||||||
- 2026-07-07 | v0.5.114 (122) | Этап 1 (CI + GPL-публикация): добавлен Gitea Actions workflow .gitea/workflows/ci.yml (push/PR → тесты+lint+assembleDebug; тег v* → подписанный release + архив исходников в Gitea Release) — Gitea 1.26.1 поддерживает Actions. Скрипт scripts/package-source.sh (GPL corresponding source §6: архив всего дерева вкл. vendor GPL/LICENSE/NOTICE — проверен, 6.7 МБ) закрывает п.4c, работает и вручную. docs/CI.md. НЕ активно до регистрации self-hosted раннера на инфре B (нужен Gitea-админ для токена — инфра-шаг) | процесс сборки/релиза | нет: конфиг CI, кода приложения не трогает
|
- 2026-07-07 | v0.5.114 (122) | Этап 1 (CI + GPL-публикация): добавлен Gitea Actions workflow .gitea/workflows/ci.yml (push/PR → тесты+lint+assembleDebug; тег v* → подписанный release + архив исходников в Gitea Release) — Gitea 1.26.1 поддерживает Actions. Скрипт scripts/package-source.sh (GPL corresponding source §6: архив всего дерева вкл. vendor GPL/LICENSE/NOTICE — проверен, 6.7 МБ) закрывает п.4c, работает и вручную. docs/CI.md. НЕ активно до регистрации self-hosted раннера на инфре B (нужен Gitea-админ для токена — инфра-шаг) | процесс сборки/релиза | нет: конфиг CI, кода приложения не трогает
|
||||||
- 2026-07-07 | v0.5.114 (122) | Этап 1 (баги данных, test-first): заведена тестовая инфраструктура (core:network testImplementation junit) + первые 12 unit-тестов. Починено: (1) ICS-парсер дат — суффикс `Z` теперь UTC (был локальным), учитывается TZID, floating→локаль; (2) parseProps сохраняет параметры свойств — несколько ATTENDEE больше НЕ схлопываются, CN/PARTSTAT/ROLE читаются (была порча участников при редактировании→PUT на сервер); (3) unescape single-pass (последовательные replace ломались на `\\n`); (4) CalDavClient.parseIcsInstant делегирует в CalendarIcs — устранён рассинхрон календарь↔задачи; (5) QR-логин: маркер-парсер вместо split('&') — пароль с `&`/`:` больше не теряется; (6) курсор пагинации почты Int→Long (обрезка Unix-времени, ломалось ~2038). Проверено: 12 тестов зелёные + assembleRelease | контракты не менялись; исправлена запись битых ATTENDEE на сервер | низкий: чинит порчу данных, покрыто тестами
|
- 2026-07-07 | v0.5.114 (122) | Этап 1 (баги данных, test-first): заведена тестовая инфраструктура (core:network testImplementation junit) + первые 12 unit-тестов. Починено: (1) ICS-парсер дат — суффикс `Z` теперь UTC (был локальным), учитывается TZID, floating→локаль; (2) parseProps сохраняет параметры свойств — несколько ATTENDEE больше НЕ схлопываются, CN/PARTSTAT/ROLE читаются (была порча участников при редактировании→PUT на сервер); (3) unescape single-pass (последовательные replace ломались на `\\n`); (4) CalDavClient.parseIcsInstant делегирует в CalendarIcs — устранён рассинхрон календарь↔задачи; (5) QR-логин: маркер-парсер вместо split('&') — пароль с `&`/`:` больше не теряется; (6) курсор пагинации почты Int→Long (обрезка Unix-времени, ломалось ~2038). Проверено: 12 тестов зелёные + assembleRelease | контракты не менялись; исправлена запись битых ATTENDEE на сервер | низкий: чинит порчу данных, покрыто тестами
|
||||||
- 2026-07-07 | v0.5.114 (122) | Этап 0 п.5 (gradle-гигиена): org.gradle.parallel=true (ускорение сборки 20 модулей); удалены мёртвые модули core:data, core:ui (пустые заготовки) и feature:widgets (не был в settings.gradle, не компилировался) + чистка settings.gradle. nonTransitiveRClass и убирание jetifier — ПРОБОВАЛ, откатил: vendor-форк talk-android ломается (R транзитивно + legacy android.support.* в его depS); причины в комментах gradle.properties. Проверено сборкой: assembleRelease BUILD SUCCESSFUL. **Этап 0 завершён** (кроме п.4c — публикация исходников, к CI этапа 1) | контракты не менялись | низкий: удалён неиспользуемый код, ускорена сборка
|
- 2026-07-07 | v0.5.114 (122) | Этап 0 п.5 (gradle-гигиена): org.gradle.parallel=true (ускорение сборки 20 модулей); удалены мёртвые модули core:data, core:ui (пустые заготовки) и feature:widgets (не был в settings.gradle, не компилировался) + чистка settings.gradle. nonTransitiveRClass и убирание jetifier — ПРОБОВАЛ, откатил: vendor-форк talk-android ломается (R транзитивно + legacy android.support.* в его depS); причины в комментах gradle.properties. Проверено сборкой: assembleRelease BUILD SUCCESSFUL. **Этап 0 завершён** (кроме п.4c — публикация исходников, к CI этапа 1) | контракты не менялись | низкий: удалён неиспользуемый код, ускорена сборка
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.forbion.f7cloud.core.auth.AuthStore
|
import ru.forbion.f7cloud.core.auth.AuthStore
|
||||||
import ru.forbion.f7cloud.core.designsystem.AppForegroundTracker
|
import ru.forbion.f7cloud.core.designsystem.AppForegroundTracker
|
||||||
|
import ru.forbion.f7cloud.core.network.NetworkFactory
|
||||||
import ru.forbion.f7cloud.core.push.F7NotificationChannels
|
import ru.forbion.f7cloud.core.push.F7NotificationChannels
|
||||||
import ru.forbion.f7cloud.core.push.F7PushRegistrar
|
import ru.forbion.f7cloud.core.push.F7PushRegistrar
|
||||||
import ru.forbion.f7cloud.feature.talknative.TalkVendorBootstrap
|
import ru.forbion.f7cloud.feature.talknative.TalkVendorBootstrap
|
||||||
@@ -37,6 +38,7 @@ class F7MobileApp : F7cloudTalkApplication(), ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
F7NotificationChannels.ensureAll(this)
|
F7NotificationChannels.ensureAll(this)
|
||||||
|
NetworkFactory.init(this) // общий HTTP-клиент + диск-кэш (переиспользование соединений)
|
||||||
TalkVendorBootstrap.onApplicationCreate(this)
|
TalkVendorBootstrap.onApplicationCreate(this)
|
||||||
val auth = AuthStore(this).load() ?: return
|
val auth = AuthStore(this).load() ?: return
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,9 +1,37 @@
|
|||||||
package ru.forbion.f7cloud.core.network
|
package ru.forbion.f7cloud.core.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фабрика HTTP-клиентов. Раньше создавала НОВЫЙ OkHttpClient на КАЖДЫЙ запрос (~32 места) →
|
||||||
|
* TLS-handshake и новый пул/диспатчер на запрос, утечка ExecutorService. Теперь:
|
||||||
|
* - один базовый клиент (общий ConnectionPool + Dispatcher + диск-кэш);
|
||||||
|
* - авторизованные варианты — через `base.newBuilder()` (шарят пул/диспатчер/кэш);
|
||||||
|
* - экземпляры кэшируются по кредам+таймаутам → переиспользуются, соединения живут.
|
||||||
|
*
|
||||||
|
* Публичный API НЕ менялся — все вызывающие (`newAuthedClient*`) работают как прежде.
|
||||||
|
* NB: 401→UnauthorizedException пока проверяется в репозиториях вручную; централизация
|
||||||
|
* через interceptor — отдельный шаг (меняет семантику исключений).
|
||||||
|
*/
|
||||||
object NetworkFactory {
|
object NetworkFactory {
|
||||||
|
@Volatile
|
||||||
|
private var base: OkHttpClient = OkHttpClient()
|
||||||
|
private val clients = ConcurrentHashMap<String, OkHttpClient>()
|
||||||
|
|
||||||
|
/** Вызывать один раз из Application.onCreate — добавляет диск-кэш (нужен cacheDir). */
|
||||||
|
fun init(context: Context) {
|
||||||
|
val cacheDir = File(context.applicationContext.cacheDir, "http-cache")
|
||||||
|
base = OkHttpClient.Builder()
|
||||||
|
.cache(Cache(cacheDir, 20L * 1024 * 1024)) // 20 МБ
|
||||||
|
.build()
|
||||||
|
clients.clear() // пересобрать производные клиенты уже с кэшем
|
||||||
|
}
|
||||||
|
|
||||||
fun newAuthedClient(
|
fun newAuthedClient(
|
||||||
username: String,
|
username: String,
|
||||||
appPassword: String,
|
appPassword: String,
|
||||||
@@ -11,13 +39,16 @@ object NetworkFactory {
|
|||||||
callTimeoutSeconds: Long = 30,
|
callTimeoutSeconds: Long = 30,
|
||||||
readTimeoutSeconds: Long = 30,
|
readTimeoutSeconds: Long = 30,
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
return OkHttpClient.Builder()
|
val key = "$username|$appPassword|$trustAllCerts|$callTimeoutSeconds|$readTimeoutSeconds"
|
||||||
.callTimeout(callTimeoutSeconds, TimeUnit.SECONDS)
|
return clients.getOrPut(key) {
|
||||||
.connectTimeout(20, TimeUnit.SECONDS)
|
base.newBuilder() // общий пул/диспатчер/кэш базового клиента
|
||||||
.readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
|
.callTimeout(callTimeoutSeconds, TimeUnit.SECONDS)
|
||||||
.applyUnsafeSslIfNeeded(trustAllCerts)
|
.connectTimeout(20, TimeUnit.SECONDS)
|
||||||
.addInterceptor(BasicAuthInterceptor(username, appPassword))
|
.readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
|
||||||
.build()
|
.applyUnsafeSslIfNeeded(trustAllCerts)
|
||||||
|
.addInterceptor(BasicAuthInterceptor(username, appPassword))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Collabora / richdocuments: cold start and WOPI can be slow on mobile networks. */
|
/** Collabora / richdocuments: cold start and WOPI can be slow on mobile networks. */
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package ru.forbion.f7cloud.core.network
|
||||||
|
|
||||||
|
import org.junit.Assert.assertNotSame
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/** Фиксирует переиспользование клиента и общий пул соединений (было: новый клиент на запрос). */
|
||||||
|
class NetworkFactoryTest {
|
||||||
|
|
||||||
|
@Test fun `одинаковые креды дают ТОТ ЖЕ экземпляр клиента`() {
|
||||||
|
val a = NetworkFactory.newAuthedClient("user", "pass", false)
|
||||||
|
val b = NetworkFactory.newAuthedClient("user", "pass", false)
|
||||||
|
assertSame("клиент должен переиспользоваться, а не создаваться заново", a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `клиенты шарят общий ConnectionPool и Dispatcher`() {
|
||||||
|
val a = NetworkFactory.newAuthedClient("u1", "p1", false)
|
||||||
|
val b = NetworkFactory.newAuthedClient("u2", "p2", false) // другие креды
|
||||||
|
assertSame(a.connectionPool, b.connectionPool)
|
||||||
|
assertSame(a.dispatcher, b.dispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `разные таймауты — разные клиенты, но общий пул`() {
|
||||||
|
val normal = NetworkFactory.newAuthedClient("u", "p", false)
|
||||||
|
val office = NetworkFactory.newAuthedClientForOffice("u", "p", false)
|
||||||
|
assertNotSame(normal, office)
|
||||||
|
assertSame(normal.connectionPool, office.connectionPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user