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

12 changes: 10 additions & 2 deletions std/shared/src/main/scala/cats/effect/std/Hotswap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,15 @@ object Hotswap {
def raise(message: String): F[Unit] =
F.raiseError[Unit](new RuntimeException(message))

def shared: Resource[F, Unit] = semaphore.permit
def shared(state: Ref[F, State]): Resource[F, Unit] =
Resource.makeFull[F, Unit] { poll =>
poll(
state.get.flatMap {
case Acquired(_, _) => semaphore.acquire
case _ => F.unit
josgarmar28 marked this conversation as resolved.
Show resolved Hide resolved
}
)
} { _ => semaphore.release }
josgarmar28 marked this conversation as resolved.
Show resolved Hide resolved

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

override def get: Resource[F, Option[R]] =
shared.evalMap { _ =>
shared(state).evalMap { _ =>
state.get.map {
josgarmar28 marked this conversation as resolved.
Show resolved Hide resolved
case Acquired(r, _) => Some(r)
case _ => None
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