-
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.
* ZIO support * Build process fixes --------- Co-authored-by: Fristi <[email protected]>
- Loading branch information
Showing
11 changed files
with
398 additions
and
22 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ jobs: | |
strategy: | ||
matrix: | ||
os: [ubuntu-latest] | ||
scala: [2.12.19, 2.13.13, 3.2.2] | ||
scala: [2.12.19, 2.13.13, 3.3.3] | ||
java: [[email protected]] | ||
runs-on: ${{ matrix.os }} | ||
steps: | ||
|
@@ -59,7 +59,7 @@ jobs: | |
- run: sbt ++${{ matrix.scala }} test docs/mdoc mimaReportBinaryIssues | ||
|
||
- name: Compress target directories | ||
run: tar cf targets.tar oauth2-jsoniter/jvm/target oauth2/js/target oauth2-cache/js/target oauth2-cache-ce2/target oauth2-jsoniter/js/target target oauth2-cache-scalacache/target mdoc/target oauth2-circe/jvm/target oauth2-cache-cats/target oauth2-cache-future/jvm/target oauth2-circe/js/target oauth2-cache/jvm/target oauth2-cache-future/js/target oauth2/jvm/target project/target | ||
run: tar cf targets.tar oauth2-jsoniter/jvm/target oauth2/js/target oauth2-cache/js/target oauth2-cache-ce2/target oauth2-cache-zio/target oauth2-jsoniter/js/target target oauth2-cache-scalacache/target mdoc/target oauth2-circe/jvm/target oauth2-cache-cats/target oauth2-cache-future/jvm/target oauth2-circe/js/target oauth2-cache/jvm/target oauth2-cache-future/js/target oauth2/jvm/target project/target | ||
|
||
- name: Upload target directories | ||
uses: actions/upload-artifact@v2 | ||
|
@@ -120,12 +120,12 @@ jobs: | |
tar xf targets.tar | ||
rm targets.tar | ||
- name: Download target directories (3.2.2) | ||
- name: Download target directories (3.3.3) | ||
uses: actions/download-artifact@v2 | ||
with: | ||
name: target-${{ matrix.os }}-3.2.2-${{ matrix.java }} | ||
name: target-${{ matrix.os }}-3.3.3-${{ matrix.java }} | ||
|
||
- name: Inflate target directories (3.2.2) | ||
- name: Inflate target directories (3.3.3) | ||
run: | | ||
tar xf targets.tar | ||
rm targets.tar | ||
|
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 |
---|---|---|
|
@@ -21,7 +21,7 @@ def crossPlugin(x: sbt.librarymanagement.ModuleID) = compilerPlugin(x.cross(Cros | |
|
||
val Scala212 = "2.12.19" | ||
val Scala213 = "2.13.13" | ||
val Scala3 = "3.2.2" | ||
val Scala3 = "3.3.3" | ||
|
||
val GraalVM11 = "[email protected]" | ||
|
||
|
@@ -62,7 +62,11 @@ val Versions = new { | |
|
||
def compilerPlugins = | ||
libraryDependencies ++= (if (scalaVersion.value.startsWith("3")) Seq() | ||
else Seq(compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"))) | ||
else | ||
Seq( | ||
compilerPlugin("org.typelevel" % "kind-projector" % "0.13.3" cross CrossVersion.full), | ||
compilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1") | ||
)) | ||
|
||
val mimaSettings = | ||
// revert the commit that made this change after releasing a new version | ||
|
@@ -79,9 +83,6 @@ val mimaSettings = | |
// } | ||
mimaPreviousArtifacts := Set.empty | ||
|
||
// Workaround for https://github.com/typelevel/sbt-tpolecat/issues/102 | ||
val jsSettings = scalacOptions ++= (if (scalaVersion.value.startsWith("3")) Seq("-scalajs") else Seq()) | ||
|
||
lazy val oauth2 = crossProject(JSPlatform, JVMPlatform) | ||
.withoutSuffixFor(JVMPlatform) | ||
.settings( | ||
|
@@ -96,8 +97,7 @@ lazy val oauth2 = crossProject(JSPlatform, JVMPlatform) | |
compilerPlugins | ||
) | ||
.jsSettings( | ||
libraryDependencies ++= Seq("org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0"), | ||
jsSettings | ||
libraryDependencies ++= Seq("org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0") | ||
) | ||
|
||
lazy val `oauth2-circe` = crossProject(JSPlatform, JVMPlatform) | ||
|
@@ -113,9 +113,6 @@ lazy val `oauth2-circe` = crossProject(JSPlatform, JVMPlatform) | |
mimaSettings, | ||
compilerPlugins | ||
) | ||
.jsSettings( | ||
jsSettings | ||
) | ||
.dependsOn(oauth2 % "compile->compile;test->test") | ||
|
||
lazy val `oauth2-jsoniter` = crossProject(JSPlatform, JVMPlatform) | ||
|
@@ -131,16 +128,15 @@ lazy val `oauth2-jsoniter` = crossProject(JSPlatform, JVMPlatform) | |
compilerPlugins, | ||
scalacOptions ++= Seq("-Wconf:cat=deprecation:info") // jsoniter-scala macro-generated code uses deprecated methods | ||
) | ||
.jsSettings( | ||
jsSettings | ||
) | ||
.dependsOn(oauth2 % "compile->compile;test->test") | ||
|
||
lazy val docs = project | ||
.in(file("mdoc")) // important: it must not be docs/ | ||
.settings( | ||
mdocVariables := Map( | ||
"VERSION" -> { if (isSnapshot.value) previousStableVersion.value.get else version.value } | ||
"VERSION" -> { | ||
if (isSnapshot.value) previousStableVersion.value.get else version.value | ||
} | ||
) | ||
) | ||
.dependsOn(oauth2.jvm) | ||
|
@@ -153,7 +149,6 @@ lazy val `oauth2-cache` = crossProject(JSPlatform, JVMPlatform) | |
mimaSettings, | ||
compilerPlugins | ||
) | ||
.jsSettings(jsSettings) | ||
.dependsOn(oauth2) | ||
|
||
// oauth2-cache-scalacache doesn't have JS support because scalacache doesn't compile for js https://github.com/cb372/scalacache/issues/354#issuecomment-913024231 | ||
|
@@ -204,6 +199,24 @@ lazy val `oauth2-cache-ce2` = project | |
) | ||
.dependsOn(`oauth2-cache`.jvm) | ||
|
||
lazy val `oauth2-cache-zio` = project | ||
.settings( | ||
name := "sttp-oauth2-cache-zio", | ||
libraryDependencies ++= Seq( | ||
"dev.zio" %% "zio" % "2.1.1", | ||
"dev.zio" %% "zio-test" % "2.1.1" % Test, | ||
"dev.zio" %% "zio-test-sbt" % "2.1.1" % Test | ||
), | ||
mimaSettings, | ||
compilerPlugins, | ||
scalacOptions -= "-Ykind-projector", | ||
scalacOptions ++= ( | ||
if (scalaVersion.value.startsWith("3")) Seq("-Ykind-projector:underscores") | ||
else Seq("-P:kind-projector:underscore-placeholders") | ||
) | ||
) | ||
.dependsOn(`oauth2-cache`.jvm) | ||
|
||
lazy val `oauth2-cache-future` = crossProject(JSPlatform, JVMPlatform) | ||
.withoutSuffixFor(JVMPlatform) | ||
.settings( | ||
|
@@ -215,7 +228,6 @@ lazy val `oauth2-cache-future` = crossProject(JSPlatform, JVMPlatform) | |
mimaSettings, | ||
compilerPlugins | ||
) | ||
.jsSettings(jsSettings) | ||
.dependsOn(`oauth2-cache`) | ||
|
||
val root = project | ||
|
@@ -232,6 +244,7 @@ val root = project | |
`oauth2-cache`.js, | ||
`oauth2-cache-cats`, | ||
`oauth2-cache-ce2`, | ||
`oauth2-cache-zio`, | ||
`oauth2-cache-future`.jvm, | ||
`oauth2-cache-future`.js, | ||
`oauth2-cache-scalacache`, | ||
|
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
89 changes: 89 additions & 0 deletions
89
...la/org/polyvariant/ocadotechnology/sttp/oauth2/cache/zio/CachingAccessTokenProvider.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,89 @@ | ||
package org.polyvariant.sttp.oauth2.cache.zio | ||
|
||
import org.polyvariant.sttp.oauth2.AccessTokenProvider | ||
import org.polyvariant.sttp.oauth2.ClientCredentialsToken | ||
import org.polyvariant.sttp.oauth2.Secret | ||
import org.polyvariant.sttp.oauth2.cache.ExpiringCache | ||
import org.polyvariant.sttp.oauth2.cache.zio.CachingAccessTokenProvider.TokenWithExpirationTime | ||
import org.polyvariant.sttp.oauth2.common.Scope | ||
import zio.Clock | ||
import zio.Semaphore | ||
import zio._ | ||
|
||
import java.time.Instant | ||
import scala.concurrent.duration.Duration | ||
|
||
final class CachingAccessTokenProvider[R]( | ||
delegate: AccessTokenProvider[RIO[R, _]], | ||
semaphore: Semaphore, | ||
tokenCache: ExpiringCache[RIO[R, _], Option[Scope], TokenWithExpirationTime] | ||
) extends AccessTokenProvider[RIO[R, _]] { | ||
|
||
override def requestToken(scope: Option[Scope]): RIO[R, ClientCredentialsToken.AccessTokenResponse] = | ||
getFromCache(scope).flatMap { | ||
case Some(value) => ZIO.succeed(value) | ||
case None => semaphore.withPermit(acquireToken(scope)) | ||
} | ||
|
||
private def acquireToken(scope: Option[Scope]): ZIO[R, Throwable, ClientCredentialsToken.AccessTokenResponse] = | ||
getFromCache(scope).flatMap { | ||
case Some(value) => ZIO.succeed(value) | ||
case None => fetchAndSaveToken(scope) | ||
} | ||
|
||
private def getFromCache(scope: Option[Scope]) = | ||
tokenCache.get(scope).flatMap { entry => | ||
Clock.instant.map { now => | ||
entry match { | ||
case Some(value) => Some(value.toAccessTokenResponse(now)) | ||
case None => None | ||
} | ||
} | ||
} | ||
|
||
private def fetchAndSaveToken(scope: Option[Scope]) = | ||
for { | ||
token <- delegate.requestToken(scope) | ||
tokenWithExpiry <- calculateExpiryInstant(token) | ||
_ <- tokenCache.put(scope, tokenWithExpiry, tokenWithExpiry.expirationTime) | ||
} yield token | ||
|
||
private def calculateExpiryInstant(response: ClientCredentialsToken.AccessTokenResponse) = | ||
Clock.instant.map(TokenWithExpirationTime.from(response, _)) | ||
|
||
} | ||
|
||
object CachingAccessTokenProvider { | ||
|
||
def apply[R]( | ||
delegate: AccessTokenProvider[RIO[R, _]], | ||
tokenCache: ExpiringCache[RIO[R, _], Option[Scope], TokenWithExpirationTime] | ||
): RIO[R, CachingAccessTokenProvider[R]] = Semaphore.make(permits = 1).map(new CachingAccessTokenProvider(delegate, _, tokenCache)) | ||
|
||
def refCacheInstance(delegate: AccessTokenProvider[Task]): Task[CachingAccessTokenProvider[Any]] = | ||
ZioRefExpiringCache[Option[Scope], TokenWithExpirationTime].flatMap(CachingAccessTokenProvider(delegate, _)) | ||
|
||
final case class TokenWithExpirationTime( | ||
accessToken: Secret[String], | ||
domain: Option[String], | ||
expirationTime: Instant, | ||
scope: Option[Scope] | ||
) { | ||
|
||
def toAccessTokenResponse(now: Instant): ClientCredentialsToken.AccessTokenResponse = { | ||
val newExpiresIn = Duration.fromNanos(java.time.Duration.between(now, expirationTime).toNanos) | ||
ClientCredentialsToken.AccessTokenResponse(accessToken, domain, newExpiresIn, scope) | ||
} | ||
|
||
} | ||
|
||
object TokenWithExpirationTime { | ||
|
||
def from(token: ClientCredentialsToken.AccessTokenResponse, now: Instant): TokenWithExpirationTime = { | ||
val expirationTime = now.plusNanos(token.expiresIn.toNanos) | ||
TokenWithExpirationTime(token.accessToken, token.domain, expirationTime, token.scope) | ||
} | ||
|
||
} | ||
|
||
} |
35 changes: 35 additions & 0 deletions
35
...ain/scala/org/polyvariant/ocadotechnology/sttp/oauth2/cache/zio/ZioRefExpiringCache.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,35 @@ | ||
package org.polyvariant.sttp.oauth2.cache.zio | ||
|
||
import org.polyvariant.sttp.oauth2.cache.ExpiringCache | ||
import org.polyvariant.sttp.oauth2.cache.zio.ZioRefExpiringCache.Entry | ||
import zio.Clock | ||
import zio.Ref | ||
import zio.Task | ||
import zio.ZIO | ||
|
||
import java.time.Instant | ||
|
||
final class ZioRefExpiringCache[K, V] private (ref: Ref[Map[K, Entry[V]]]) extends ExpiringCache[Task, K, V] { | ||
|
||
override def get(key: K): Task[Option[V]] = | ||
ref.get.map(_.get(key)).flatMap { entry => | ||
Clock.instant.flatMap { now => | ||
(entry, now) match { | ||
case (Some(Entry(value, expiryInstant)), now) => | ||
if (now.isBefore(expiryInstant)) ZIO.succeed(Some(value)) else remove(key).as(None) | ||
case _ => | ||
ZIO.none | ||
} | ||
} | ||
} | ||
|
||
override def put(key: K, value: V, expirationTime: Instant): Task[Unit] = ref.update(_ + (key -> Entry(value, expirationTime))) | ||
|
||
override def remove(key: K): Task[Unit] = ref.update(_ - key) | ||
} | ||
|
||
object ZioRefExpiringCache { | ||
private final case class Entry[V](value: V, expirationTime: Instant) | ||
|
||
def apply[K, V]: Task[ExpiringCache[Task, K, V]] = Ref.make(Map.empty[K, Entry[V]]).map(new ZioRefExpiringCache(_)) | ||
} |
72 changes: 72 additions & 0 deletions
72
...ariant/ocadotechnology/sttp/oauth2/cache/zio/CachingAccessTokenProviderParallelSpec.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,72 @@ | ||
package org.polyvariant.sttp.oauth2.cache.zio | ||
|
||
import org.polyvariant.sttp.oauth2.ClientCredentialsToken.AccessTokenResponse | ||
import org.polyvariant.sttp.oauth2.Secret | ||
import org.polyvariant.sttp.oauth2.cache.ExpiringCache | ||
import org.polyvariant.sttp.oauth2.cache.zio.CachingAccessTokenProvider.TokenWithExpirationTime | ||
import org.polyvariant.sttp.oauth2.common.Scope | ||
import zio.test._ | ||
import zio.{Duration => ZDuration} | ||
import zio.Ref | ||
import zio.Task | ||
import zio.ZIO | ||
|
||
import java.time.Instant | ||
import scala.concurrent.duration._ | ||
|
||
object CachingAccessTokenProviderParallelSpec extends ZIOSpecDefault { | ||
|
||
private val testScope: Option[Scope] = Scope.of("test-scope") | ||
private val token = AccessTokenResponse(Secret("secret"), None, 10.seconds, testScope) | ||
|
||
private val sleepDuration: FiniteDuration = 1.second | ||
|
||
def spec = suite("CachingAccessTokenProvider")( | ||
test("block multiple parallel") { | ||
prepareTest.flatMap { case (delegate, cachingProvider) => | ||
delegate.setToken(testScope, token) *> | ||
(cachingProvider.requestToken(testScope) zipPar cachingProvider.requestToken(testScope)).map { case (result1, result2) => | ||
assert(result1)(Assertion.equalTo(token.copy(expiresIn = result1.expiresIn))) && | ||
assert(result2)(Assertion.equalTo(token.copy(expiresIn = result2.expiresIn))) && | ||
// if both calls would be made in parallel, both would get the same expiresIn from TestAccessTokenProvider. | ||
// When blocking is in place, the second call would be delayed by sleepDuration and would hit the cache, | ||
// which has Instant on top of which new expiresIn would be calculated | ||
assert(diffInExpirations(result1, result2))(Assertion.isGreaterThanEqualTo(sleepDuration)) | ||
} | ||
} | ||
}, | ||
test("not block multiple parallel access if its already in cache") { | ||
prepareTest.flatMap { case (delegate, cachingProvider) => | ||
delegate.setToken(testScope, token) *> cachingProvider.requestToken(testScope) *> | ||
(cachingProvider.requestToken(testScope) zipPar cachingProvider.requestToken(testScope)) map { case (result1, result2) => | ||
assert(result1)(Assertion.equalTo(token.copy(expiresIn = result1.expiresIn))) && | ||
assert(result2)(Assertion.equalTo(token.copy(expiresIn = result2.expiresIn))) && | ||
// second call should not be forced to wait sleepDuration, because some active token is already in cache | ||
assert(diffInExpirations(result1, result2))(Assertion.isLessThan(sleepDuration)) | ||
} | ||
} | ||
} | ||
) @@ TestAspect.withLiveEnvironment | ||
|
||
private def diffInExpirations(result1: AccessTokenResponse, result2: AccessTokenResponse) = | ||
if (result1.expiresIn > result2.expiresIn) result1.expiresIn - result2.expiresIn else result2.expiresIn - result1.expiresIn | ||
|
||
class DelayingCache[K, V](delegate: ExpiringCache[Task, K, V]) extends ExpiringCache[Task, K, V] { | ||
override def get(key: K): Task[Option[V]] = delegate.get(key) | ||
|
||
override def put(key: K, value: V, expirationTime: Instant): Task[Unit] = | ||
ZIO.sleep(ZDuration.fromScala(sleepDuration)) *> delegate.put(key, value, expirationTime) | ||
|
||
override def remove(key: K): Task[Unit] = delegate.remove(key) | ||
} | ||
|
||
private def prepareTest = | ||
for { | ||
state <- Ref.make[TestAccessTokenProvider.State](TestAccessTokenProvider.State.empty) | ||
delegate = TestAccessTokenProvider(state) | ||
cache <- ZioRefExpiringCache[Option[Scope], TokenWithExpirationTime] | ||
delayingCache = new DelayingCache(cache) | ||
cachingProvider <- CachingAccessTokenProvider(delegate, delayingCache) | ||
} yield (delegate, cachingProvider) | ||
|
||
} |
Oops, something went wrong.