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

net.playq.tk.aws.cost.CostClient.scala Maven / Gradle / Ivy

package net.playq.tk.aws.cost

import cats.effect.concurrent.Ref
import cats.syntax.list.*
import izumi.functional.bio.{BlockingIO2, F, IO2, Temporal2}
import net.playq.tk.metrics.Metrics
import net.playq.tk.metrics.{MacroMetricCostMeter, MacroMetricCostTimer}
import net.playq.tk.quantified.SyncThrowable
import net.playq.tk.util.retry.{RetryPolicy, TkScheduler}
import software.amazon.awssdk.services.costexplorer.CostExplorerClient

import scala.jdk.CollectionConverters.*

trait CostClient[F[_, _]] {
  def getCosts(request: CostRequest): F[Throwable, CostResponse]

  private[cost] def rawRequestF[E, A](
    metric: String
  )(f: CostExplorerClient => F[E, A]
  )(implicit
    saveCounter: MacroMetricCostMeter[metric.type],
    saveTimer: MacroMetricCostTimer[metric.type],
  ): F[E, A]

  def rawRequest[A](
    metric: String
  )(f: CostExplorerClient => A
  )(implicit
    saveCounter: MacroMetricCostMeter[metric.type],
    saveTimer: MacroMetricCostTimer[metric.type],
  ): F[Throwable, A]
}

object CostClient {

  final class Impl[F[+_, +_]: IO2: SyncThrowable: BlockingIO2: TkScheduler](
    client: CostExplorerClient,
    metrics: Metrics[F],
  ) extends CostClient[F] {

    override def rawRequestF[E, A](
      metric: String
    )(f: CostExplorerClient => F[E, A]
    )(implicit
      saveCounter: MacroMetricCostMeter[metric.type],
      saveTimer: MacroMetricCostTimer[metric.type],
    ): F[E, A] = {
      F.shiftBlocking {
        metrics.withTimer(metric) {
          f(client).catchAll {
            failure =>
              metrics.mark(metric)(saveCounter) *> F.fail(failure)
          }
        }(saveTimer)
      }
    }

    override def rawRequest[A](
      metric: String
    )(f: CostExplorerClient => A
    )(implicit
      saveCounter: MacroMetricCostMeter[metric.type],
      saveTimer: MacroMetricCostTimer[metric.type],
    ): F[Throwable, A] = {
      rawRequestF(metric)(c => F.syncThrowable(f(c)))
    }

    override def getCosts(request: CostRequest): F[Throwable, CostResponse] = {
      rawRequestF("get-costs") {
        client =>
          val schedule = RetryPolicy.recursWhile[F, Option[String]](_.nonEmpty)
          for {
            ref       <- Ref.of[F[Throwable, _], List[CostResponse]](Nil)
            nextToken <- Ref.of[F[Throwable, _], Option[String]](None)
            _ <- TkScheduler[F].repeat {
              for {
                next   <- nextToken.get
                result <- F.syncThrowable(client.getCostAndUsage(request.withNextPageToken(next).makeRequest()))
                token   = Option(result.nextPageToken()).filter(_.nonEmpty)
                _      <- nextToken.set(token)
                res = CostResponse(
                  groupDefinitions = Option(result.groupDefinitions()).map(_.asScala.map(CostGroupDefinition(_)).toSeq).getOrElse(Seq.empty),
                  resultsByTime    = result.resultsByTime().asScala.map(CostResultByTime(_)).toSeq,
                  nextPageToken    = result.nextPageToken(),
                )
                _ <- ref.update(res :: _)
              } yield token
            }(schedule)
            all <- ref.get
            res <- F.fromOption(new RuntimeException("Got empty result!")) {
              all.toNel.map(_.reduceLeft {
                (b1, b2) =>
                  b1.copy(resultsByTime = b2.resultsByTime ++ b1.resultsByTime)
              })
            }
          } yield res
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy