Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent Hotswap#get from acquiring a lock when there is no Resource present #3922

20 changes: 10 additions & 10 deletions std/shared/src/main/scala/cats/effect/std/Hotswap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ sealed trait Hotswap[F[_], R] {
def swap(next: Resource[F, R]): F[R]

/**
* Gets the current resource, if it exists. The returned resource is guaranteed to be
* available for the duration of the returned resource.
* Acquires a shared lock to retrieve the current resource, if it exists. The returned
* resource is guaranteed to be available for its duration. The lock is released if the
* current resource does not exist.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original comment was okay, the lock is an implementation detail.

def get: Resource[F, Option[R]]

Expand Down Expand Up @@ -121,8 +122,6 @@ object Hotswap {
def raise(message: String): F[Unit] =
F.raiseError[Unit](new RuntimeException(message))

def shared: Resource[F, Unit] = semaphore.permit

def exclusive: Resource[F, Unit] =
Resource.makeFull[F, Unit](poll => poll(semaphore.acquireN(Long.MaxValue)))(_ =>
semaphore.releaseN(Long.MaxValue))
Expand All @@ -141,12 +140,13 @@ object Hotswap {
}

override def get: Resource[F, Option[R]] =
shared.evalMap { _ =>
state.get.map {
case Acquired(r, _) => Some(r)
case _ => None
}
}
Resource.makeFull[F, Option[R]] { poll =>
poll(semaphore.acquire) *>
josgarmar28 marked this conversation as resolved.
Show resolved Hide resolved
state.get.flatMap {
case Acquired(r, _) => F.pure(Some(r))
case _ => semaphore.release.as(None)
}
} { r => if (r.isDefined) semaphore.release else F.unit }

override def clear: F[Unit] =
exclusive.surround(swapFinalizer(Cleared).uncancelable)
Expand Down
12 changes: 11 additions & 1 deletion tests/shared/src/test/scala/cats/effect/std/HotswapSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ class HotswapSpec extends BaseSpec { outer =>

TestControl.executeEmbed(go, IORuntimeConfig(1, 2)).replicateA_(1000) must completeAs(())
}
}

"get should not acquire a lock when there is no resource present" in ticked {
implicit ticker =>
val go = Hotswap.create[IO, Unit].use { hs =>
hs.get.useForever.start *>
hs.swap(IO.sleep(1.second).toResource) *>
Copy link
Member

@armanbilge armanbilge Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should insert a sleep between these two steps to be sure that the fiber starts. Also I think it's okay to just swap in a Resource.unit, basically we are checking that the swap succeeds and does not hang. If the get above had acquired a shared lock, then the swap would hang due to be unable to obtain an exclusive lock.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed!

IO.sleep(2.seconds) *>
hs.get.use_.timeout(1.second).void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the rest tests anything meaningful.

}
go must completeAs(())
}
}
}
Loading