notifications.kt (3479B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024 Taler Systems S.A. 4 * 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 * 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 * 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.common.db 21 22 import kotlinx.coroutines.* 23 import kotlinx.coroutines.flow.Flow 24 import kotlinx.coroutines.flow.MutableSharedFlow 25 import org.postgresql.ds.PGSimpleDataSource 26 import org.slf4j.Logger 27 import tech.libeufin.common.ExpoBackoffDecorr 28 import tech.libeufin.common.fmtLog 29 import java.io.Closeable 30 import java.util.concurrent.ConcurrentHashMap 31 32 // SharedFlow that are manually counted for manual garbage collection 33 class CountedSharedFlow<T> { 34 val flow: MutableSharedFlow<T> = MutableSharedFlow() 35 var count: Int = 0 36 } 37 38 fun watchNotifications( 39 pgSource: PGSimpleDataSource, 40 schema: String, 41 logger: Logger, 42 listeners: Map<String, (suspend (String) -> Unit)> 43 ): Closeable { 44 val backoff = ExpoBackoffDecorr() 45 // JDBC notification reads block, so keep them off the caller's dispatcher. 46 val job = CoroutineScope(Dispatchers.IO).launch { 47 while (isActive) { 48 try { 49 pgSource.pgConnection(schema).use { conn -> 50 51 // Listen to all notifications channels 52 for (channel in listeners.keys) { 53 conn.execSQLUpdate("LISTEN $channel") 54 } 55 56 backoff.reset() 57 58 while (isActive) { 59 // Bound the blocking read so close() can cancel an idle watcher. 60 conn.getNotifications(1000) 61 .forEach { 62 // Dispatch 63 try { 64 listeners[it.name]!!(it.parameter) 65 } catch (e: Exception) { 66 ensureActive() 67 throw Exception("channel ${it.name} with input '${it.parameter}'", e) 68 } 69 } 70 } 71 } 72 } catch (e: Exception) { 73 ensureActive() 74 e.fmtLog(logger) 75 delay(backoff.next()) 76 } 77 } 78 } 79 return Closeable { runBlocking { job.cancelAndJoin() } } 80 } 81 82 /** Listen to flow from [map] for [key] using [lambda]*/ 83 suspend fun <R, K, V> listen(map: ConcurrentHashMap<K, CountedSharedFlow<V>>, key: K, lambda: suspend (Flow<V>) -> R): R { 84 // Register listener, create a new flow if missing 85 val flow = map.compute(key) { _, v -> 86 val tmp = v ?: CountedSharedFlow() 87 tmp.count++ 88 tmp 89 }!!.flow 90 91 try { 92 return lambda(flow) 93 } finally { 94 // Unregister listener, removing unused flow 95 map.compute(key) { _, v -> 96 v!! 97 v.count-- 98 if (v.count > 0) v else null 99 } 100 } 101 }