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

dev.profunktor.redis4cats.runner.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018-2021 ProfunKtor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package dev.profunktor.redis4cats

import java.util.UUID

import scala.annotation.nowarn
import scala.concurrent.duration._

import cats.effect.kernel._
import cats.effect.kernel.implicits._
import cats.syntax.all._
import dev.profunktor.redis4cats.effect.Log
import dev.profunktor.redis4cats.hlist._
import cats.Applicative

object Runner {
  type CancelFibers[F[_]] = Throwable => F[Unit]

  case class Ops[F[_]](
      name: String,
      mainCmd: F[Unit],
      onComplete: CancelFibers[F] => F[Unit],
      onError: F[Unit],
      afterCompletion: F[Unit],
      mkError: () => Throwable
  )

  def apply[F[_]: Async: Log]: RunnerPartiallyApplied[F] =
    new RunnerPartiallyApplied[F]
}

private[redis4cats] class RunnerPartiallyApplied[F[_]: Async: Log] {

  def filterExec[T <: HList](ops: Runner.Ops[F])(commands: T)(implicit w: WitnessFilter[T]): F[w.S] = {
    import w._
    exec[T](ops)(commands).map(_.filterUnit)
  }

  def exec[T <: HList](ops: Runner.Ops[F])(commands: T)(implicit w: Witness[T]): F[w.R] =
    (Deferred[F, Either[Throwable, w.R]], Sync[F].delay(UUID.randomUUID)).tupled.flatMap {
      case (promise, uuid) =>
        def cancelFibers[A](fibs: HList)(err: Throwable): F[Unit] =
          joinOrCancel(fibs, HNil)(false) >> promise.complete(err.asLeft).ensure(promiseAlreadyCompleted)(identity).void

        def onErrorOrCancelation(fibs: HList): F[Unit] =
          cancelFibers(fibs)(ops.mkError()).guarantee(ops.onError)

        (Deferred[F, Unit], Ref.of[F, Int](0)).tupled
          .flatMap {
            case (gate, counter) =>
              // wait for commands to be scheduled
              val synchronizer: F[Unit] =
                counter.modify {
                  case n if n === (commands.size - 1) =>
                    n + 1 -> gate.complete(()).ensure(promiseAlreadyCompleted)(identity).void
                  case n => n + 1 -> Applicative[F].unit
                }.flatten

              Log[F].debug(s"${ops.name} started - ID: $uuid") >>
                (ops.mainCmd >> runner(synchronizer, commands, HNil))
                  .bracketCase(_ => gate.get) {
                    case (fibs, Outcome.Succeeded(_)) =>
                      for {
                        _ <- Log[F].debug(s"${ops.name} completed - ID: $uuid")
                        _ <- ops.onComplete(cancelFibers(fibs))
                        r <- joinOrCancel(fibs, HNil)(true)
                        // Casting here is fine since we have a `Witness` that proves this true
                        _ <- promise.complete(r.asInstanceOf[w.R].asRight).ensure(promiseAlreadyCompleted)(identity)
                      } yield ()
                    case (fibs, Outcome.Errored(e)) =>
                      Log[F].error(s"${ops.name} failed: ${e.getMessage} - ID: $uuid") >> onErrorOrCancelation(fibs)
                    case (fibs, Outcome.Canceled()) =>
                      Log[F].error(s"${ops.name} canceled - ID: $uuid") >> onErrorOrCancelation(fibs)
                  }
                  .guarantee(ops.afterCompletion) >> promise.get.rethrow.timeout(3.seconds)
          }
    }

  // Forks every command in order
  @nowarn("cat=other-match-analysis")
  private def runner[H <: HList, G <: HList](f: F[Unit], ys: H, res: G): F[HList] =
    ys match {
      case HNil                           => res.pure[F].widen
      case HCons((h: F[_] @unchecked), t) => (h, f).parTupled.map(_._1).start.flatMap(fb => runner(f, t, fb :: res))
    }

  // Joins or cancel fibers correspondent to previous executed commands
  private def joinOrCancel[H <: HList, G <: HList](ys: H, res: G)(isJoin: Boolean): F[HList] =
    ys match {
      case HNil => Applicative[F].pure(res)
      case HCons((h: Fiber[F, Throwable, Any] @unchecked), t) if isJoin =>
        h.joinWithNever.flatMap(x => joinOrCancel(t, x :: res)(isJoin))
      case HCons((h: Fiber[F, Throwable, Any] @unchecked), t) =>
        h.cancel.flatMap(x => joinOrCancel(t, x :: res)(isJoin))
      case HCons(h, t) =>
        Log[F].error(s"Unexpected result: ${h.toString}") >> joinOrCancel(t, res)(isJoin)
    }

  private val promiseAlreadyCompleted = new AssertionError("Promise already completed")
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy