From 69289d184dee309fa83a1faa0e2a66c05b4a4a6d Mon Sep 17 00:00:00 2001
From: William Walker <wnwalker@ucsc.edu>
Date: Thu, 5 Dec 2024 08:40:29 -0800
Subject: [PATCH] Dispatcher coordination

-Create limited library dispatcher and assign to injected HTTP client engines
-Allow choosing main library dispatcher on a per-platform basis by adding dispatcher param to createDispatcher() utility fun
---
 .../kotlin/edu/ucsc/its/temerity/Actual.kt    | 12 +++++++-
 .../kotlin/edu/ucsc/its/temerity/Expect.kt    |  4 +--
 .../its/temerity/core/DispatcherFactory.kt    |  5 +++-
 .../edu/ucsc/its/temerity/core/Temerity.kt    | 29 ++++++++++++-------
 .../kotlin/edu/ucsc/its/temerity/Actual.kt    | 12 +++++++-
 5 files changed, 47 insertions(+), 15 deletions(-)

diff --git a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
index 5aac3ef..a15413e 100644
--- a/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
+++ b/temerity/src/androidMain/kotlin/edu/ucsc/its/temerity/Actual.kt
@@ -17,9 +17,19 @@
  */
 package edu.ucsc.its.temerity
 
+import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher
 import io.ktor.client.engine.android.Android
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
 import org.koin.dsl.module
 
 internal actual fun platformModule() = module {
-  single { Android.create() }
+  single { (threadCount: Int, dispatcherName: String) ->
+    createDispatcher(Dispatchers.IO, threadCount, dispatcherName)
+  }
+  single { (libraryCoroutineDispatcher: CoroutineDispatcher) ->
+    Android.create {
+      dispatcher = libraryCoroutineDispatcher
+    }
+  }
 }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
