Sync : clean a bit by introducing 2 moshi adapters

This commit is contained in:
ganfra 2021-02-04 14:01:21 +01:00 committed by Benoit Marty
parent 8e2161bd9e
commit 3633199e68
7 changed files with 163 additions and 141 deletions

View file

@ -13,6 +13,7 @@ buildscript {
} }
} }
android { android {
compileSdkVersion 30 compileSdkVersion 30
testOptions.unitTests.includeAndroidResources = true testOptions.unitTests.includeAndroidResources = true
@ -88,6 +89,7 @@ android {
java.srcDirs += "src/sharedTest/java" java.srcDirs += "src/sharedTest/java"
} }
} }
} }
static def gitRevision() { static def gitRevision() {

View file

@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.network.parsing.ForceToBooleanJsonAdapter
import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory import org.matrix.android.sdk.internal.network.parsing.RuntimeJsonAdapterFactory
import org.matrix.android.sdk.internal.network.parsing.TlsVersionMoshiAdapter import org.matrix.android.sdk.internal.network.parsing.TlsVersionMoshiAdapter
import org.matrix.android.sdk.internal.network.parsing.UriMoshiAdapter import org.matrix.android.sdk.internal.network.parsing.UriMoshiAdapter
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncJsonAdapter import org.matrix.android.sdk.internal.session.sync.parsing.DefaultLazyRoomSyncJsonAdapter
object MoshiProvider { object MoshiProvider {
@ -45,7 +45,8 @@ object MoshiProvider {
.add(ForceToBooleanJsonAdapter()) .add(ForceToBooleanJsonAdapter())
.add(CipherSuiteMoshiAdapter()) .add(CipherSuiteMoshiAdapter())
.add(TlsVersionMoshiAdapter()) .add(TlsVersionMoshiAdapter())
.add(LazyRoomSyncJsonAdapter()) // Use addLast here so we can inject a SplitLazyRoomSyncJsonAdapter later to override the default parsing.
.addLast(DefaultLazyRoomSyncJsonAdapter())
.add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java)
.registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT)
.registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE)

View file

@ -17,10 +17,7 @@
package org.matrix.android.sdk.internal.session.sync package org.matrix.android.sdk.internal.session.sync
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okio.buffer
import okio.source
import org.matrix.android.sdk.R import org.matrix.android.sdk.R
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@ -32,8 +29,8 @@ import org.matrix.android.sdk.internal.session.filter.FilterRepository
import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask import org.matrix.android.sdk.internal.session.homeserver.GetHomeServerCapabilitiesTask
import org.matrix.android.sdk.internal.session.reportSubtask import org.matrix.android.sdk.internal.session.reportSubtask
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSync import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSync
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSyncJsonAdapter
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import org.matrix.android.sdk.internal.session.sync.parsing.InitialSyncResponseParser
import org.matrix.android.sdk.internal.session.user.UserStore import org.matrix.android.sdk.internal.session.user.UserStore
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.logDuration import org.matrix.android.sdk.internal.util.logDuration
@ -64,7 +61,8 @@ internal class DefaultSyncTask @Inject constructor(
private val syncTaskSequencer: SyncTaskSequencer, private val syncTaskSequencer: SyncTaskSequencer,
private val globalErrorReceiver: GlobalErrorReceiver, private val globalErrorReceiver: GlobalErrorReceiver,
@SessionFilesDirectory @SessionFilesDirectory
private val fileDirectory: File private val fileDirectory: File,
private val syncResponseParser: InitialSyncResponseParser
) : SyncTask { ) : SyncTask {
private val workingDir = File(fileDirectory, "is") private val workingDir = File(fileDirectory, "is")
@ -101,9 +99,10 @@ internal class DefaultSyncTask @Inject constructor(
val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT) val readTimeOut = (params.timeout + TIMEOUT_MARGIN).coerceAtLeast(TimeOutInterceptor.DEFAULT_LONG_TIMEOUT)
if (isInitialSync) { if (isInitialSync) {
logDuration("INIT_SYNC strategy: $initialSyncStrategy") { val initSyncStrategy = initialSyncStrategy
if (initialSyncStrategy is InitialSyncStrategy.Optimized) { logDuration("INIT_SYNC strategy: $initSyncStrategy") {
safeInitialSync(requestParams) if (initSyncStrategy is InitialSyncStrategy.Optimized) {
safeInitialSync(requestParams, initSyncStrategy)
} else { } else {
val syncResponse = logDuration("INIT_SYNC Request") { val syncResponse = logDuration("INIT_SYNC Request") {
executeRequest<SyncResponse>(globalErrorReceiver) { executeRequest<SyncResponse>(globalErrorReceiver) {
@ -132,7 +131,7 @@ internal class DefaultSyncTask @Inject constructor(
Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}") Timber.v("Sync task finished on Thread: ${Thread.currentThread().name}")
} }
private suspend fun safeInitialSync(requestParams: Map<String, String>) { private suspend fun safeInitialSync(requestParams: Map<String, String>, initSyncStrategy: InitialSyncStrategy.Optimized) {
workingDir.mkdirs() workingDir.mkdirs()
val workingFile = File(workingDir, "initSync.json") val workingFile = File(workingDir, "initSync.json")
val status = initialSyncStatusRepository.getStep() val status = initialSyncStatusRepository.getStep()
@ -163,7 +162,7 @@ internal class DefaultSyncTask @Inject constructor(
} }
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADED) initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_DOWNLOADED)
} }
handleSyncFile(workingFile) handleSyncFile(workingFile, initSyncStrategy)
// Delete all files // Delete all files
workingDir.deleteRecursively() workingDir.deleteRecursively()
@ -188,35 +187,12 @@ internal class DefaultSyncTask @Inject constructor(
} }
} }
private suspend fun handleSyncFile(workingFile: File) { private suspend fun handleSyncFile(workingFile: File, initSyncStrategy: InitialSyncStrategy.Optimized) {
val syncResponseLength = workingFile.length().toInt() logDuration("INIT_SYNC handleSyncFile()") {
logDuration("INIT_SYNC handleSyncFile() file size $syncResponseLength bytes") {
if (syncResponseLength < (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.minSizeToSplit ?: Long.MAX_VALUE) {
// OK, no need to split just handle as a regular sync response
Timber.v("INIT_SYNC no need to split")
handleInitialSyncFile(workingFile)
} else {
Timber.v("INIT_SYNC Split into several smaller files")
// Set file mode
// TODO This is really ugly, I should improve that
LazyRoomSyncJsonAdapter.initWith(workingFile)
handleInitialSyncFile(workingFile)
// Reset file mode
LazyRoomSyncJsonAdapter.reset()
}
}
}
private suspend fun handleInitialSyncFile(workingFile: File) {
val syncResponse = logDuration("INIT_SYNC Read file and parse") { val syncResponse = logDuration("INIT_SYNC Read file and parse") {
MoshiProvider.providesMoshi().adapter(SyncResponse::class.java) syncResponseParser.parse(initSyncStrategy, workingFile)
.fromJson(workingFile.source().buffer())!!
} }
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_PARSED) initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_PARSED)
// Log some stats // Log some stats
val nbOfJoinedRooms = syncResponse.rooms?.join?.size ?: 0 val nbOfJoinedRooms = syncResponse.rooms?.join?.size ?: 0
val nbOfJoinedRoomsInFile = syncResponse.rooms?.join?.values?.count { it is LazyRoomSync.Stored } val nbOfJoinedRoomsInFile = syncResponse.rooms?.join?.values?.count { it is LazyRoomSync.Stored }
@ -227,6 +203,7 @@ internal class DefaultSyncTask @Inject constructor(
} }
initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS) initialSyncStatusRepository.setStep(InitialSyncStatus.STEP_SUCCESS)
} }
}
companion object { companion object {
private const val MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT = 50 private const val MAX_NUMBER_OF_RETRY_AFTER_TIMEOUT = 50

View file

@ -16,17 +16,17 @@
package org.matrix.android.sdk.internal.session.sync.model package org.matrix.android.sdk.internal.session.sync.model
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader
import okio.buffer import okio.buffer
import okio.source import okio.source
import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.File import java.io.File
@JsonClass(generateAdapter = false) @JsonClass(generateAdapter = false)
internal sealed class LazyRoomSync { internal sealed class LazyRoomSync {
data class Parsed(val _roomSync: RoomSync) : LazyRoomSync() data class Parsed(val _roomSync: RoomSync) : LazyRoomSync()
data class Stored(val file: File) : LazyRoomSync() data class Stored(val roomSyncAdapter: JsonAdapter<RoomSync>, val file: File) : LazyRoomSync()
val roomSync: RoomSync val roomSync: RoomSync
get() { get() {
@ -35,8 +35,7 @@ internal sealed class LazyRoomSync {
is Stored -> { is Stored -> {
// Parse the file now // Parse the file now
file.inputStream().use { pos -> file.inputStream().use { pos ->
MoshiProvider.providesMoshi().adapter(RoomSync::class.java) roomSyncAdapter.fromJson(JsonReader.of(pos.source().buffer()))!!
.fromJson(JsonReader.of(pos.source().buffer()))!!
} }
} }
} }

View file

@ -1,90 +0,0 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.sync.model
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.initialSyncStrategy
import timber.log.Timber
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
internal class LazyRoomSyncJsonAdapter : JsonAdapter<LazyRoomSync>() {
@FromJson
override fun fromJson(reader: JsonReader): LazyRoomSync {
return if (workingDirectory != null) {
val path = reader.path
// val roomId = reader.path.substringAfter("\$.rooms.join.")
// inputStream.available() return 0... So read it to a String then decide to store in a file or to parse it now
val json = reader.nextSource().inputStream().bufferedReader().use {
it.readText()
}
val limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.minSizeToStoreInFile ?: Long.MAX_VALUE
if (json.length > limit) {
Timber.v("INIT_SYNC $path content length: ${json.length} copy to a file")
// Copy the source to a file
val file = createFile()
file.writeText(json)
LazyRoomSync.Stored(file)
} else {
Timber.v("INIT_SYNC $path content length: ${json.length} parse it now")
// Parse it now
val roomSync = MoshiProvider.providesMoshi().adapter(RoomSync::class.java).fromJson(json)!!
LazyRoomSync.Parsed(roomSync)
}
} else {
// Parse it now
val roomSync = MoshiProvider.providesMoshi().adapter(RoomSync::class.java).fromJson(reader)!!
LazyRoomSync.Parsed(roomSync)
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: LazyRoomSync?) {
// This Adapter is not supposed to serialize object
throw UnsupportedOperationException()
}
companion object {
fun initWith(file: File) {
workingDirectory = file.parentFile
atomicInteger.set(0)
}
fun reset() {
workingDirectory = null
}
private fun createFile(): File {
val parent = workingDirectory ?: error("workingDirectory is not initialized")
val index = atomicInteger.getAndIncrement()
return File(parent, "room_$index.json")
}
private var workingDirectory: File? = null
private val atomicInteger = AtomicInteger(0)
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.sync.parsing
import com.squareup.moshi.Moshi
import okio.buffer
import okio.source
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
import timber.log.Timber
import java.io.File
import javax.inject.Inject
internal class InitialSyncResponseParser @Inject constructor(private val moshi: Moshi) {
fun parse(syncStrategy: InitialSyncStrategy.Optimized, workingFile: File): SyncResponse {
val syncResponseLength = workingFile.length().toInt()
Timber.v("Sync file size is $syncResponseLength bytes")
val shouldSplit = syncResponseLength >= syncStrategy.minSizeToSplit
Timber.v("INIT_SYNC should split in several files: $shouldSplit")
return getMoshi(syncStrategy, workingFile, shouldSplit)
.adapter(SyncResponse::class.java)
.fromJson(workingFile.source().buffer())!!
}
private fun getMoshi(syncStrategy: InitialSyncStrategy.Optimized, workingFile: File, shouldSplit: Boolean): Moshi {
// If we don't have to split we'll rely on the already default moshi
if (!shouldSplit) return moshi
// Otherwise, we create a new adapter for handling Map of Lazy sync
return moshi.newBuilder()
.add(SplitLazyRoomSyncJsonAdapter(workingFile, syncStrategy))
.build()
}
}

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.sync.parsing
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
import org.matrix.android.sdk.internal.session.sync.model.LazyRoomSync
import org.matrix.android.sdk.internal.session.sync.model.RoomSync
import timber.log.Timber
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
internal class DefaultLazyRoomSyncJsonAdapter() {
@FromJson
fun fromJson(reader: JsonReader, delegate: JsonAdapter<RoomSync>): LazyRoomSync? {
val roomSync = delegate.fromJson(reader) ?: return null
return LazyRoomSync.Parsed(roomSync)
}
@ToJson
fun toJson(writer: JsonWriter, value: LazyRoomSync?) {
// This Adapter is not supposed to serialize object
Timber.v("To json $value with $writer")
throw UnsupportedOperationException()
}
}
internal class SplitLazyRoomSyncJsonAdapter(
private val workingDirectory: File,
private val syncStrategy: InitialSyncStrategy.Optimized
) {
private val atomicInteger = AtomicInteger(0)
private fun createFile(): File {
val index = atomicInteger.getAndIncrement()
return File(workingDirectory.parentFile, "room_$index.json")
}
@FromJson
fun fromJson(reader: JsonReader, delegate: JsonAdapter<RoomSync>): LazyRoomSync? {
val path = reader.path
val json = reader.nextSource().inputStream().bufferedReader().use {
it.readText()
}
val limit = syncStrategy.minSizeToStoreInFile
return if (json.length > limit) {
Timber.v("INIT_SYNC $path content length: ${json.length} copy to a file")
// Copy the source to a file
val file = createFile()
file.writeText(json)
LazyRoomSync.Stored(delegate, file)
} else {
Timber.v("INIT_SYNC $path content length: ${json.length} parse it now")
val roomSync = delegate.fromJson(json) ?: return null
LazyRoomSync.Parsed(roomSync)
}
}
@ToJson
fun toJson(writer: JsonWriter, value: LazyRoomSync?) {
// This Adapter is not supposed to serialize object
Timber.v("To json $value with $writer")
throw UnsupportedOperationException()
}
}