From 658f7de30e9b3564727a37d89087f653b278a1cd Mon Sep 17 00:00:00 2001 From: AL-Session <160798022+AL-Session@users.noreply.github.com> Date: Wed, 22 May 2024 08:43:47 +1000 Subject: [PATCH] [SES-1930] Catch HTTP exceptions from threads (#1491) * Catch HTTP exceptions * Fixes #1490 * Removed catch blocks that won't actually catch due to thread execution pool reasons & added a thread limiting mechanism to prevent excessive thread creation (when the queue is full then further tasks are queued) * Corrected thread exception catching (hopefully) * Addressed PR feedback * Reverted build number bump used for testing without reinstall * Added print of stack trace to any caught thread exceptions * Log exception directly and do not print stack trace on thread exception * Added TAG for logging output --------- Co-authored-by: alansley --- .../libsignal/utilities/ThreadUtils.kt | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt b/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt index ac81564bd1..e920d85b47 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/ThreadUtils.kt @@ -1,23 +1,60 @@ package org.session.libsignal.utilities import android.os.Process -import java.util.concurrent.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.SynchronousQueue +import java.util.concurrent.ThreadPoolExecutor +import java.util.concurrent.TimeUnit object ThreadUtils { + const val TAG = "ThreadUtils" + const val PRIORITY_IMPORTANT_BACKGROUND_THREAD = Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE - val executorPool: ExecutorService = Executors.newCachedThreadPool() + // Paraphrased from: https://www.baeldung.com/kotlin/create-thread-pool + // "A cached thread pool such as one created via: + // `val executorPool: ExecutorService = Executors.newCachedThreadPool()` + // will utilize resources according to the requirements of submitted tasks. It will try to reuse + // existing threads for submitted tasks but will create as many threads as it needs if new tasks + // keep pouring in (with a memory usage of at least 1MB per created thread). These threads will + // live for up to 60 seconds of idle time before terminating by default. As such, it presents a + // very sharp tool that doesn't include any backpressure mechanism - and a sudden peak in load + // can bring the system down with an OutOfMemory error. We can achieve a similar effect but with + // better control by creating a ThreadPoolExecutor manually." + + private val corePoolSize = Runtime.getRuntime().availableProcessors() // Default thread pool size is our CPU core count + private val maxPoolSize = corePoolSize * 4 // Allow a maximum pool size of up to 4 threads per core + private val keepAliveTimeSecs = 100L // How long to keep idle threads in the pool before they are terminated + private val workQueue = SynchronousQueue() + val executorPool: ExecutorService = ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTimeSecs, TimeUnit.SECONDS, workQueue) + + // Note: To see how many threads are running in our app at any given time we can use: + // val threadCount = getAllStackTraces().size @JvmStatic fun queue(target: Runnable) { - executorPool.execute(target) + executorPool.execute { + try { + target.run() + } catch (e: Exception) { + Log.e(TAG, e) + } + } } fun queue(target: () -> Unit) { - executorPool.execute(target) + executorPool.execute { + try { + target() + } catch (e: Exception) { + Log.e(TAG, e) + } + } } + // Thread executor used by the audio recorder only @JvmStatic fun newDynamicSingleThreadedExecutor(): ExecutorService { val executor = ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, LinkedBlockingQueue())