Skip to content

Commit

Permalink
Merge pull request #419 from SystemFw/feature/resource/allocated
Browse files Browse the repository at this point in the history
Resource.allocated
  • Loading branch information
alexandru authored Nov 21, 2018
2 parents 18cac46 + eedc830 commit 09b89bc
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 0 deletions.
64 changes: 64 additions & 0 deletions core/shared/src/main/scala/cats/effect/Resource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,70 @@ sealed abstract class Resource[F[_], A] {
*/
def flatMap[B](f: A => Resource[F, B]): Resource[F, B] =
Bind(this, f)

/**
* Given a `Resource`, possibly built by composing multiple
* `Resource`s monadically, returns the acquired resource, as well
* as an action that runs all the finalizers for releasing it.
*
* If the outer `F` fails or is interrupted, `allocated` guarantees
* that the finalizers will be called. However, if the outer `F`
* succeeds, it's up to the user to ensure the returned `F[Unit]`
* is called once `A` needs to be released. If the returned
* `F[Unit]` is not called, the finalizers will not be run.
*
* For this reason, this is an advanced and potentially unsafe api
* which can cause a resource leak if not used correctly, please
* prefer [[use]] as the standard way of running a `Resource`
* program.
*
* Use cases include interacting with side-effectful apis that
* expect separate acquire and release actions (like the `before`
* and `after` methods of many test frameworks), or complex library
* code that needs to modify or move the finalizer for an existing
* resource.
*
*/
def allocated(implicit F: Bracket[F, Throwable]): F[(A, F[Unit])] = {

// Indirection for calling `loop` needed because `loop` must be @tailrec
def continue(
current: Resource[F, Any],
stack: List[Any => Resource[F, Any]],
release: F[Unit]): F[(Any, F[Unit])] =
loop(current, stack, release)

// Interpreter that knows how to evaluate a Resource data structure;
// Maintains its own stack for dealing with Bind chains
@tailrec def loop(
current: Resource[F, Any],
stack: List[Any => Resource[F, Any]],
release: F[Unit]): F[(Any, F[Unit])] =
current match {
case Resource.Allocate(resource) =>
F.bracketCase(resource) {
case (a, rel) =>
stack match {
case Nil => F.pure(a -> F.guarantee(rel(ExitCase.Completed))(release))
case f0 :: xs => continue(f0(a), xs, F.guarantee(rel(ExitCase.Completed))(release))
}
} {
case (_, ExitCase.Completed) =>
F.unit
case ((_, release), ec) =>
release(ec)
}
case Resource.Bind(source, f0) =>
loop(source, f0.asInstanceOf[Any => Resource[F, Any]] :: stack, release)
case Resource.Suspend(resource) =>
resource.flatMap(continue(_, stack, release))
}

loop(this.asInstanceOf[Resource[F, Any]], Nil, F.unit).map {
case (a, release) =>
(a.asInstanceOf[A], release)
}
}
}

object Resource extends ResourceInstances {
Expand Down
24 changes: 24 additions & 0 deletions laws/shared/src/test/scala/cats/effect/ResourceTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ class ResourceTests extends BaseTestsSuite {
}
}

testAsync("allocated produces the same value as the resource") { implicit ec =>
check { resource: Resource[IO, Int] =>
val a0 = Resource(resource.allocated).use(IO.pure).attempt
val a1 = resource.use(IO.pure).attempt

a0 <-> a1
}
}

test("allocate does not release until close is invoked") {
val released = new java.util.concurrent.atomic.AtomicBoolean(false)
val release = Resource.make(IO.unit)(_ => IO(released.set(true)))
val resource = Resource.liftF(IO.unit)

val prog = for {
res <- (release *> resource).allocated
(_, close) = res
releaseAfterF <- IO(released.get() shouldBe false)
_ <- close >> IO(released.get() shouldBe true)
} yield ()

prog.unsafeRunSync
}

test("safe attempt suspended resource") {
val exception = new Exception("boom!")
val suspend = Resource.suspend[IO, Int](IO.raiseError(exception))
Expand Down

0 comments on commit 09b89bc

Please sign in to comment.