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

io.janstenpickle.hotswapref.HotswapRef.scala Maven / Gradle / Ivy

The newest version!
package io.janstenpickle.hotswapref

import cats.effect.kernel.Resource.ExitCase
import cats.effect.kernel.{Concurrent, Ref, Resource, Unique}
import cats.effect.std.{Hotswap, Semaphore}
import cats.syntax.eq._
import cats.syntax.flatMap._
import cats.syntax.functor._

/** A concurrent data structure that wraps a [[cats.effect.std.Hotswap]] providing access to `R` using a
  * [[cats.effect.kernel.Ref]], that is set on resource acquisition, while providing asynchronous hotswap functionality
  * via calls to [[swap]].
  *
  * In short, calls to [[swap]] do not block the usage of `R` via calls to [[access]].
  *
  * Repeated concurrent calls to [[swap]] are ordered by a semaphore to ensure that `R` doesn't churn unexpectedly. As
  * with calls to [[swap]] on [[cats.effect.std.Hotswap]], [[swap]] will block until the previous
  * [[cats.effect.kernel.Resource]] is finalized. Additionally open references to `R` are counted when it is accessed
  * via [[access]], any `R` with open references will block at finalization until all references are released, and
  * therefore subsequent calls to [[swap]] will block.
  */
trait HotswapRef[F[_], R] {

  /** Swap the current resource with a new version
    *
    * This makes use of `evalTap` on the provided [[cats.effect.kernel.Resource]] to ensure the
    * [[cats.effect.kernel.Ref]] with `R` is updated immediately on allocation and may be used by [[access]] calls while
    * [[swap]] blocks, waiting for the previous [[cats.effect.kernel.Resource]] to finalize.
    *
    * This means that while there is no previous finalization process in progress when this is called, `R` may be
    * swapped in the holder ref, but will block until all references to `R` are removed and `R` is torn down.
    *
    * A semaphore guarantees that concurrent access to [[swap]] will wait while previous resources are finalized.
    */
  def swap(next: Resource[F, R]): F[Unit]

  /** Access `R` safely
    *
    * Note that access to `R` is protected by a shared-mode lock via a [[cats.effect.kernel.Resource]] scope. A resource
    * `R` with unreleased locks cannot be finalized and therefore cannot be fully swapped.
    */
  def access: Resource[F, R]
}

object HotswapRef {

  /** Creates a new [[HotswapRef]] initialized with the specified resource. The [[HotswapRef]] instance is returned
    * within a [[cats.effect.kernel.Resource]]
    */
  def apply[F[_], R](initial: Resource[F, R])(implicit F: Concurrent[F]): Resource[F, HotswapRef[F, R]] = {

    type Secured[A] = (A, Lock[F], Unique.Token)
    type Allocated[A] = (A, ExitCase => F[Unit])

    /* Secure a resource by enriching it with a semaphore-based lock and a unique token and by modifying its finalizer.
     *
     * The lock is acquired in shared mode when the resource is accessed (permits concurrent access but prohibits
     * finalization) and in exclusive mode when the resource is finalized (prohibits access).
     *
     * The token is used during access for consistent read of the holder reference.
     */
    def secure(res: Resource[F, R]): Resource[F, Secured[R]] = {
      val allocated = Lock[F].flatMap { lock =>
        Unique[F].unique.flatMap { token =>
          res.allocated.map { case (r, release) =>
            ((r, lock, token), (_: ExitCase) => lock.exclusive.surround(release))
          }
        }
      }

      Resource.applyFull(poll => poll(allocated))
    }

    def impl(hotswap: Hotswap[F, Secured[R]], holder: Ref[F, Secured[R]], sem: Semaphore[F]): HotswapRef[F, R] =
      new HotswapRef[F, R] {
        override def swap(next: Resource[F, R]): F[Unit] =
          sem.permit.surround(hotswap.swap(secure(next).evalTap(holder.set))).void

        override val access: Resource[F, R] = Resource.applyFull { poll =>
          /* Access to the resource is protected by a shared-mode lock. The holder reference is read at least twice:
           * first, to retrieve its content, and then, after acquiring the lock, to check if the content hasn't changed
           * since the first read. If the holder has been swapped, the lock is released and the new content is passed
           * to the next step of the loop. Otherwise, it's used to build the resulting `Resource`.
           */
          val step: Secured[R] => F[Either[Secured[R], Allocated[R]]] = { case (r, lock, token) =>
            poll(lock.shared.allocated).flatMap { case (_, lockRelease) =>
              holder.get.flatMap { case tup1 @ (_, _, token1) =>
                if (token =!= token1) lockRelease.as(Left(tup1))
                else F.pure(Right((r, _ => lockRelease)))
              }
            }
          }

          holder.get.flatMap(_.tailRecM(step))
        }
      }

    Hotswap(secure(initial)).evalMap { case (hotswap, securedR) =>
      Ref.of(securedR).flatMap { holder =>
        Semaphore(1L).map { sem =>
          impl(hotswap, holder, sem)
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy