From ac802508462301af0649414c7faf49f60d7c7e91 Mon Sep 17 00:00:00 2001 From: William Walker <wnwalker@ucsc.edu> Date: Tue, 17 Dec 2024 19:57:23 -0800 Subject: [PATCH] Request Logic & Logging -Improve readability of request logic; move requests off separate dispatcher since they are already executed on it inside the HttpClient -Simplify library dispatcher initialization by using default constructor instead of helper fun() -Add DSL for interacting with Kermit Logger builder interface courtesy of https://github.com/psh/KermitExt/blob/060cd7fab8a73af4187498af8ed169fa4624bf5a/kermit-config/src/commonMain/kotlin/com/gatebuzz/kermit/ext/Kermit.kt --- gradle/libs.versions.toml | 2 +- .../edu/ucsc/its/temerity/core/Temerity.kt | 110 +++++++++++------- .../extensions/coroutines/CoroutinesExt.kt | 3 +- .../its/temerity/extensions/log/Kermit.kt | 87 ++++++++++++++ 4 files changed, 156 insertions(+), 46 deletions(-) create mode 100644 temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c272be..e58ee35 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,7 +59,7 @@ alertKmp = "1.0.7" filekit = "0.8.7" composetray = "0.4.0" -dokka = "2.0.0-Beta" +dokka = "2.0.0" spotless = "7.0.0.BETA4" gitSemVer = "3.1.7" conventionalCommits = "1.0.12" diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt index fdb62b1..56a3eb3 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt @@ -241,9 +241,7 @@ public class Temerity internal constructor( else -> Unit } } - val apiResponsePayload = createJobScope(webRequestDispatcher).run { - response.getOrThrow() - } + val apiResponsePayload = response.getOrThrow() if (apiResponsePayload == "[]") throw BreakException(page - 1) // Handle deserialization exceptions. Queue up decode: @@ -271,12 +269,14 @@ public class Temerity internal constructor( public override suspend fun getUsers(): List<User> = withContext(libraryCoroutineDispatcher) { val userRequest = platformApi.getUsers() - decodeResponseCatching(userRequest.executeApiResponse<String>()) + val response = userRequest.executeApiResponse<String>() + decodeResponseCatching(response) } public override suspend fun getUser(userId: Long): User = withContext(libraryCoroutineDispatcher) { - getUsers().first { it.userId == userId } + val returnedUsers = getUsers() + returnedUsers.first { it.userId == userId } } public override suspend fun createUser(primaryIdentifier: String, newUser: NewUser): HttpResponse = @@ -285,24 +285,36 @@ public class Temerity internal constructor( if (validationMessage != "Valid") { error(validationMessage) } else { - platformApi.createUser(primaryIdentifier, json.encodeToString(newUser)).getOrThrow() + val serializedNewUser = createJobScope(jsonProcessingDispatcher).run { + json.encodeToString(newUser) + } + val request = platformApi.createUser(primaryIdentifier, serializedNewUser) + request.getOrThrow() } } public override suspend fun updateUser(userId: Long, userUpdate: UserUpdate): HttpResponse = withContext(libraryCoroutineDispatcher) { - platformApi.setUser(userId, json.encodeToString(userUpdate)).getOrThrow() + val serializedUpdatedUser = createJobScope(jsonProcessingDispatcher).run { + json.encodeToString(userUpdate) + } + val request = platformApi.setUser(userId, serializedUpdatedUser) + request.getOrThrow() } public override suspend fun deleteUser(userId: Long): HttpResponse = withContext(libraryCoroutineDispatcher) { - platformApi.deleteUser(userId).getOrThrow() + val request = platformApi.deleteUser(userId) + request.getOrThrow() } public override suspend fun refreshCachedUserRoles(): List<String> = withContext(libraryCoroutineDispatcher) { - val returnedUsersResponse = platformApi.getUsers().executeApiResponse<String>().getOrThrow() - val returnedUserList = json.decodeFromString<List<User>>(returnedUsersResponse) + val usersRequest = platformApi.getUsers() + val returnedUsersResponse = usersRequest.executeApiResponse<String>().getOrThrow() + val returnedUserList = createJobScope(jsonProcessingDispatcher).run { + json.decodeFromString<List<User>>(returnedUsersResponse) + } val roleTypes = returnedUserList.map { it.userType }.distinct() cachedUserRoleList.clear() roleTypes.forEach { @@ -324,11 +336,15 @@ public class Temerity internal constructor( public override suspend fun getUserGroups(userId: Long): List<UserGroup> = withContext(libraryCoroutineDispatcher) { - decodeResponseCatching(platformApi.getUserGroups(userId).executeApiResponse<String>()) + val request = platformApi.getUserGroups(userId) + val response = request.executeApiResponse<String>() + decodeResponseCatching(response) } public override suspend fun getUserGroupsOwned(userId: Long): List<UserGroup> = withContext(libraryCoroutineDispatcher) { - decodeResponseCatching<List<UserGroup>>(platformApi.getUserGroupsOwned(userId).executeApiResponse()) + val request = platformApi.getUserGroupsOwned(userId) + val response = request.executeApiResponse<String>() + decodeResponseCatching<List<UserGroup>>(response) } public override suspend fun createUserWithExternalId(primaryIdentifier: String, newUser: NewUser): HttpResponse { @@ -353,10 +369,10 @@ public class Temerity internal constructor( val returnedGroups = ArrayList<Group>() try { for (x in (0..Int.MAX_VALUE)) { - val thisPage = platformApi.getGroupPage(x, true) - val thisPageGroups: List<Group> = decodeResponseCatching(thisPage.executeApiResponse<String>(), page = x) + val thisPageRequest = platformApi.getGroupPage(x, true) + val response = thisPageRequest.executeApiResponse<String>() + val thisPageGroups: List<Group> = decodeResponseCatching(response, page = x) returnedGroups.addAll(thisPageGroups) - delay(750) } } catch (e: Exception) { when (e) { @@ -474,8 +490,11 @@ public class Temerity internal constructor( entryOffset = x * 75, eventType = eventType.value, ) + val response = createJobScope(webRequestDispatcher).run { + endpointRequest.executeApiResponse<String>() + } val logEventEntries: List<AuditLogEntry> = - decodeResponseCatching(endpointRequest.executeApiResponse<String>(), page = x) + decodeResponseCatching(response, page = x) returnedEntries.addAll(logEventEntries) } } catch (e: Exception) { @@ -505,26 +524,31 @@ public class Temerity internal constructor( } } - private fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> = - when (sortOrder) { - null -> { - // Currently reads from prefs, falling back to BuildConfig constant. Can be overridden by passing a sort order as a parameter. - // TODO: Implement a way to pass a clientDefaultSortOrder to Builder - // TODO: Persist user-defined default sort order for subsequent runs - apply { sortByCreationDate(NEW_FIRST) } + private suspend fun ArrayList<AuditLogEntry>.applyOrDefault(sortOrder: AuditLogSortOrder?): ArrayList<AuditLogEntry> = withContext(libraryCoroutineDispatcher) { + with(this@applyOrDefault) { + when (sortOrder) { + null -> { + // Currently reads from prefs, falling back to BuildConfig constant. Can be overridden by passing a sort order as a parameter. + // TODO: Implement a way to pass a clientDefaultSortOrder to Builder + // TODO: Persist user-defined default sort order for subsequent runs + apply { sortByCreationDate(NEW_FIRST) } + } + + else -> apply { sortByCreationDate(sortOrder) } } - - else -> apply { sortByCreationDate(sortOrder) } } - - // TODO: Implement setUser() function + } public override suspend fun getDevices(): List<Device> = withContext(libraryCoroutineDispatcher) { - decodeResponseCatching(platformApi.getDevices().executeApiResponse<String>()) + val request = platformApi.getDevices() + val response = request.executeApiResponse<String>() + decodeResponseCatching(response) } public override suspend fun getDevice(deviceId: Long): Device = withContext(libraryCoroutineDispatcher) { - decodeResponseCatching(platformApi.getDeviceById(deviceId).executeApiResponse<String>()) + val request = platformApi.getDeviceById(deviceId) + val response = request.executeApiResponse<String>() + decodeResponseCatching(response) } public override suspend fun getUserSessions( @@ -532,13 +556,13 @@ public class Temerity internal constructor( startTime: LocalDate, endTime: LocalDate, ): List<UserRecordingSession> = withContext(libraryCoroutineDispatcher) { - decodeResponseCatching( - platformApi.getUserSessions( - userId, - startTime.applyScheduledSessionDateFormat(), - endTime.applyScheduledSessionDateFormat(), - ).executeApiResponse<String>(), + val request = platformApi.getUserSessions( + userId, + startTime.applyScheduledSessionDateFormat(), + endTime.applyScheduledSessionDateFormat(), ) + val response = request.executeApiResponse<String>() + decodeResponseCatching(response) } public override suspend fun getDeviceSchedule( @@ -546,20 +570,18 @@ public class Temerity internal constructor( startTime: LocalDate, endTime: LocalDate, ): List<DeviceRecordingSession> = withContext(libraryCoroutineDispatcher) { - decodeResponseCatching( - platformApi.getDeviceSchedule( - deviceId, - startTime.applyScheduledSessionDateFormat(), - endTime.applyScheduledSessionDateFormat(), - ).executeApiResponse<String>(), + val request = platformApi.getDeviceSchedule( + deviceId, + startTime.applyScheduledSessionDateFormat(), + endTime.applyScheduledSessionDateFormat(), ) + val response = request.executeApiResponse<String>() + decodeResponseCatching(response) } public override suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray = withContext(libraryCoroutineDispatcher) { val apiRequest = platformApi.getStorageAnalyticsReport(groupId) - val responseBytes = createJobScope(webRequestDispatcher).run { - apiRequest.executeApiResponse<ByteReadChannel>().getOrThrow() - } + val responseBytes = apiRequest.executeApiResponse<ByteReadChannel>().getOrThrow() withContext(fileProcessDispatcher) { responseBytes.toByteArray(limit = 2000000000) } // Limit file downloads to 2 GB diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt index d9790ad..402405c 100644 --- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/coroutines/CoroutinesExt.kt @@ -18,6 +18,7 @@ package edu.ucsc.its.temerity.extensions.coroutines import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT +import io.ktor.util.SilentSupervisor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -29,7 +30,7 @@ internal fun createJobScope(coroutineContext: CoroutineContext, allowIndependent return CoroutineScope(coroutineContext + parentJob) } -internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = createJobScope(dispatcher, allowIndependentFailure = true) +internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = CoroutineScope(dispatcher + SilentSupervisor()) internal fun availableThreads(maximumThreadCount: Int? = null): Int { val availableThreadCount = Runtime.getRuntime().availableProcessors().plus(1) // +1 to maintain full utilization in case one coroutine blocks. Never less than 2 (DEFAULT_MINIMUM_THREAD_COUNT). See: https://github.com/Kotlin/kotlinx.coroutines/issues/261 diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt new file mode 100644 index 0000000..fe4e986 --- /dev/null +++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/extensions/log/Kermit.kt @@ -0,0 +1,87 @@ +/* + * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu) + * Copyright 2022-2024 The Regents of the University of California. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package edu.ucsc.its.temerity.extensions.log + +import co.touchlab.kermit.LogWriter +import co.touchlab.kermit.Logger +import co.touchlab.kermit.Severity +import co.touchlab.kermit.StaticConfig + +/* + * An overload for the Kermit logger that allows for more concise and readable configuration code. + * Adapted from: https://github.com/psh/KermitExt/blob/060cd7fab8a73af4187498af8ed169fa4624bf5a/kermit-config/src/commonMain/kotlin/com/gatebuzz/kermit/ext/Kermit.kt + */ +internal class Kermit { + + companion object { + @JvmStatic + fun builder(): Builder = LoggerBuilder() + + operator fun invoke(block: Builder.() -> Unit): Logger = with(LoggerBuilder()) { + block(this) + build() + } + } + + interface Builder { + fun tag(tag: String): Builder + fun minSeverity(severity: Severity): Builder + fun setLogWriters(vararg logWriter: LogWriter): Builder + fun addLogWriter(vararg logWriter: LogWriter): Builder + operator fun LogWriter.unaryPlus() + fun build(): Logger + } + + internal class LoggerBuilder : Builder { + private var tag: String = "" + private var logWriters: MutableList<LogWriter> = mutableListOf() + private var minSeverity: Severity = Severity.Verbose + + override fun tag(tag: String): Builder { + this.tag = tag + return this + } + + override fun minSeverity(severity: Severity): Builder { + this.minSeverity = severity + return this + } + + override fun setLogWriters(vararg logWriter: LogWriter): Builder { + logWriters = logWriter.toMutableList() + return this + } + + override operator fun LogWriter.unaryPlus() { + logWriters.add(this) + } + + override fun addLogWriter(vararg logWriter: LogWriter): Builder { + logWriters.addAll(logWriter) + return this + } + + override fun build(): Logger { + if (logWriters.isEmpty()) throw Exception("At least one log writer is needed") + + return Logger( + StaticConfig(minSeverity, logWriters.toList()), + ) + } + } +} -- GitLab