All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.stellar.walletsdk.anchor.Watcher.kt Maven / Gradle / Ivy

There is a newer version: 1.7.2
Show newest version
package org.stellar.walletsdk.anchor

import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.Instant
import mu.KotlinLogging
import org.stellar.walletsdk.asset.StellarAssetId
import org.stellar.walletsdk.auth.AuthToken

private val log = KotlinLogging.logger {}

class Watcher
internal constructor(
  private val anchor: Anchor,
  private val pollDelay: Duration,
  private val channelSize: Int,
  private val exceptionHandler: WalletExceptionHandler
) {
  suspend fun watchOneTransaction(
    authToken: AuthToken,
    id: String,
    lang: String? = null
  ): WatcherResult {
    val channel = Channel(channelSize)

    val job = coroutineScope {
      launch(Job()) {
          var oldStatus: TransactionStatus? = null
          val retryContext = RetryContext()

          do {
            var shouldExit: Boolean

            try {
              val transaction =
                anchor.interactive().getTransactionBy(authToken, id = id, lang = lang)
              val statusChange = StatusChange(transaction, transaction.status, oldStatus)

              if (statusChange.status != statusChange.oldStatus) {
                channel.send(statusChange)
              }
              oldStatus = statusChange.status

              if (!statusChange.isTerminal()) {
                delay(pollDelay)
              }

              shouldExit = statusChange.isTerminal()
              retryContext.refresh()
            } catch (e: Exception) {
              try {
                retryContext.onError(e)
                shouldExit = exceptionHandler.invoke(retryContext)
              } catch (e: Exception) {
                shouldExit = true
                log.error(e) { "CRITICAL: Couldn't invoke exception handler" }
              }
              if (shouldExit) {
                channel.send(ExceptionHandlerExit)
              }
            }
          } while (!shouldExit)

          channel.send(ChannelClosed)
        }
        .also { it.invokeOnCompletion { channel.close() } }
    }

    return WatcherResult(channel, job)
  }

  suspend fun watchAsset(
    authToken: AuthToken,
    asset: StellarAssetId,
    since: Instant? = null,
    lang: String? = null,
    kind: TransactionKind? = null
  ): WatcherResult {
    val channel = Channel(channelSize)

    val job = coroutineScope {
      launch(Job()) {
          var transactionStatuses = mapOf()
          val retryContext = RetryContext()

          do {
            var shouldExit: Boolean

            try {
              val transactions =
                anchor
                  .interactive()
                  .getTransactionsForAsset(asset, authToken, since, kind = kind, lang = lang)
                  .associateBy { it.id }

              transactions.forEach {
                val tx = it.value
                val previousStatus = transactionStatuses[it.key]?.status

                if (tx.status != previousStatus) {
                  channel.send(StatusChange(tx, tx.status, previousStatus))
                }
              }

              val unfinishedTransactions =
                transactions.filter { it.value.status.isTerminal().not() }

              transactionStatuses = transactions

              if (unfinishedTransactions.isNotEmpty()) {
                delay(pollDelay)
              }

              shouldExit = unfinishedTransactions.isEmpty()
              retryContext.refresh()
            } catch (e: Exception) {
              try {
                retryContext.onError(e)
                shouldExit = exceptionHandler.invoke(retryContext)
              } catch (e: Exception) {
                shouldExit = true
                log.error(e) { "CRITICAL: Couldn't invoke exception handler" }
              }
              if (shouldExit) {
                channel.send(ExceptionHandlerExit)
              }
            }
          } while (!shouldExit)

          channel.send(ChannelClosed)
        }
        .also { it.invokeOnCompletion { channel.close() } }
    }

    return WatcherResult(channel, job)
  }
}

typealias WalletExceptionHandler = suspend (RetryContext) -> (Boolean)

/**
 * Simple exception handler that retries on the error
 *
 * @constructor Create empty Retry exception handler
 * @property maxRetryCount maximum consequent retry count. If this specified number of errors
 * happens in a row, handler will give up retrying.
 * @property backoffPeriod delay before resuming the job
 */
class RetryExceptionHandler(
  private val maxRetryCount: Int = 3,
  private val backoffPeriod: Duration = 5.seconds
) : WalletExceptionHandler {
  override suspend fun invoke(ctx: RetryContext): Boolean {
    log.error(ctx.exception) {
      "Exception on getting transaction data. Try ${ctx.retries}/${maxRetryCount}"
    }

    if (ctx.retries < maxRetryCount) {
      delay(backoffPeriod)
      return false
    }
    return true
  }
}

class RetryContext {
  var retries: Int = 0
    private set
  var exception: Exception? = null
    private set

  internal fun refresh() {
    retries = 0
    exception = null
  }

  internal fun onError(e: Exception) {
    exception = e
    retries++
  }
}

sealed interface StatusUpdateEvent

data class StatusChange(
  val transaction: AnchorTransaction,
  val status: TransactionStatus,
  val oldStatus: TransactionStatus? = null,
) : StatusUpdateEvent {
  fun isTerminal(): Boolean {
    return status.isTerminal()
  }

  fun isError(): Boolean {
    return status.isError()
  }
}

object ChannelClosed : StatusUpdateEvent

object ExceptionHandlerExit : StatusUpdateEvent

data class WatcherResult(val channel: Channel, val job: Job)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy