Repository navigation
Bound clear and release by loadingTimeout #390
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,14 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { | |
| `loading timeout does not expire loaded values`[IO].run() | ||
| } | ||
|
|
||
| test("clear gives up on stuck loads") { | ||
| `clear gives up on stuck loads`[IO].run() | ||
| } | ||
|
|
||
| test("release gives up on stuck loads") { | ||
| `release gives up on stuck loads`[IO].run() | ||
| } | ||
|
|
||
| test(s"refresh periodically") { | ||
| refreshPeriodically[IO].run() | ||
| } | ||
|
|
@@ -181,6 +189,69 @@ class ExpiringCacheSpec extends AsyncFunSuite with Matchers { | |
| } | ||
| } | ||
|
|
||
| private def `clear gives up on stuck loads`[F[_]: Async] = { | ||
| val config = ExpiringCache.Config[F, Int, Int]( | ||
| expireAfterRead = 1.minute, | ||
| loadingTimeout = 100.millis.some, | ||
| ) | ||
| ExpiringCache.of[F, Int, Int](config).use { cache => | ||
| for { | ||
| started <- Deferred[F, Unit] | ||
| gate <- Deferred[F, Unit] | ||
| released <- Deferred[F, Unit] | ||
| loader <- cache | ||
| .getOrUpdate1(0) { started.complete(()) *> gate.get.as((0, 0, released.complete(()).void.some)) } | ||
| .attempt | ||
| .start | ||
| _ <- started.get | ||
| waiter <- cache.getOrUpdate(0) { 1.pure[F] }.attempt.start | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Capture the pending load before clearing the cache.
Call 🤖 Prompt for AI Agents |
||
| result <- { | ||
| for { | ||
| _ <- Temporal[F].timeout(cache.clear.flatten, 2.seconds) | ||
| outcome <- Temporal[F].timeout(waiter.joinWithNever, 2.seconds) | ||
| _ <- Sync[F].delay { outcome should matchPattern { case Left(ExpiredError) => } } | ||
| _ <- gate.complete(()) | ||
| outcome <- Temporal[F].timeout(loader.joinWithNever, 2.seconds) | ||
| _ <- Sync[F].delay { outcome should matchPattern { case Left(ExpiredError) => } } | ||
| _ <- Temporal[F].timeout(released.get, 2.seconds) | ||
| value <- cache.get(0) | ||
| _ <- Sync[F].delay { value shouldEqual none } | ||
| } yield {} | ||
| }.guarantee { gate.complete(()) *> loader.join.void } | ||
| } yield result | ||
| } | ||
| } | ||
|
|
||
| private def `release gives up on stuck loads`[F[_]: Async] = { | ||
| val config = ExpiringCache.Config[F, Int, Int]( | ||
| expireAfterRead = 1.minute, | ||
| loadingTimeout = 100.millis.some, | ||
| ) | ||
| for { | ||
| started <- Deferred[F, Unit] | ||
| gate <- Deferred[F, Unit] | ||
| released <- Deferred[F, Unit] | ||
| (cache, release) <- ExpiringCache.of[F, Int, Int](config).allocated | ||
| loader <- cache | ||
| .getOrUpdate1(0) { started.complete(()) *> gate.get.as((0, 0, released.complete(()).void.some)) } | ||
| .attempt | ||
| .start | ||
| _ <- started.get | ||
| // The release is uncancelable, hence run in a fiber of its own with the timeout on the join, | ||
| // so that a release that hangs fails the test rather than blocking it. | ||
| releasing <- release.start | ||
| result <- { | ||
| for { | ||
| _ <- Temporal[F].timeout(releasing.joinWithNever, 2.seconds) | ||
| _ <- gate.complete(()) | ||
| outcome <- Temporal[F].timeout(loader.joinWithNever, 2.seconds) | ||
| _ <- Sync[F].delay { outcome should matchPattern { case Left(ExpiredError) => } } | ||
| _ <- Temporal[F].timeout(released.get, 2.seconds) | ||
| } yield {} | ||
| }.guarantee { gate.complete(()) *> loader.join.void *> releasing.join.void } | ||
| } yield result | ||
| } | ||
|
|
||
| private def refreshPeriodically[F[_]: Async] = { | ||
| val refresh = ExpiringCache.Refresh[Int](100.millis) { _.some.pure[F] } | ||
| val config = ExpiringCache.Config( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
rg -n 'LoadingCache' scache/src/main/scala/com/evolution/scache/Cache.scalaRepository: evolution-gaming/scache
Length of output: 204
🏁 Script executed:
Repository: evolution-gaming/scache
Length of output: 25300
Document the one-minute timeout for
Cache.loading.Cache.loadingcallsLoadingCache.of[F, K, V]without aloadingTimeoutargument. The overload usesSome(1.minute). Itsclearand resource-release waits therefore stop waiting after one minute.Proposed correction
📝 Committable suggestion
🤖 Prompt for AI Agents