From e292fa357724df8695b6110eec6e4a60c7986363 Mon Sep 17 00:00:00 2001 From: Antoine A <> Date: Sat, 16 Mar 2024 02:23:06 +0100 Subject: Simplify error handling for microsecond overflows that never occur in practice --- .../kotlin/tech/libeufin/bank/db/AccountDAO.kt | 4 +- .../kotlin/tech/libeufin/bank/db/CashoutDAO.kt | 4 +- .../main/kotlin/tech/libeufin/bank/db/Database.kt | 23 +------- .../kotlin/tech/libeufin/bank/db/ExchangeDAO.kt | 4 +- .../main/kotlin/tech/libeufin/bank/db/TanDAO.kt | 10 ++-- .../main/kotlin/tech/libeufin/bank/db/TokenDAO.kt | 12 ++-- .../kotlin/tech/libeufin/bank/db/TransactionDAO.kt | 2 +- .../kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 4 +- bank/src/test/kotlin/StatsTest.kt | 2 +- common/src/main/kotlin/time.kt | 66 +++++++--------------- nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt | 4 +- .../kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt | 14 ++--- .../kotlin/tech/libeufin/nexus/db/PaymentDAO.kt | 12 ++-- 13 files changed, 52 insertions(+), 109 deletions(-) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt index 49ca49c0..7781d807 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -52,7 +52,7 @@ class AccountDAO(private val db: Database) { checkPaytoIdempotent: Boolean, ctx: BankPaytoCtx ): AccountCreationResult = db.serializable { it -> - val now = Instant.now().toDbMicros() ?: throw faultyTimestampByBank() + val now = Instant.now().micros() it.transaction { conn -> val idempotent = conn.prepareStatement(""" SELECT password_hash, name=? @@ -194,7 +194,7 @@ class AccountDAO(private val db: Database) { login: String, is2fa: Boolean ): AccountDeletionResult = db.serializable { conn -> - val now = Instant.now().toDbMicros() ?: throw faultyTimestampByBank() + val now = Instant.now().micros() val stmt = conn.prepareStatement(""" SELECT out_not_found, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt index 0f9d8f86..45da0854 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -66,7 +66,7 @@ class CashoutDAO(private val db: Database) { stmt.setLong(5, amountCredit.value) stmt.setInt(6, amountCredit.frac) stmt.setString(7, subject) - stmt.setLong(8, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(8, now.micros()) stmt.setBoolean(9, is2fa) stmt.executeQuery().use { when { @@ -117,7 +117,7 @@ class CashoutDAO(private val db: Database) { creation_time = it.getTalerTimestamp("creation_time"), confirmation_time = when (val timestamp = it.getLong("confirmation_date")) { 0L -> null - else -> TalerProtocolTimestamp(timestamp.microsToJavaInstant() ?: throw faultyTimestampByBank()) + else -> TalerProtocolTimestamp(timestamp.asInstant()) }, tan_channel = it.getString("tan_channel")?.run { TanChannel.valueOf(this) }, tan_info = it.getString("tan_info"), diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt index 1880b67b..19706a7b 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -35,25 +35,6 @@ import kotlin.math.abs private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-db") -/** - * This error occurs in case the timestamp took by the bank for some - * event could not be converted in microseconds. Note: timestamp are - * taken via the Instant.now(), then converted to nanos, and then divided - * by 1000 to obtain the micros. - * - * It could be that a legitimate timestamp overflows in the process of - * being converted to micros - as described above. In the case of a timestamp, - * the fault lies to the bank, because legitimate timestamps must (at the - * time of writing!) go through the conversion to micros. - * - * On the other hand (and for the sake of completeness), in the case of a - * timestamp that was calculated after a client-submitted duration, the overflow - * lies to the client, because they must have specified a gigantic amount of time - * that overflew the conversion to micros and should simply have specified "forever". - */ -internal fun faultyTimestampByBank() = internalServerError("Bank took overflowing timestamp") -internal fun faultyDurationByClient() = badRequest("Overflowing duration, please specify 'forever' instead.") - class Database(dbConfig: String, internal val bankCurrency: String, internal val fiatCurrency: String?): DbPool(dbConfig, "libeufin_bank") { internal val notifWatcher: NotificationWatcher = NotificationWatcher(pgSource) @@ -206,7 +187,5 @@ enum class AbortResult { } fun ResultSet.getTalerTimestamp(name: String): TalerProtocolTimestamp{ - return TalerProtocolTimestamp( - getLong(name).microsToJavaInstant() ?: throw faultyTimestampByBank() - ) + return TalerProtocolTimestamp(getLong(name).asInstant()) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt index 6540172a..69249f1a 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt @@ -130,7 +130,7 @@ class ExchangeDAO(private val db: Database) { stmt.setString(6, req.exchange_base_url.url) stmt.setString(7, req.credit_account.canonical) stmt.setString(8, login) - stmt.setLong(9, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(9, now.micros()) stmt.executeQuery().use { when { @@ -191,7 +191,7 @@ class ExchangeDAO(private val db: Database) { stmt.setInt(4, req.amount.frac) stmt.setString(5, req.debit_account.canonical) stmt.setString(6, login) - stmt.setLong(7, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(7, now.micros()) stmt.executeQuery().use { when { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt index 66771c93..c7328d65 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt @@ -23,7 +23,7 @@ import tech.libeufin.bank.Operation import tech.libeufin.bank.TanChannel import tech.libeufin.bank.internalServerError import tech.libeufin.common.oneOrNull -import tech.libeufin.common.toDbMicros +import tech.libeufin.common.micros import java.time.Duration import java.time.Instant import java.util.concurrent.TimeUnit @@ -46,7 +46,7 @@ class TanDAO(private val db: Database) { stmt.setString(1, body) stmt.setString(2, op.name) stmt.setString(3, code) - stmt.setLong(4, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(4, now.micros()) stmt.setLong(5, TimeUnit.MICROSECONDS.convert(validityPeriod)) stmt.setInt(6, retryCounter) stmt.setString(7, login) @@ -76,7 +76,7 @@ class TanDAO(private val db: Database) { stmt.setLong(1, id) stmt.setString(2, login) stmt.setString(3, code) - stmt.setLong(4, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(4, now.micros()) stmt.setLong(5, TimeUnit.MICROSECONDS.convert(validityPeriod)) stmt.setInt(6, retryCounter) stmt.executeQuery().use { @@ -100,7 +100,7 @@ class TanDAO(private val db: Database) { ) = db.serializable { conn -> val stmt = conn.prepareStatement("SELECT tan_challenge_mark_sent(?,?,?)") stmt.setLong(1, id) - stmt.setLong(2, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(2, now.micros()) stmt.setLong(3, TimeUnit.MICROSECONDS.convert(retransmissionPeriod)) stmt.executeQuery() } @@ -129,7 +129,7 @@ class TanDAO(private val db: Database) { stmt.setLong(1, id) stmt.setString(2, login) stmt.setString(3, code) - stmt.setLong(4, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(4, now.micros()) stmt.executeQuery().use { when { !it.next() -> throw internalServerError("TAN try returned nothing") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt index 0723ac88..ee1a1669 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt @@ -22,9 +22,9 @@ package tech.libeufin.bank.db import tech.libeufin.bank.BearerToken import tech.libeufin.bank.TokenScope import tech.libeufin.common.executeUpdateViolation -import tech.libeufin.common.microsToJavaInstant +import tech.libeufin.common.asInstant import tech.libeufin.common.oneOrNull -import tech.libeufin.common.toDbMicros +import tech.libeufin.common.micros import java.time.Instant /** Data access logic for auth tokens */ @@ -56,8 +56,8 @@ class TokenDAO(private val db: Database) { ) VALUES (?, ?, ?, ?::token_scope_enum, ?, ?) """) stmt.setBytes(1, content) - stmt.setLong(2, creationTime.toDbMicros() ?: throw faultyTimestampByBank()) - stmt.setLong(3, expirationTime.toDbMicros() ?: throw faultyDurationByClient()) + stmt.setLong(2, creationTime.micros()) + stmt.setLong(3, expirationTime.micros()) stmt.setString(4, scope.name) stmt.setLong(5, bankCustomer) stmt.setBoolean(6, isRefreshable) @@ -80,8 +80,8 @@ class TokenDAO(private val db: Database) { stmt.setBytes(1, token) stmt.oneOrNull { BearerToken( - creationTime = it.getLong("creation_time").microsToJavaInstant() ?: throw faultyDurationByClient(), - expirationTime = it.getLong("expiration_time").microsToJavaInstant() ?: throw faultyDurationByClient(), + creationTime = it.getLong("creation_time").asInstant(), + expirationTime = it.getLong("expiration_time").asInstant(), login = it.getString("login"), scope = TokenScope.valueOf(it.getString("scope")), isRefreshable = it.getBoolean("is_refreshable") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt index bbd70bcf..e82f0ba2 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt @@ -51,7 +51,7 @@ class TransactionDAO(private val db: Database) { is2fa: Boolean, requestUid: ShortHashCode?, ): BankTransactionResult = db.serializable { conn -> - val now = timestamp.toDbMicros() ?: throw faultyTimestampByBank() + val now = timestamp.micros() conn.transaction { val stmt = conn.prepareStatement(""" SELECT diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt index 27b4002e..3a41c0d8 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -56,7 +56,7 @@ class WithdrawalDAO(private val db: Database) { stmt.setObject(2, uuid) stmt.setLong(3, amount.value) stmt.setInt(4, amount.frac) - stmt.setLong(5, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(5, now.micros()) stmt.executeQuery().use { when { !it.next() -> @@ -165,7 +165,7 @@ class WithdrawalDAO(private val db: Database) { ) stmt.setString(1, login) stmt.setObject(2, uuid) - stmt.setLong(3, now.toDbMicros() ?: throw faultyTimestampByBank()) + stmt.setLong(3, now.micros()) stmt.setBoolean(4, is2fa) stmt.executeQuery().use { when { diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt index 3f235d4f..c34565fd 100644 --- a/bank/src/test/kotlin/StatsTest.kt +++ b/bank/src/test/kotlin/StatsTest.kt @@ -37,7 +37,7 @@ class StatsTest { suspend fun cashin(amount: String) { db.conn { conn -> val stmt = conn.prepareStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)") - stmt.setLong(1, Instant.now().toDbMicros()!!) + stmt.setLong(1, Instant.now().micros()) stmt.setBytes(2, ShortHashCode.rand().raw) val amount = TalerAmount(amount) stmt.setLong(3, amount.value) diff --git a/common/src/main/kotlin/time.kt b/common/src/main/kotlin/time.kt index 834183ee..255004f5 100644 --- a/common/src/main/kotlin/time.kt +++ b/common/src/main/kotlin/time.kt @@ -26,61 +26,35 @@ import java.time.temporal.ChronoUnit private val logger: Logger = LoggerFactory.getLogger("libeufin-common") -/** - * Converts the 'this' Instant to the number of nanoseconds - * since the Epoch. It returns the result as Long, or null - * if one arithmetic overflow occurred. - */ -private fun Instant.toNanos(): Long? { - val oneSecNanos = ChronoUnit.SECONDS.duration.toNanos() - val nanoBase: Long = this.epochSecond * oneSecNanos - if (nanoBase != 0L && nanoBase / this.epochSecond != oneSecNanos) { - logger.error("Multiplication overflow: could not convert Instant to nanos.") - return null - } - val res = nanoBase + this.nano - if (res < nanoBase) { - logger.error("Addition overflow: could not convert Instant to nanos.") - return null - } - return res -} - -/** - * This function converts an Instant input to the - * number of microseconds since the Epoch, except that - * it yields Long.MAX if the Input is Instant.MAX. - * - * Takes the name after the way timestamps are designed - * in the database: micros since Epoch, or Long.MAX for - * "never". - * - * Returns the Long representation of 'this' or null - * if that would overflow. - */ -fun Instant.toDbMicros(): Long? { - if (this == Instant.MAX) +/** + * Convert Instant to microseconds since the epoch. + * + * Returns Long.MAX_VALUE if instant is Instant.MAX + **/ +fun Instant.micros(): Long { + if (this == Instant.MAX) return Long.MAX_VALUE - val nanos = this.toNanos() ?: run { - logger.error("Could not obtain micros to store to database, convenience conversion to nanos overflew.") - return null + try { + val micros = ChronoUnit.MICROS.between(Instant.EPOCH, this) + if (micros == Long.MAX_VALUE) throw ArithmeticException() + return micros + } catch (e: ArithmeticException) { + throw Exception("${this} is too big to be converted to micros resolution", e) } - return nanos / 1000L } -/** - * This helper is typically used to convert a timestamp expressed - * in microseconds from the DB back to the Web application. In case - * of _any_ error, it logs it and returns null. +/** + * Convert microsecons to Instant. + * + * Returns Instant.MAX if microseconds is Long.MAX_VALUE */ -fun Long.microsToJavaInstant(): Instant? { +fun Long.asInstant(): Instant { if (this == Long.MAX_VALUE) return Instant.MAX return try { Instant.EPOCH.plus(this, ChronoUnit.MICROS) - } catch (e: Exception) { - logger.error(e.message) - return null + } catch (e: ArithmeticException ) { + throw Exception("${this} is too big to be converted to Instant", e) } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt index 1367c97c..fa74c791 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt @@ -58,7 +58,7 @@ class FileLogger(path: String?) { // Subdir based on current day. val now = Instant.now() val asUtcDate = LocalDate.ofInstant(now, ZoneId.of("UTC")) - val nowMs = now.toDbMicros() + val nowMs = now.micros() // Creating the combined dir. val subDir = dir.resolve("${asUtcDate.year}-${asUtcDate.monthValue}-${asUtcDate.dayOfMonth}").resolve("fetch") subDir.createDirectories() @@ -86,7 +86,7 @@ class FileLogger(path: String?) { // Subdir based on current day. val now = Instant.now() val asUtcDate = LocalDate.ofInstant(now, ZoneId.of("UTC")) - val nowMs = now.toDbMicros() + val nowMs = now.micros() // Creating the combined dir. val subDir = dir.resolve("${asUtcDate.year}-${asUtcDate.monthValue}-${asUtcDate.dayOfMonth}").resolve("submit") subDir.createDirectories() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt index eba3c78a..162fddee 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt @@ -48,10 +48,7 @@ class InitiatedDAO(private val db: Database) { stmt.setInt(2, paymentData.amount.frac) stmt.setString(3, paymentData.wireTransferSubject) stmt.setString(4, paymentData.creditPaytoUri.toString()) - val initiationTime = paymentData.initiationTime.toDbMicros() ?: run { - throw Exception("Initiation time could not be converted to microseconds for the database.") - } - stmt.setLong(5, initiationTime) + stmt.setLong(5, paymentData.initiationTime.micros()) stmt.setString(6, paymentData.requestUid) if (stmt.executeUpdateViolation()) return@conn PaymentInitiationResult.SUCCESS @@ -73,7 +70,7 @@ class InitiatedDAO(private val db: Database) { ,submission_counter = submission_counter + 1 WHERE initiated_outgoing_transaction_id = ? """) - stmt.setLong(1, now.toDbMicros()!!) + stmt.setLong(1, now.micros()) stmt.setString(2, orderId) stmt.setLong(3, id) stmt.execute() @@ -93,7 +90,7 @@ class InitiatedDAO(private val db: Database) { ,submission_counter = submission_counter + 1 WHERE initiated_outgoing_transaction_id = ? """) - stmt.setLong(1, now.toDbMicros()!!) + stmt.setLong(1, now.micros()) stmt.setString(2, msg) stmt.setLong(3, id) stmt.execute() @@ -174,10 +171,7 @@ class InitiatedDAO(private val db: Database) { suspend fun submittable(currency: String): List = db.conn { conn -> fun extract(it: ResultSet): InitiatedPayment { val rowId = it.getLong("initiated_outgoing_transaction_id") - val initiationTime = it.getLong("initiation_time").microsToJavaInstant() - if (initiationTime == null) { // nexus fault - throw Exception("Found invalid timestamp at initiated payment with ID: $rowId") - } + val initiationTime = it.getLong("initiation_time").asInstant() return InitiatedPayment( id = it.getLong("initiated_outgoing_transaction_id"), amount = it.getAmount("amount", currency), diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt index 2e315f38..2730a437 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt @@ -38,8 +38,7 @@ class PaymentDAO(private val db: Database) { SELECT out_tx_id, out_initiated, out_found FROM register_outgoing((?,?)::taler_amount,?,?,?,?) """) - val executionTime = paymentData.executionTime.toDbMicros() - ?: throw Exception("Could not convert outgoing payment execution_time to microseconds") + val executionTime = paymentData.executionTime.micros() stmt.setLong(1, paymentData.amount.value) stmt.setInt(2, paymentData.amount.frac) stmt.setString(3, paymentData.wireTransferSubject) @@ -72,10 +71,8 @@ class PaymentDAO(private val db: Database) { SELECT out_found, out_tx_id, out_bounce_id FROM register_incoming_and_bounce((?,?)::taler_amount,?,?,?,?,(?,?)::taler_amount,?) """) - val refundTimestamp = now.toDbMicros() - ?: throw Exception("Could not convert refund execution time from Instant.now() to microsends.") - val executionTime = paymentData.executionTime.toDbMicros() - ?: throw Exception("Could not convert payment execution time from Instant to microseconds.") + val refundTimestamp = now.micros() + val executionTime = paymentData.executionTime.micros() stmt.setLong(1, paymentData.amount.value) stmt.setInt(2, paymentData.amount.frac) stmt.setString(3, paymentData.wireTransferSubject) @@ -109,8 +106,7 @@ class PaymentDAO(private val db: Database) { SELECT out_found, out_tx_id FROM register_incoming_and_talerable((?,?)::taler_amount,?,?,?,?,?) """) - val executionTime = paymentData.executionTime.toDbMicros() - ?: throw Exception("Could not convert payment execution time from Instant to microseconds.") + val executionTime = paymentData.executionTime.micros() stmt.setLong(1, paymentData.amount.value) stmt.setInt(2, paymentData.amount.frac) stmt.setString(3, paymentData.wireTransferSubject) -- cgit v1.2.3