-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement expiring cache based on scalacache (#299)
* implement expiring cache based on scalacache * improve caching docs
- Loading branch information
Showing
5 changed files
with
171 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
--- | ||
sidebar_position: 6 | ||
description: Caching | ||
--- | ||
|
||
# Caching | ||
|
||
The sttp-oauth2 library comes with `CachingAccessTokenProvider` and `CachingTokenIntrospection` - interfaces that allow caching the responses provided by the OAuth2 provider. Both of those require an implementation of the `ExpiringCache` algebra, defined as follows: | ||
|
||
```scala | ||
trait ExpiringCache[F[_], K, V] { | ||
def get(key: K): F[Option[V]] | ||
|
||
def put(key: K, value: V, expirationTime: Instant): F[Unit] | ||
|
||
def remove(key: K): F[Unit] | ||
} | ||
``` | ||
|
||
As the user of the library you can either choose to implement your own cache mechanism, or go for one of the provided: | ||
|
||
| Class |Description | Import module | | ||
|---------------------------|-------------------------------------------------------------|-------------------| | ||
| `CatsRefExpiringCache` | Simple Cats Effect 3 Ref based implementation. Good enough for `CachingAccessTokenProvider`, but for `CachingTokenIntrospection` it's recommended to use an instance which better handles memory (this instance does not periodically remove expired entries) | `"com.ocadotechnology" %% "sttp-oauth2-cache-cats" % "@VERSION@"` | | ||
| `CatsRefExpiringCache` | Simple Cats Effect 2 Ref based implementation. Good enough for `CachingAccessTokenProvider`, but for `CachingTokenIntrospection` it's recommended to use an instance which better handles memory (this instance does not periodically remove expired entries) | `"com.ocadotechnology" %% "sttp-oauth2-cache-ce2" % "@VERSION@"` | | ||
| `ScalacacheExpiringCache` | Implementation based on https://github.com/cb372/scalacache | `"com.ocadotechnology" %% "sttp-oauth2-cache-scalacache" % "@VERSION@"` | | ||
| `MonixFutureCache` | Future based implementation powered by [Monix](https://monix.io/) | `"com.ocadotechnology" %% "sttp-oauth2-cache-future" % "@VERSION@"` | | ||
|
40 changes: 40 additions & 0 deletions
40
...main/scala/com/ocadotechnology/sttp/oauth2/cache/scalacache/ScalacacheExpiringCache.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.ocadotechnology.sttp.oauth2.cache.scalacache | ||
|
||
import cats.effect.Async | ||
import cats.effect.kernel.Clock | ||
import cats.implicits._ | ||
import com.ocadotechnology.sttp.oauth2.cache.ExpiringCache | ||
import scalacache._ | ||
|
||
import java.time.Instant | ||
import java.util.concurrent.TimeUnit | ||
import scala.concurrent.duration.FiniteDuration | ||
|
||
final class ScalacacheExpiringCache[F[_]: Async, K, V](cache: Cache[F, K, V]) extends ExpiringCache[F, K, V] { | ||
|
||
override def get(key: K): F[Option[V]] = | ||
cache.get(key) | ||
|
||
override def put(key: K, value: V, expirationTime: Instant): F[Unit] = | ||
for { | ||
now <- Clock[F].realTimeInstant | ||
ttl = calculateTTL(expirationTime, now) | ||
_ <- cache.put(key)(value, Some(ttl)).void | ||
} yield () | ||
|
||
override def remove(key: K): F[Unit] = | ||
cache.remove(key).void | ||
|
||
private def calculateTTL(expirationTime: Instant, now: Instant): FiniteDuration = | ||
if (expirationTime isAfter now) | ||
FiniteDuration(expirationTime.toEpochMilli() - now.toEpochMilli(), TimeUnit.MILLISECONDS) | ||
else FiniteDuration(0, TimeUnit.MILLISECONDS) | ||
|
||
} | ||
|
||
object ScalacacheExpiringCache { | ||
|
||
def apply[F[_]: Async, K, V](cache: Cache[F, K, V]): ExpiringCache[F, K, V] = | ||
new ScalacacheExpiringCache[F, K, V](cache) | ||
|
||
} |
80 changes: 80 additions & 0 deletions
80
.../scala/com/ocadotechnology/sttp/oauth2/cache/scalacache/ScalacacheExpiringCacheSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package com.ocadotechnology.sttp.oauth2.cache.scalacache | ||
|
||
import cats.effect.IO | ||
import cats.effect.kernel.Outcome.Succeeded | ||
import cats.effect.testkit.TestContext | ||
import cats.effect.testkit.TestInstances | ||
import org.scalatest.Assertion | ||
import org.scalatest.matchers.should.Matchers | ||
import org.scalatest.wordspec.AnyWordSpec | ||
|
||
import scalacache.caffeine._ | ||
|
||
import scala.concurrent.duration._ | ||
|
||
class ScalacacheExpiringCacheSpec extends AnyWordSpec with Matchers with TestInstances { | ||
private implicit val ticker: Ticker = Ticker(TestContext()) | ||
|
||
private val someKey = "key" | ||
private val someValue = 1 | ||
|
||
def runTest(test: IO[Assertion]): Assertion = | ||
unsafeRun(test) match { | ||
case Succeeded(Some(assertion)) => assertion | ||
case wrongResult => fail(s"Test should finish successfully. Instead ended with $wrongResult") | ||
} | ||
|
||
"Cache" should { | ||
"return nothing on empty cache" in unsafeRun { | ||
for { | ||
cacheBackend <- CaffeineCache[IO, String, Int] | ||
cache = ScalacacheExpiringCache[IO, String, Int](cacheBackend) | ||
value <- cache.get(someKey) | ||
} yield value | ||
}.shouldBe(Succeeded(Some(None))) | ||
|
||
"store and retrieve value immediately" in unsafeRun { | ||
for { | ||
cacheBackend <- CaffeineCache[IO, String, Int] | ||
cache = ScalacacheExpiringCache[IO, String, Int](cacheBackend) | ||
now <- IO.realTimeInstant | ||
_ <- cache.put(someKey, someValue, now.plusSeconds(60)) | ||
value <- cache.get(someKey) | ||
} yield value | ||
}.shouldBe(Succeeded(Some(Some(someValue)))) | ||
|
||
"return value right before expiration boundary" in unsafeRun { | ||
for { | ||
cacheBackend <- CaffeineCache[IO, String, Int] | ||
cache = ScalacacheExpiringCache[IO, String, Int](cacheBackend) | ||
now <- IO.realTimeInstant | ||
_ <- cache.put(someKey, someValue, now.plusSeconds(60)) | ||
_ <- IO.sleep(60.seconds - 1.nano) | ||
value <- cache.get(someKey) | ||
} yield value | ||
}.shouldBe(Succeeded(Some(Some(someValue)))) | ||
|
||
"not return value if expired" in unsafeRun { | ||
for { | ||
cacheBackend <- CaffeineCache[IO, String, Int] | ||
cache = ScalacacheExpiringCache[IO, String, Int](cacheBackend) | ||
now <- IO.realTimeInstant | ||
_ <- cache.put(someKey, someValue, now.plusSeconds(60)) | ||
_ <- IO.sleep(60.seconds) | ||
value <- cache.get(someKey) | ||
} yield value | ||
}.shouldBe(Succeeded(Some(None))) | ||
|
||
"remove value when expired" in unsafeRun { | ||
for { | ||
cacheBackend <- CaffeineCache[IO, String, Int] | ||
cache = ScalacacheExpiringCache[IO, String, Int](cacheBackend) | ||
now <- IO.realTimeInstant | ||
_ <- cache.put(someKey, someValue, now.plusSeconds(1)) | ||
_ <- IO.sleep(3.seconds) | ||
value <- cache.get(someKey) | ||
} yield value | ||
}.shouldBe(Succeeded(Some(None))) | ||
|
||
} | ||
} |