index 41bdf96..13146c7 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Expect.kt
@@ -20,8 +20,8 @@ package edu.ucsc.its.temerity
 import org.koin.core.module.Module
 
 /**
- * This function provides an expectation for the implementation of a platform-specific dependency module
+ * This function provides an implementation of a platform-specific dependency module
  * Implementations for each source set contain factories for injected platform-specific dependencies that the Temerity library relies on.
- * For now, this includes an HttpClientEngine for making requests, and a PreferencesSettings object for storing settings.
+ * For now, this includes an HttpClientEngine for making requests
  */
 internal expect fun platformModule(): Module
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
index 088ae9d..ba2536d 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
@@ -19,12 +19,15 @@ package edu.ucsc.its.temerity.core
 
 import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT
 import edu.ucsc.its.temerity.createJobScope
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 
 internal object DispatcherFactory {
 
-  internal fun createLibraryScope(threadCount: Int): CoroutineScope = createJobScope(Dispatchers.IO.limitedParallelism(threadCount), allowIndependentFailure = true)
+  internal fun createDispatcher(dispatcherThreadPool: CoroutineDispatcher = Dispatchers.IO, threadCount: Int, dispatcherName: String): CoroutineDispatcher = dispatcherThreadPool.limitedParallelism(threadCount, dispatcherName)
+
+  internal fun createLibraryScope(dispatcher: CoroutineDispatcher): CoroutineScope = createJobScope(dispatcher, allowIndependentFailure = true)
 
   private fun availableThreads() = Runtime.getRuntime().availableProcessors().minus(1)
 
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 d732782..935c401 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
@@ -37,6 +37,7 @@ import edu.ucsc.its.temerity.TemClientConfig
 import edu.ucsc.its.temerity.TemerityApi
 import edu.ucsc.its.temerity.api.PlatformApi
 import edu.ucsc.its.temerity.api.createPlatformApi
+import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher
 import edu.ucsc.its.temerity.core.DispatcherFactory.createLibraryScope
 import edu.ucsc.its.temerity.core.JsonFactory.buildJson
 import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_WEB_TIMEOUT
@@ -66,6 +67,7 @@ import io.ktor.client.plugins.logging.LogLevel
 import io.ktor.client.plugins.logging.Logging
 import io.ktor.client.request.header
 import io.ktor.client.statement.HttpResponse
+import io.ktor.client.utils.clientDispatcher
 import io.ktor.http.URLProtocol
 import io.ktor.util.cio.toByteArray
 import io.ktor.utils.io.ByteReadChannel
@@ -97,6 +99,8 @@ import org.koin.dsl.module
 import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.DurationUnit
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
 import co.touchlab.kermit.Logger as KermitLogger
 import io.ktor.client.plugins.logging.Logger as KtorLogger
 
@@ -118,7 +122,7 @@ public class Temerity internal constructor(
     // TODO: Use this as cache4k expiration time
     public val DEFAULT_CACHE_EXPIRATION: Duration = 15.minutes
     internal const val DEFAULT_MINIMUM_THREAD_COUNT: Int = 2
-    
+
     internal fun createLogger(tag: String?, supportKtxNotebook: Boolean = false): co.touchlab.kermit.Logger =
       co.touchlab.kermit.Logger(
         config = loggerConfigInit(
@@ -136,7 +140,7 @@ public class Temerity internal constructor(
   internal val libModule = module {
     factory { (config: TemClientConfig, kermit: co.touchlab.kermit.Logger) ->
       buildHttpClient(
-        httpClientEngine = get(),
+        httpClientEngine = get( parameters = { parametersOf(libraryCoroutineDispatcher) }),
         config = config,
         logger = kermit,
       )
@@ -150,8 +154,8 @@ public class Temerity internal constructor(
       }
       ktorfit.createPlatformApi()
     }
-    single { (threadCount: Int) ->
-      createLibraryScope(threadCount)
+    single { (dispatcher: CoroutineDispatcher) ->
+      createLibraryScope(dispatcher)
     }
   }
 
@@ -161,7 +165,7 @@ public class Temerity internal constructor(
    * via factories defined as part of the lib Module.
    */
   private fun createKoinApp() = object : TemerityKoinContext() {
-    val logger = createLogger("TemerityLib")
+    val logger = createLogger("TemerityLib Koin DI Context")
     override val koinApp = koinApplication {
       logger(object : org.koin.core.logger.Logger() {
         override fun display(level: Level, msg: MESSAGE) {
@@ -175,9 +179,9 @@ public class Temerity internal constructor(
         }
       })
       modules(
-        platformModule(),
         libModule,
-      )
+        platformModule()
+        )
     }
     override val koin: Koin = koinApp.koin
   }
@@ -196,6 +200,7 @@ public class Temerity internal constructor(
 
   private var platformApi: PlatformApi
   private var cachedUserRoleList: ConcurrentMutableMap<Int, String>
+  private var libraryCoroutineDispatcher: CoroutineDispatcher
   private var libraryCoroutineScope: CoroutineScope
 
   init {
@@ -207,12 +212,16 @@ public class Temerity internal constructor(
       "Service url must be provided. Set it using TemClientConfig.serviceUrl(url)"
     }
 
-    libraryCoroutineScope = get<CoroutineScope> {
+    libraryCoroutineDispatcher = get<CoroutineDispatcher> {
+      val dispatcherName = "Temerity Library Dispatcher"
       when (val optThreadCount = config.threadCount) {
-        null -> parametersOf(DispatcherFactory.calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT))
-        else -> parametersOf(DispatcherFactory.setThreadCount(optThreadCount))
+        null -> parametersOf(DispatcherFactory.calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT), dispatcherName)
+        else -> parametersOf(DispatcherFactory.setThreadCount(optThreadCount), dispatcherName)
       }
     }
+    libraryCoroutineScope = get<CoroutineScope> {
+      parametersOf(libraryCoroutineDispatcher)
+    }
 
     platformApi = get<PlatformApi> {
       parametersOf(config)
diff --git a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
index 8574a6a..cbc6a9d 100644
--- a/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
+++ b/temerity/src/jvmMain/kotlin/edu/ucsc/its/temerity/Actual.kt
@@ -17,9 +17,19 @@
  */
 package edu.ucsc.its.temerity
 
+import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher
 import io.ktor.client.engine.java.Java
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
 import org.koin.dsl.module
 
 internal actual fun platformModule() = module {
-  factory { Java.create() }
+  single { (threadCount: Int, dispatcherName: String) ->
+    createDispatcher(Dispatchers.IO, threadCount, dispatcherName)
+  }
+  factory { (libraryCoroutineDispatcher: CoroutineDispatcher) ->
+    Java.create {
+      dispatcher = libraryCoroutineDispatcher
+    }
+  }
 }
-- 
GitLab