From a5b5cf41c98a937039e12ef8dc8c7a546f648a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Thu, 9 Oct 2025 14:38:34 -0300 Subject: [PATCH] migrate to cats-retry 4 --- .../scala/explore/targets/TargetSource.scala | 8 +++-- .../scala/explore/common/RetryHelpers.scala | 35 ------------------- .../scala/explore/common/SimbadSearch.scala | 26 +++++++------- .../scala/observe/ui/components/MainApp.scala | 11 +++--- project/Versions.scala | 2 +- .../main/scala/lucuma/ui/sso/SSOClient.scala | 25 ++++++------- .../scala/lucuma/ui/utils/RetryHelpers.scala | 13 +++---- 7 files changed, 46 insertions(+), 74 deletions(-) delete mode 100644 explore/common/src/main/scala/explore/common/RetryHelpers.scala diff --git a/explore/app/src/main/scala/explore/targets/TargetSource.scala b/explore/app/src/main/scala/explore/targets/TargetSource.scala index 038455a393..952434b7f8 100644 --- a/explore/app/src/main/scala/explore/targets/TargetSource.scala +++ b/explore/app/src/main/scala/explore/targets/TargetSource.scala @@ -6,6 +6,7 @@ package explore.targets import cats.Order import cats.data.NonEmptyList import cats.effect.Async +import cats.effect.std.Random import cats.syntax.all.* import eu.timepit.refined.types.string.NonEmptyString import explore.common.SimbadSearch @@ -40,7 +41,8 @@ object TargetSource: override def toString: String = programId.toString - case class FromCatalog[F[_]: Async: Logger](catalogName: CatalogName) extends TargetSource[F]: + case class FromCatalog[F[_]: Async: Random: Logger](catalogName: CatalogName) + extends TargetSource[F]: val name: String = Enumerated[CatalogName].tag(catalogName).capitalize val existing: Boolean = false @@ -97,12 +99,12 @@ object TargetSource: override def toString: String = catalogName.toString - def forAllCatalogs[F[_]: Async: Logger]: NonEmptyList[TargetSource[F]] = + def forAllCatalogs[F[_]: Async: Random: Logger]: NonEmptyList[TargetSource[F]] = NonEmptyList.fromListUnsafe( Enumerated[CatalogName].all.map(source => TargetSource.FromCatalog(source)) ) - def forAllSiderealCatalogs[F[_]: Async: Logger]: NonEmptyList[TargetSource[F]] = + def forAllSiderealCatalogs[F[_]: Async: Random: Logger]: NonEmptyList[TargetSource[F]] = NonEmptyList.of(TargetSource.FromCatalog(CatalogName.Simbad)) // TODO Test diff --git a/explore/common/src/main/scala/explore/common/RetryHelpers.scala b/explore/common/src/main/scala/explore/common/RetryHelpers.scala deleted file mode 100644 index 40773905e5..0000000000 --- a/explore/common/src/main/scala/explore/common/RetryHelpers.scala +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA) -// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause - -package explore.common - -import cats.Applicative -import org.typelevel.log4cats.Logger -import retry.* -import retry.RetryDetails.* -import retry.RetryPolicies.* - -import java.util as ju -import scala.concurrent.duration.* - -import ju.concurrent.TimeUnit - -trait RetryHelpers { - def retryPolicy[F[_]: Applicative] = - capDelay( - FiniteDuration.apply(5, TimeUnit.SECONDS), - fullJitter[F](FiniteDuration.apply(10, TimeUnit.MILLISECONDS)) - ).join(limitRetries[F](12)) - - def logError[F[_]: Logger](msg: String)(err: Throwable, details: RetryDetails): F[Unit] = - details match { - case WillDelayAndRetry(_, retriesSoFar, _) => - Logger[F].warn(err)(s"$msg failed - Will retry. Retries so far: [$retriesSoFar]") - - case GivingUp(totalRetries, _) => - Logger[F].error(err)(s"$msg failed - Giving up after [$totalRetries] retries.") - } - -} - -object RetryHelpers extends RetryHelpers diff --git a/explore/common/src/main/scala/explore/common/SimbadSearch.scala b/explore/common/src/main/scala/explore/common/SimbadSearch.scala index a17a804dd2..7d0cdb5ba9 100644 --- a/explore/common/src/main/scala/explore/common/SimbadSearch.scala +++ b/explore/common/src/main/scala/explore/common/SimbadSearch.scala @@ -5,6 +5,7 @@ package explore.common import cats.data.NonEmptyChain import cats.effect.* +import cats.effect.std.Random import cats.syntax.all.* import eu.timepit.refined.types.string.NonEmptyString import explore.model.Constants @@ -22,9 +23,9 @@ import java.util.concurrent.TimeoutException import scala.concurrent.duration.* object SimbadSearch { - import RetryHelpers.* + import lucuma.ui.utils.RetryHelpers.* - def search[F[_]]( + def search[F[_]: Random]( term: NonEmptyString, wildcard: Boolean = false )(implicit F: Async[F], logger: Logger[F]): F[List[CatalogTargetResult]] = @@ -39,7 +40,7 @@ object SimbadSearch { searchSingle[F](uri, term, wildcard) .raceAllToSuccess - private def searchSingle[F[_]]( + private def searchSingle[F[_]: Random]( simbadUrl: Uri, term: NonEmptyString, wildcard: Boolean @@ -56,16 +57,12 @@ object SimbadSearch { else baseURL - def isWorthRetrying(e: Throwable): F[Boolean] = e match { - case _: TimeoutException => F.pure(!wildcard) - case _ => F.pure(true) - } + def isWorthRetrying(e: Throwable): Boolean = + e match + case _: TimeoutException => !wildcard + case _ => true - retryingOnSomeErrors( - retryPolicy[F], - isWorthRetrying, - logError[F]("Simbad") - ) { + retryingOnErrors { FetchClientBuilder[F] .withRequestTimeout(15.seconds) .resource @@ -83,6 +80,9 @@ object SimbadSearch { case _ => Logger[F].error(s"Simbad search failed for term [$term]").as(List.empty) } - } + }( + retryPolicy[F], + errorHandler = ResultHandler.retryOnSomeErrors(isWorthRetrying, log = logError[F]("Simbad")) + ) } } diff --git a/modules/web/client/src/main/scala/observe/ui/components/MainApp.scala b/modules/web/client/src/main/scala/observe/ui/components/MainApp.scala index ea476e1cd4..6fab319875 100644 --- a/modules/web/client/src/main/scala/observe/ui/components/MainApp.scala +++ b/modules/web/client/src/main/scala/observe/ui/components/MainApp.scala @@ -168,11 +168,14 @@ object MainApp extends ServerEventHandler: wsConnection <- useResourceOnMount: // wsConnection to server (1) Reconnect( - retryingOnAllErrors[WSConnectionHighLevel[IO]]( + retryingOnErrors( + WebSocketClient[IO].connectHighLevel(WSRequest(EventWsUri)) + )( policy = WSRetryPolicy, - onError = - (_: Throwable, _) => syncStatus.async.set(SyncStatus.OutOfSync.some).toResource - )(WebSocketClient[IO].connectHighLevel(WSRequest(EventWsUri))), + errorHandler = ResultHandler.retryOnAllErrors(log = + (_, _) => syncStatus.async.set(SyncStatus.OutOfSync.some).toResource + ) + ), _ => true.pure[IO] ).map(_.some) clientConfigPot <- useStateView(Pot.pending[ClientConfig]) diff --git a/project/Versions.scala b/project/Versions.scala index e3d9e6c0d0..dd6f37bd20 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -4,7 +4,7 @@ object Versions { val cats = "2.13.0" val catsEffect = "3.6.3" val catsParse = "1.1.0" - val catsRetry = "3.1.3" + val catsRetry = "4.0.0" val catsTime = "0.6.0" val circe = "0.14.15" val circeRefined = "0.15.1" diff --git a/ui/lib/src/main/scala/lucuma/ui/sso/SSOClient.scala b/ui/lib/src/main/scala/lucuma/ui/sso/SSOClient.scala index 1b6b58e471..722e7eef12 100644 --- a/ui/lib/src/main/scala/lucuma/ui/sso/SSOClient.scala +++ b/ui/lib/src/main/scala/lucuma/ui/sso/SSOClient.scala @@ -5,6 +5,7 @@ package lucuma.ui.sso import cats.Applicative import cats.effect.* +import cats.effect.std.Random import cats.implicits.* import eu.timepit.refined.* import eu.timepit.refined.collection.NonEmpty @@ -26,7 +27,7 @@ import java.util as ju case class JwtOrcidProfile(exp: Long, `lucuma-user`: User) derives Decoder -case class SSOClient[F[_]: Async: Logger](config: SSOConfig): +case class SSOClient[F[_]: Async: Random: Logger](config: SSOConfig): private val client = FetchClientBuilder[F] .withRequestTimeout(config.readTimeout) .withCredentials(RequestCredentials.include) @@ -41,7 +42,7 @@ case class SSOClient[F[_]: Async: Logger](config: SSOConfig): } val guest: F[UserVault] = - retryingOnAllErrors(retryPolicy[F], logError[F]("Switching to guest")) { + retryingOnErrors { client.use( _.expect[String](Request[F](Method.POST, config.uri / "api" / "v1" / "auth-as-guest")) .map(body => @@ -57,10 +58,10 @@ case class SSOClient[F[_]: Async: Logger](config: SSOConfig): .getOrElse(throw new RuntimeException("Error decoding the token")) ) ) - } + }(retryPolicy[F], ResultHandler.retryOnAllErrors(log = logError[F]("Switching to guest"))) val whoami: F[Option[UserVault]] = - retryingOnAllErrors(retryPolicy[F], logError[F]("Calling whoami")) { + retryingOnErrors { client .flatMap(_.run(Request[F](Method.POST, config.uri / "api" / "v1" / "refresh-token"))) .use { @@ -86,29 +87,29 @@ case class SSOClient[F[_]: Async: Logger](config: SSOConfig): case _ => Applicative[F].pure(none[UserVault]) } - } + }(retryPolicy[F], ResultHandler.retryOnAllErrors(log = logError[F]("Calling whoami"))) .adaptError { case t => new Exception("Error connecting to authentication server.", t) } def switchRole(roleId: StandardRole.Id): F[Option[UserVault]] = - retryingOnAllErrors(retryPolicy[F], logError[F]("Switching role")) { + retryingOnErrors { client .flatMap( _.run( - Request(Method.GET, - (config.uri / "auth" / "v1" / "set-role").withQueryParam("role", roleId.show) + Request( + Method.GET, + (config.uri / "auth" / "v1" / "set-role").withQueryParam("role", roleId.show) ) ) ) .use_ *> whoami - - } + }(retryPolicy[F], ResultHandler.retryOnAllErrors(log = logError[F]("Switching role"))) val logout: F[Unit] = - retryingOnAllErrors(retryPolicy[F], logError[F]("Calling logout")) { + retryingOnErrors { client.flatMap(_.run(Request(Method.POST, config.uri / "api" / "v1" / "logout"))).use_ - } + }(retryPolicy[F], ResultHandler.retryOnAllErrors(log = logError[F]("Calling logout"))) val switchToORCID: F[Unit] = logout.attempt >> redirectToLogin diff --git a/ui/lib/src/main/scala/lucuma/ui/utils/RetryHelpers.scala b/ui/lib/src/main/scala/lucuma/ui/utils/RetryHelpers.scala index 355bd2ad5a..0909b4e97c 100644 --- a/ui/lib/src/main/scala/lucuma/ui/utils/RetryHelpers.scala +++ b/ui/lib/src/main/scala/lucuma/ui/utils/RetryHelpers.scala @@ -13,19 +13,20 @@ import java.util as ju import scala.concurrent.duration.* import ju.concurrent.TimeUnit +import cats.effect.std.Random trait RetryHelpers: - def retryPolicy[F[_]: Applicative] = + def retryPolicy[F[_]: Applicative: Random] = capDelay( FiniteDuration.apply(5, TimeUnit.SECONDS), fullJitter[F](FiniteDuration.apply(10, TimeUnit.MILLISECONDS)) ).join(limitRetries[F](12)) def logError[F[_]: Logger](msg: String)(err: Throwable, details: RetryDetails): F[Unit] = - details match - case WillDelayAndRetry(_, retriesSoFar, _) => - Logger[F].warn(err)(s"$msg failed - Will retry. Retries so far: [$retriesSoFar]") - case GivingUp(totalRetries, _) => - Logger[F].error(err)(s"$msg failed - Giving up after [$totalRetries] retries.") + details.nextStepIfUnsuccessful match + case NextStep.DelayAndRetry(_) => + Logger[F].warn(err)(s"$msg failed - Will retry. Retries so far: [${details.retriesSoFar}]") + case NextStep.GiveUp => + Logger[F].error(err)(s"$msg failed - Giving up after [${details.retriesSoFar}] retries.") object RetryHelpers extends RetryHelpers