├── .github ├── release-drafter.yml └── workflows │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .jvmopts ├── .mergify.yml ├── .scalafmt.conf ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.sbt ├── docker-compose.yml ├── modules ├── benchmarks │ └── src │ │ ├── main │ │ └── scala │ │ │ └── scalacache │ │ │ └── benchmark │ │ │ └── CaffeineBenchmark.scala │ │ └── test │ │ └── scala │ │ └── scalacache │ │ └── benchmark │ │ └── ProfilingMemoize.scala ├── caffeine │ └── src │ │ ├── main │ │ └── scala │ │ │ └── scalacache │ │ │ └── caffeine │ │ │ └── CaffeineCache.scala │ │ └── test │ │ └── scala │ │ └── scalacache │ │ └── caffeine │ │ └── CaffeineCacheSpec.scala ├── circe │ └── src │ │ ├── main │ │ └── scala │ │ │ └── scalacache │ │ │ └── serialization │ │ │ └── Circe.scala │ │ └── test │ │ └── scala │ │ └── scalacache │ │ └── serialization │ │ └── CirceCodecSpec.scala ├── core │ └── src │ │ ├── main │ │ ├── scala-2 │ │ │ └── scalacache │ │ │ │ └── memoization │ │ │ │ ├── Macros.scala │ │ │ │ └── package.scala │ │ ├── scala-3 │ │ │ └── scalacache │ │ │ │ └── memoization │ │ │ │ ├── Macros.scala │ │ │ │ └── package.scala │ │ └── scala │ │ │ └── scalacache │ │ │ ├── AbstractCache.scala │ │ │ ├── Cache.scala │ │ │ ├── Entry.scala │ │ │ ├── Flags.scala │ │ │ ├── HashingAlgorithm.scala │ │ │ ├── LoggingSupport.scala │ │ │ ├── logging │ │ │ └── Logger.scala │ │ │ ├── memoization │ │ │ ├── MemoizationConfig.scala │ │ │ ├── MethodCallToStringConverter.scala │ │ │ └── annotations.scala │ │ │ ├── package.scala │ │ │ ├── serialization │ │ │ ├── Codec.scala │ │ │ ├── FailedToDecode.scala │ │ │ ├── GenericCodecObjectInputStream.scala │ │ │ ├── binary │ │ │ │ ├── BinaryAnyRefCodecs.scala │ │ │ │ ├── BinaryCodec.scala │ │ │ │ ├── BinaryPrimitiveCodecs.scala │ │ │ │ ├── JavaSerializationAnyRefCodec.scala │ │ │ │ └── package.scala │ │ │ └── gzip │ │ │ │ ├── GZippingJavaSerializationAnyRefCodec.scala │ │ │ │ └── GzippingBinaryCodec.scala │ │ │ └── syntax.scala │ │ └── test │ │ └── scala │ │ ├── issue42 │ │ └── Issue42Spec.scala │ │ ├── sample │ │ └── Sample.scala │ │ └── scalacache │ │ ├── AbstractCacheSpec.scala │ │ ├── memoization │ │ ├── CacheKeyExcludingConstructorParamsSpec.scala │ │ ├── CacheKeyIncludingConstructorParamsSpec.scala │ │ ├── CacheKeyIncludingOnlyMethodParamsSpec.scala │ │ ├── CacheKeySpecCommon.scala │ │ ├── MemoizeSpec.scala │ │ ├── MethodCallToStringConverterSpec.scala │ │ └── pkg │ │ │ └── package.scala │ │ └── mocks.scala ├── docs │ └── src │ │ └── main │ │ ├── mdoc │ │ ├── docs │ │ │ ├── cache-implementations.md │ │ │ ├── flags.md │ │ │ ├── index.md │ │ │ ├── memoization.md │ │ │ ├── restrictions.md │ │ │ └── serialization.md │ │ └── index.md │ │ └── resources │ │ └── microsite │ │ └── data │ │ └── menu.yml ├── memcached │ └── src │ │ ├── main │ │ └── scala │ │ │ └── scalacache │ │ │ └── memcached │ │ │ ├── MemcachedCache.scala │ │ │ ├── MemcachedKeySanitizer.scala │ │ │ └── MemcachedTTLConverter.scala │ │ └── test │ │ └── scala │ │ └── scalacache │ │ └── memcached │ │ ├── MemcachedCacheSpec.scala │ │ ├── MemcachedKeySanitizerSpec.scala │ │ └── MemcachedTTLConverterSpec.scala ├── redis │ └── src │ │ ├── main │ │ └── scala │ │ │ └── scalacache │ │ │ └── redis │ │ │ ├── RedisCache.scala │ │ │ ├── RedisCacheBase.scala │ │ │ ├── RedisClusterCache.scala │ │ │ ├── RedisSerialization.scala │ │ │ ├── SentinelRedisCache.scala │ │ │ └── ShardedRedisCache.scala │ │ └── test │ │ └── scala │ │ └── scalacache │ │ └── redis │ │ ├── CaseClass.scala │ │ ├── Issue32Spec.scala │ │ ├── RedisCacheSpec.scala │ │ ├── RedisCacheSpecBase.scala │ │ ├── RedisClusterCacheSpec.scala │ │ ├── RedisSerializationSpec.scala │ │ ├── RedisTestUtil.scala │ │ ├── SentinelRedisCacheSpec.scala │ │ ├── ShardedRedisCacheSpec.scala │ │ └── jedisWrappers.scala └── tests │ └── src │ └── test │ └── scala │ └── integrationtests │ └── IntegrationTests.scala └── project ├── build.properties ├── build.sbt └── plugins.sbt /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | 6 | exclude-labels: 7 | - 'skip-changelog' 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**'] 13 | push: 14 | branches: ['**'] 15 | tags: [v*] 16 | 17 | env: 18 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 19 | GPG_TTY: $(tty) 20 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 21 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | jobs: 25 | build: 26 | name: Build and Test 27 | strategy: 28 | matrix: 29 | os: [ubuntu-latest] 30 | scala: [2.13.7, 2.12.15, 3.1.0] 31 | java: [openjdk@1.11.0] 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - name: Checkout current branch (full) 35 | uses: actions/checkout@v2 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Setup Java and Scala 40 | uses: olafurpg/setup-scala@v13 41 | with: 42 | java-version: ${{ matrix.java }} 43 | 44 | - name: Cache sbt 45 | uses: actions/cache@v2 46 | with: 47 | path: | 48 | ~/.sbt 49 | ~/.ivy2/cache 50 | ~/.coursier/cache/v1 51 | ~/.cache/coursier/v1 52 | ~/AppData/Local/Coursier/Cache/v1 53 | ~/Library/Caches/Coursier/v1 54 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 55 | 56 | - name: Check that workflows are up to date 57 | run: sbt ++${{ matrix.scala }} githubWorkflowCheck 58 | 59 | - name: Check Formatting 60 | run: sbt ++${{ matrix.scala }} scalafmtCheckAll 61 | 62 | - name: Setup Dependencies 63 | run: docker-compose up -d 64 | 65 | - name: Run ci task from sbt-spiewak 66 | run: sbt ++${{ matrix.scala }} ci 67 | 68 | - name: Run unidoc 69 | if: matrix.scala == '2.13.7' 70 | run: sbt ++${{ matrix.scala }} unidoc 71 | 72 | - name: Compile Docs 73 | run: sbt ++${{ matrix.scala }} docs/mdoc 74 | 75 | - name: Compress target directories 76 | run: tar cf targets.tar target modules/docs/target modules/tests/target modules/caffeine/target modules/memcached/target modules/circe/target modules/redis/target modules/core/target project/target 77 | 78 | - name: Upload target directories 79 | uses: actions/upload-artifact@v2 80 | with: 81 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 82 | path: targets.tar 83 | 84 | publish: 85 | name: Publish Artifacts 86 | needs: [build] 87 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master') 88 | strategy: 89 | matrix: 90 | os: [ubuntu-latest] 91 | scala: [2.13.7] 92 | java: [openjdk@1.11.0] 93 | runs-on: ${{ matrix.os }} 94 | steps: 95 | - name: Checkout current branch (full) 96 | uses: actions/checkout@v2 97 | with: 98 | fetch-depth: 0 99 | 100 | - name: Setup Java and Scala 101 | uses: olafurpg/setup-scala@v13 102 | with: 103 | java-version: ${{ matrix.java }} 104 | 105 | - name: Cache sbt 106 | uses: actions/cache@v2 107 | with: 108 | path: | 109 | ~/.sbt 110 | ~/.ivy2/cache 111 | ~/.coursier/cache/v1 112 | ~/.cache/coursier/v1 113 | ~/AppData/Local/Coursier/Cache/v1 114 | ~/Library/Caches/Coursier/v1 115 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 116 | 117 | - name: Download target directories (2.13.7) 118 | uses: actions/download-artifact@v2 119 | with: 120 | name: target-${{ matrix.os }}-2.13.7-${{ matrix.java }} 121 | 122 | - name: Inflate target directories (2.13.7) 123 | run: | 124 | tar xf targets.tar 125 | rm targets.tar 126 | 127 | - name: Download target directories (2.12.15) 128 | uses: actions/download-artifact@v2 129 | with: 130 | name: target-${{ matrix.os }}-2.12.15-${{ matrix.java }} 131 | 132 | - name: Inflate target directories (2.12.15) 133 | run: | 134 | tar xf targets.tar 135 | rm targets.tar 136 | 137 | - name: Download target directories (3.1.0) 138 | uses: actions/download-artifact@v2 139 | with: 140 | name: target-${{ matrix.os }}-3.1.0-${{ matrix.java }} 141 | 142 | - name: Inflate target directories (3.1.0) 143 | run: | 144 | tar xf targets.tar 145 | rm targets.tar 146 | 147 | - name: Import signing key 148 | run: echo $PGP_SECRET | base64 -d | gpg --batch --import 149 | 150 | - run: sbt ++${{ matrix.scala }} release 151 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | *.iml 4 | *.ipr 5 | *.jfr 6 | target 7 | coveralls-token.txt 8 | /*.html 9 | dump.rdb 10 | logs 11 | .ensime 12 | .ensime_lucene 13 | .ensime_cache 14 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Xms4G 2 | -Xmx4G 3 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge for scala-steward PRs - patch 3 | conditions: 4 | - author=scala-steward 5 | - body~=labels:.*semver-patch 6 | - status-success~=Build and Test 7 | actions: 8 | merge: 9 | method: merge 10 | - name: Automatic merge for scala-steward PRs - minor 11 | conditions: 12 | - author=scala-steward 13 | - body~=labels:.*semver-minor 14 | - status-success~=Build and Test 15 | - "#approved-reviews-by>=1" 16 | actions: 17 | merge: 18 | method: merge -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | align.preset = "more" 2 | maxColumn = 120 3 | version = "3.1.2" 4 | runner.dialect = scala213 5 | fileOverride { 6 | "glob:**/src/main/scala-3/**" { 7 | runner.dialect = scala3 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013 Chris Birchall 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScalaCache 2 | 3 | [![Join the chat at https://gitter.im/cb372/scalacache](https://badges.gitter.im/cb372/scalacache.svg)](https://gitter.im/cb372/scalacache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![Build Status](https://github.com/cb372/scalacache/workflows/Continuous%20Integration/badge.svg)](https://github.com/cb372/scalacache/actions) [![Maven Central](https://img.shields.io/maven-central/v/com.github.cb372/scalacache-core_2.12.svg)](http://search.maven.org/#search%7Cga%7C1%7Cscalacache) 6 | 7 | A facade for the most popular cache implementations, with a simple, idiomatic Scala API. 8 | 9 | Use ScalaCache to add caching to any Scala app with the minimum of fuss. 10 | 11 | The following cache implementations are supported, and it's easy to plugin your own implementation: 12 | * Memcached 13 | * Redis 14 | * [Caffeine](https://github.com/ben-manes/caffeine) 15 | 16 | ## Documentation 17 | 18 | Documentation is available on [the ScalaCache website](https://cb372.github.io/scalacache/). 19 | 20 | ## Compatibility 21 | 22 | ScalaCache is available for Scala 2.11.x, 2.12.x, and 2.13.x. 23 | 24 | The JVM must be Java 8 or newer. 25 | 26 | ## Compiling the documentation 27 | 28 | To make a change to the documentation: 29 | 30 | 1. Make sure that memcached is running on localhost:11211 31 | 2. Perform the desired changes 32 | 3. Run `sbt doc/makeMicrosite` 33 | 34 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | inThisBuild( 2 | List( 3 | baseVersion := "1.0", 4 | organization := "com.github.cb372", 5 | organizationName := "scalacache", 6 | homepage := Some(url("https://github.com/cb372/scalacache")), 7 | licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), 8 | developers := List( 9 | Developer( 10 | "cb372", 11 | "Chris Birchall", 12 | "chris.birchall@gmail.com", 13 | url("https://twitter.com/cbirchall") 14 | ) 15 | ) 16 | ) 17 | ) 18 | 19 | val CatsEffectVersion = "3.3.14" 20 | 21 | scalafmtOnCompile in ThisBuild := true 22 | 23 | lazy val root: Project = Project(id = "scalacache", base = file(".")) 24 | .enablePlugins(SonatypeCiReleasePlugin) 25 | .enablePlugins(ScalaUnidocPlugin) 26 | .settings( 27 | commonSettings, 28 | publishArtifact := false 29 | ) 30 | .aggregate( 31 | core, 32 | memcached, 33 | redis, 34 | caffeine, 35 | circe, 36 | tests 37 | ) 38 | 39 | lazy val core = 40 | Project(id = "core", file("modules/core")) 41 | .settings(commonSettings) 42 | .settings( 43 | moduleName := "scalacache-core", 44 | libraryDependencies ++= Seq( 45 | "org.slf4j" % "slf4j-api" % "1.7.36", 46 | "org.typelevel" %% "cats-effect" % CatsEffectVersion, 47 | scalatest, 48 | scalacheck 49 | ) ++ (if (scalaVersion.value.startsWith("2.")) { 50 | Seq( 51 | "org.scala-lang" % "scala-reflect" % scalaVersion.value, 52 | "org.scala-lang.modules" %% "scala-collection-compat" % "2.6.0" 53 | ) 54 | } else Nil), 55 | coverageMinimum := 60, 56 | coverageFailOnMinimum := true 57 | ) 58 | 59 | def createModule(name: String) = 60 | Project(id = name, base = file(s"modules/$name")) 61 | .settings(commonSettings) 62 | .settings( 63 | moduleName := s"scalacache-$name", 64 | libraryDependencies += scalatest 65 | ) 66 | .dependsOn(core) 67 | 68 | lazy val memcached = createModule("memcached") 69 | .settings( 70 | libraryDependencies ++= Seq( 71 | "net.spy" % "spymemcached" % "2.12.3" 72 | ) 73 | ) 74 | 75 | lazy val redis = createModule("redis") 76 | .settings( 77 | libraryDependencies ++= Seq( 78 | "redis.clients" % "jedis" % "3.7.1" 79 | ), 80 | coverageMinimum := 56, 81 | coverageFailOnMinimum := true 82 | ) 83 | 84 | lazy val caffeine = createModule("caffeine") 85 | .settings( 86 | libraryDependencies ++= Seq( 87 | "com.github.ben-manes.caffeine" % "caffeine" % "3.0.6", 88 | "org.typelevel" %% "cats-effect-testkit" % CatsEffectVersion % Test, 89 | "com.google.code.findbugs" % "jsr305" % "3.0.2" % Provided 90 | ), 91 | coverageMinimum := 80, 92 | coverageFailOnMinimum := true 93 | ) 94 | 95 | lazy val circe = createModule("circe") 96 | .settings( 97 | libraryDependencies ++= Seq( 98 | "io.circe" %% "circe-core" % "0.14.1", 99 | "io.circe" %% "circe-parser" % "0.14.1", 100 | "io.circe" %% "circe-generic" % "0.14.1" % Test, 101 | scalacheck, 102 | scalatestplus 103 | ), 104 | coverageMinimum := 80, 105 | coverageFailOnMinimum := true 106 | ) 107 | 108 | lazy val tests = createModule("tests") 109 | .settings(publishArtifact := false) 110 | .dependsOn(caffeine, memcached, redis, circe) 111 | 112 | lazy val docs = createModule("docs") 113 | .enablePlugins(MicrositesPlugin) 114 | .settings( 115 | publishArtifact := false, 116 | micrositeName := "ScalaCache", 117 | micrositeAuthor := "Chris Birchall", 118 | micrositeDescription := "A facade for the most popular cache implementations, with a simple, idiomatic Scala API.", 119 | micrositeBaseUrl := "/scalacache", 120 | micrositeDocumentationUrl := "/scalacache/docs", 121 | micrositeHomepage := "https://github.com/cb372/scalacache", 122 | micrositeGithubOwner := "cb372", 123 | micrositeGithubRepo := "scalacache", 124 | micrositeGitterChannel := true, 125 | micrositeTwitterCreator := "@cbirchall", 126 | micrositeShareOnSocial := true, 127 | mdocIn := (sourceDirectory in Compile).value / "mdoc" 128 | ) 129 | .dependsOn( 130 | core, 131 | memcached, 132 | redis, 133 | caffeine, 134 | circe 135 | ) 136 | 137 | lazy val benchmarks = createModule("benchmarks") 138 | .enablePlugins(JmhPlugin) 139 | .settings( 140 | githubWorkflowArtifactUpload := false, 141 | publishArtifact := false, 142 | fork in (Compile, run) := true, 143 | javaOptions in Jmh ++= Seq("-server", "-Xms2G", "-Xmx2G", "-XX:+UseG1GC", "-XX:-UseBiasedLocking"), 144 | javaOptions in (Test, run) ++= Seq( 145 | "-XX:+UnlockCommercialFeatures", 146 | "-XX:+FlightRecorder", 147 | "-XX:StartFlightRecording=delay=20s,duration=60s,filename=memoize.jfr", 148 | "-server", 149 | "-Xms2G", 150 | "-Xmx2G", 151 | "-XX:+UseG1GC", 152 | "-XX:-UseBiasedLocking" 153 | ) 154 | ) 155 | .dependsOn(caffeine) 156 | 157 | lazy val scalatest = "org.scalatest" %% "scalatest" % "3.2.19" % Test 158 | 159 | lazy val scalacheck = "org.scalacheck" %% "scalacheck" % "1.15.4" % Test 160 | 161 | lazy val scalatestplus = "org.scalatestplus" %% "scalacheck-1-15" % "3.2.10.0" % Test 162 | 163 | lazy val commonSettings = 164 | mavenSettings ++ 165 | Seq( 166 | organization := "com.github.cb372", 167 | scalacOptions ++= Seq("-language:higherKinds", "-language:postfixOps"), 168 | parallelExecution in Test := false 169 | ) 170 | 171 | lazy val mavenSettings = Seq( 172 | publishArtifact in Test := false, 173 | pomIncludeRepository := { _ => 174 | false 175 | } 176 | ) 177 | 178 | val Scala30 = "3.1.0" 179 | val Scala213 = "2.13.7" 180 | val Scala212 = "2.12.15" 181 | val Jdk11 = "openjdk@1.11.0" 182 | 183 | ThisBuild / scalaVersion := Scala213 184 | ThisBuild / crossScalaVersions := Seq(Scala213, Scala212, Scala30) 185 | ThisBuild / githubWorkflowJavaVersions := Seq(Jdk11) 186 | ThisBuild / githubWorkflowBuild := Seq( 187 | WorkflowStep.Sbt(List("scalafmtCheckAll"), name = Some("Check Formatting")), 188 | WorkflowStep.Run(List("docker-compose up -d"), name = Some("Setup Dependencies")), 189 | WorkflowStep.Sbt(List("ci"), name = Some("Run ci task from sbt-spiewak")), 190 | WorkflowStep.Sbt(List("unidoc"), name = Some("Run unidoc"), cond = Some(s"matrix.scala == '$Scala213'")), 191 | WorkflowStep.Sbt(List("docs/mdoc"), name = Some("Compile Docs")) 192 | ) 193 | ThisBuild / githubWorkflowEnv += ("GPG_TTY" -> "$(tty)") 194 | ThisBuild / githubWorkflowPublishPreamble := Seq( 195 | WorkflowStep.Run( 196 | List("echo $PGP_SECRET | base64 -d | gpg --batch --import"), 197 | name = Some("Import signing key") 198 | ) 199 | ) 200 | ThisBuild / spiewakCiReleaseSnapshots := true 201 | ThisBuild / spiewakMainBranches := Seq("master") 202 | ThisBuild / autoAPIMappings := true 203 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | redis-cluster: 4 | image: grokzen/redis-cluster:latest 5 | ports: 6 | - '7000-7005:7000-7005' 7 | - '6379:7006' 8 | - '6380:7007' 9 | environment: 10 | IP: 0.0.0.0 11 | STANDALONE: 'true' 12 | memcached: 13 | image: memcached:latest 14 | ports: 15 | - '11211:11211' 16 | -------------------------------------------------------------------------------- /modules/benchmarks/src/main/scala/scalacache/benchmark/CaffeineBenchmark.scala: -------------------------------------------------------------------------------- 1 | package scalacache.benchmark 2 | 3 | import org.openjdk.jmh.annotations._ 4 | import org.openjdk.jmh.infra.Blackhole 5 | import java.util.concurrent.TimeUnit 6 | 7 | import com.github.benmanes.caffeine.cache.Caffeine 8 | 9 | import scalacache._ 10 | import caffeine._ 11 | import memoization._ 12 | import cats.effect.SyncIO 13 | import cats.effect.Clock 14 | import scala.annotation.nowarn 15 | 16 | @State(Scope.Thread) 17 | class CaffeineBenchmark { 18 | 19 | implicit val clockSyncIO: Clock[SyncIO] = Clock[SyncIO] 20 | 21 | val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() 22 | implicit val cache: Cache[SyncIO, String, String] = 23 | CaffeineCache[SyncIO, String, String](underlyingCache) 24 | 25 | val key = "key" 26 | val value: String = "value" 27 | 28 | def itemCachedNoMemoize(key: String): Option[String] = { 29 | cache.get(key).unsafeRunSync() 30 | } 31 | 32 | @nowarn 33 | def itemCachedMemoize(key: String): String = 34 | memoize(None) { 35 | value 36 | }.unsafeRunSync() 37 | 38 | // populate the cache 39 | cache.put(key)(value) 40 | 41 | @Benchmark 42 | @BenchmarkMode(Array(Mode.AverageTime)) 43 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 44 | def scalacacheGetNoMemoize(bh: Blackhole) = { 45 | bh.consume(itemCachedNoMemoize(key)) 46 | } 47 | 48 | @Benchmark 49 | @BenchmarkMode(Array(Mode.AverageTime)) 50 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 51 | def scalacacheGetWithMemoize(bh: Blackhole) = { 52 | bh.consume(itemCachedMemoize(key)) 53 | } 54 | 55 | @Benchmark 56 | @BenchmarkMode(Array(Mode.AverageTime)) 57 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 58 | def caffeineGet(bh: Blackhole) = { 59 | bh.consume(underlyingCache.getIfPresent(key)) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/benchmarks/src/test/scala/scalacache/benchmark/ProfilingMemoize.scala: -------------------------------------------------------------------------------- 1 | package scalacache.benchmark 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine 4 | 5 | import scalacache._ 6 | import scalacache.caffeine._ 7 | import scalacache.memoization._ 8 | import cats.effect.SyncIO 9 | import cats.effect.Clock 10 | import scala.annotation.nowarn 11 | 12 | /** Just runs forever, endlessly calling memoize, so Java Flight Recorder can output sampling data. 13 | */ 14 | object ProfilingMemoize extends App { 15 | 16 | implicit val clockSyncIO = Clock[SyncIO] 17 | val underlyingCache = Caffeine.newBuilder().build[String, Entry[String]]() 18 | implicit val cache = CaffeineCache[SyncIO, String, String](underlyingCache) 19 | 20 | val key = "key" 21 | val value: String = "value" 22 | 23 | @nowarn 24 | def itemCachedMemoize(key: String): String = 25 | memoize(None) { 26 | value 27 | }.unsafeRunSync() 28 | 29 | var result: String = _ 30 | var i = 0L 31 | 32 | while (i < Long.MaxValue) { 33 | result = itemCachedMemoize(key) 34 | i += 1 35 | } 36 | println(result) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /modules/caffeine/src/main/scala/scalacache/caffeine/CaffeineCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.caffeine 18 | 19 | import cats.effect.Sync 20 | import cats.implicits._ 21 | import com.github.benmanes.caffeine.cache.{Caffeine, Cache => CCache} 22 | import scalacache.logging.Logger 23 | import scalacache.{AbstractCache, Entry} 24 | 25 | import java.time.Instant 26 | import scala.concurrent.duration.Duration 27 | 28 | /* 29 | * Thin wrapper around Caffeine. 30 | * 31 | * This cache implementation is synchronous. 32 | */ 33 | class CaffeineCache[F[_]: Sync, K, V](val underlying: CCache[K, Entry[V]]) extends AbstractCache[F, K, V] { 34 | protected val F: Sync[F] = Sync[F] 35 | 36 | override protected final val logger = Logger.getLogger(getClass.getName) 37 | 38 | def doGet(key: K): F[Option[V]] = { 39 | F.delay { 40 | Option(underlying.getIfPresent(key)) 41 | }.flatMap(_.filterA(Entry.isBeforeExpiration[F, V])) 42 | .map(_.map(_.value)) 43 | .flatTap { result => 44 | logCacheHitOrMiss(key, result) 45 | } 46 | } 47 | 48 | def doPut(key: K, value: V, ttl: Option[Duration]): F[Unit] = 49 | ttl.traverse(toExpiryTime).flatMap { expiry => 50 | F.delay { 51 | val entry = Entry(value, expiry) 52 | underlying.put(key, entry) 53 | } *> logCachePut(key, ttl) 54 | } 55 | 56 | override def doRemove(key: K): F[Unit] = 57 | F.delay(underlying.invalidate(key)) 58 | 59 | override def doRemoveAll: F[Unit] = 60 | F.delay(underlying.invalidateAll()) 61 | 62 | override def close: F[Unit] = { 63 | // Nothing to do 64 | F.unit 65 | } 66 | 67 | private def toExpiryTime(ttl: Duration): F[Instant] = 68 | Sync[F].monotonic.map(m => Instant.ofEpochMilli(m.toMillis).plusMillis(ttl.toMillis)) 69 | 70 | } 71 | 72 | object CaffeineCache { 73 | 74 | /** Create a new Caffeine cache. 75 | */ 76 | def apply[F[_]: Sync, K <: AnyRef, V]: F[CaffeineCache[F, K, V]] = 77 | Sync[F].delay(Caffeine.newBuilder.build[K, Entry[V]]()).map(apply(_)) 78 | 79 | /** Create a new cache utilizing the given underlying Caffeine cache. 80 | * 81 | * @param underlying 82 | * a Caffeine cache 83 | */ 84 | def apply[F[_]: Sync, K, V]( 85 | underlying: CCache[K, Entry[V]] 86 | ): CaffeineCache[F, K, V] = 87 | new CaffeineCache(underlying) 88 | 89 | } 90 | -------------------------------------------------------------------------------- /modules/caffeine/src/test/scala/scalacache/caffeine/CaffeineCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.caffeine 18 | 19 | import java.time.Instant 20 | 21 | import scala.concurrent.duration._ 22 | 23 | import cats.effect.Clock 24 | import cats.effect.IO 25 | import cats.effect.Sync 26 | import cats.effect.testkit.TestContext 27 | import cats.effect.testkit.TestInstances 28 | import com.github.benmanes.caffeine.cache.Caffeine 29 | import org.scalatest.BeforeAndAfter 30 | import org.scalatest.concurrent.ScalaFutures 31 | import org.scalatest.flatspec.AnyFlatSpec 32 | import org.scalatest.matchers.should.Matchers 33 | import scalacache._ 34 | import org.scalatest.compatible.Assertion 35 | import cats.effect.kernel.Outcome 36 | 37 | class CaffeineCacheSpec extends AnyFlatSpec with Matchers with BeforeAndAfter with ScalaFutures with TestInstances { 38 | 39 | private def ticked[A](f: Ticker => IO[Assertion]): Assertion = { 40 | implicit val ticker = Ticker(TestContext()) 41 | 42 | unsafeRun(f(ticker)) shouldBe Outcome.succeeded(Some(succeed)) 43 | } 44 | 45 | case class MyInt(int: Int) 46 | 47 | private def newCCache = Caffeine.newBuilder.build[MyInt, Entry[String]] 48 | 49 | private def newFCache[F[_]: Sync, V]( 50 | underlying: com.github.benmanes.caffeine.cache.Cache[MyInt, Entry[V]] 51 | ) = { 52 | CaffeineCache[F, MyInt, V](underlying) 53 | } 54 | 55 | private def newIOCache[V]( 56 | underlying: com.github.benmanes.caffeine.cache.Cache[MyInt, Entry[V]] 57 | ) = { 58 | newFCache[IO, V](underlying) 59 | } 60 | 61 | behavior of "get" 62 | 63 | it should "return the value stored in the underlying cache if expiration is not specified" in ticked { _ => 64 | val underlying = newCCache 65 | val entry = Entry("hello", expiresAt = None) 66 | underlying.put(MyInt(1), entry) 67 | 68 | newIOCache(underlying).get(MyInt(1)).map(_ shouldBe Some("hello")) 69 | } 70 | 71 | it should "return None if the given key does not exist in the underlying cache" in ticked { _ => 72 | val underlying = newCCache 73 | newIOCache(underlying).get(MyInt(2)).map(_ shouldBe None) 74 | } 75 | 76 | it should "return None if the given key exists but the value has expired" in ticked { _ => 77 | Clock[IO].monotonic.flatMap { now => 78 | val underlying = newCCache 79 | val expiredEntry = 80 | Entry("hello", expiresAt = Some(Instant.ofEpochMilli(now.toMillis).minusSeconds(60))) 81 | underlying.put(MyInt(1), expiredEntry) 82 | newIOCache(underlying).get(MyInt(1)).map(_ shouldBe None) 83 | } 84 | } 85 | 86 | it should "return the value stored in the underlying cache if the value has not expired" in ticked { _ => 87 | Clock[IO].monotonic.flatMap { now => 88 | val underlying = newCCache 89 | val expiredEntry = 90 | Entry("hello", expiresAt = Some(Instant.ofEpochMilli(now.toMillis).plusSeconds(60))) 91 | underlying.put(MyInt(1), expiredEntry) 92 | newIOCache(underlying).get(MyInt(1)).map(_ shouldBe Some("hello")) 93 | } 94 | } 95 | 96 | behavior of "put" 97 | 98 | it should "store the given key-value pair in the underlying cache with no TTL" in ticked { _ => 99 | val underlying = newCCache 100 | newIOCache(underlying).put(MyInt(1))("hello", None) *> 101 | IO { underlying.getIfPresent(MyInt(1)) } 102 | .map(_ shouldBe Entry("hello", None)) 103 | } 104 | 105 | behavior of "put with TTL" 106 | 107 | it should "store the given key-value pair in the underlying cache with the given TTL" in ticked { implicit ticker => 108 | val ctx = ticker.ctx 109 | val now = Instant.ofEpochMilli(ctx.now().toMillis) 110 | 111 | val underlying = newCCache 112 | 113 | newFCache[IO, String](underlying).put(MyInt(1))("hello", Some(10.seconds)).map { _ => 114 | underlying.getIfPresent(MyInt(1)) should be(Entry("hello", expiresAt = Some(now.plusSeconds(10)))) 115 | } 116 | } 117 | 118 | it should "support a TTL greater than Int.MaxValue millis" in ticked { implicit ticker => 119 | val ctx = ticker.ctx 120 | val now = Instant.ofEpochMilli(ctx.now().toMillis) 121 | 122 | val underlying = newCCache 123 | newFCache[IO, String](underlying).put(MyInt(1))("hello", Some(30.days)).map { _ => 124 | underlying.getIfPresent(MyInt(1)) should be( 125 | Entry("hello", expiresAt = Some(now.plusMillis(30.days.toMillis))) 126 | ) 127 | } 128 | } 129 | 130 | behavior of "remove" 131 | 132 | it should "delete the given key and its value from the underlying cache" in ticked { _ => 133 | val underlying = newCCache 134 | val entry = Entry("hello", expiresAt = None) 135 | underlying.put(MyInt(1), entry) 136 | underlying.getIfPresent(MyInt(1)) should be(entry) 137 | 138 | newIOCache(underlying).remove(MyInt(1)) *> 139 | IO(underlying.getIfPresent(MyInt(1))).map(_ shouldBe null) 140 | } 141 | 142 | behavior of "get after put" 143 | 144 | it should "store the given key-value pair in the underlying cache with no TTL, then get it back" in ticked { _ => 145 | val underlying = newCCache 146 | val cache = newIOCache(underlying) 147 | cache.put(MyInt(1))("hello", None) *> 148 | cache.get(MyInt(1)).map { _ shouldBe defined } 149 | } 150 | 151 | behavior of "get after put with TTL" 152 | 153 | it should "store the given key-value pair with the given TTL, then get it back when not expired" in ticked { _ => 154 | val underlying = newCCache 155 | val cache = newFCache[IO, String](underlying) 156 | 157 | cache.put(MyInt(1))("hello", Some(5.seconds)) *> 158 | cache.get(MyInt(1)).map { _ shouldBe defined } 159 | } 160 | 161 | it should "store the given key-value pair with the given TTL, then get it back (after a sleep) when not expired" in ticked { 162 | _ => 163 | val underlying = newCCache 164 | val cache = newFCache[IO, String](underlying) 165 | 166 | cache.put(MyInt(1))("hello", Some(50.seconds)) *> 167 | IO.sleep(40.seconds) *> // sleep, but not long enough for the entry to expire 168 | cache.get(MyInt(1)).map { _ shouldBe defined } 169 | } 170 | 171 | it should "store the given key-value pair with the given TTL, then return None if the entry has expired" in ticked { 172 | _ => 173 | val underlying = newCCache 174 | val cache = newFCache[IO, String](underlying) 175 | 176 | cache.put(MyInt(1))("hello", Some(50.seconds)) *> 177 | IO.sleep(60.seconds) *> // sleep long enough for the entry to expire 178 | cache.get(MyInt(1)).map { _ shouldBe empty } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /modules/circe/src/main/scala/scalacache/serialization/Circe.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization 18 | 19 | import java.nio.ByteBuffer 20 | import io.circe.jawn.JawnParser 21 | import scalacache.serialization.binary.BinaryCodec 22 | 23 | package object circe { 24 | 25 | private[this] val parser = new JawnParser 26 | 27 | implicit def codec[A](implicit encoder: io.circe.Encoder[A], decoder: io.circe.Decoder[A]): BinaryCodec[A] = 28 | new BinaryCodec[A] { 29 | 30 | override def encode(value: A): Array[Byte] = encoder.apply(value).noSpaces.getBytes("utf-8") 31 | 32 | override def decode(bytes: Array[Byte]): Codec.DecodingResult[A] = 33 | parser.decodeByteBuffer(ByteBuffer.wrap(bytes)).left.map(FailedToDecode.apply) 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /modules/circe/src/test/scala/scalacache/serialization/CirceCodecSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization 18 | 19 | import io.circe.Json 20 | import org.scalacheck.Arbitrary 21 | import io.circe.syntax._ 22 | import org.scalatest.flatspec.AnyFlatSpec 23 | import org.scalatest.matchers.should.Matchers 24 | import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks 25 | import scalacache.serialization.binary.BinaryCodec 26 | import org.scalatest.compatible.Assertion 27 | 28 | case class Fruit(name: String, tastinessQuotient: Double) 29 | 30 | class CirceCodecSpec extends AnyFlatSpec with Matchers with ScalaCheckDrivenPropertyChecks { 31 | 32 | behavior of "JSON serialization using circe" 33 | 34 | import scalacache.serialization.circe._ 35 | 36 | private def serdesCheck[A: Arbitrary](expectedJson: A => String)(implicit codec: BinaryCodec[A]): Assertion = { 37 | forAll(minSuccessful(10000)) { (a: A) => 38 | val serialised = codec.encode(a) 39 | new String(serialised, "utf-8") shouldBe expectedJson(a) 40 | val deserialised = codec.decode(serialised) 41 | deserialised.toOption.get shouldBe a 42 | } 43 | } 44 | 45 | it should "serialize and deserialize Ints" in { 46 | serdesCheck[Int](x => s"$x") 47 | } 48 | 49 | it should "serialize and deserialize Longs" in { 50 | serdesCheck[Long](x => s"$x") 51 | } 52 | 53 | it should "serialize and deserialize Doubles" in { 54 | serdesCheck[Double](x => s"$x") 55 | } 56 | 57 | it should "serialize and deserialize Floats" in { 58 | serdesCheck[Float](x => s"$x") 59 | } 60 | 61 | it should "serialize and deserialize Booleans" in { 62 | serdesCheck[Boolean](x => s"$x") 63 | } 64 | 65 | it should "serialize and deserialize Char" in { 66 | serdesCheck[Char](x => x.asJson.toString) 67 | } 68 | 69 | it should "serialize and deserialize Short" in { 70 | serdesCheck[Short](x => s"$x") 71 | } 72 | 73 | it should "serialize and deserialize String" in { 74 | serdesCheck[String](x => Json.fromString(x).noSpaces) 75 | } 76 | 77 | it should "serialize and deserialize Array[Byte]" in { 78 | serdesCheck[Array[Byte]](x => x.mkString("[", ",", "]")) 79 | } 80 | 81 | it should "serialize and deserialize a case class" in { 82 | import io.circe.generic.auto._ 83 | val fruitCodec = implicitly[BinaryCodec[Fruit]] 84 | 85 | val banana = Fruit("banana", 0.7) 86 | val serialised = fruitCodec.encode(banana) 87 | new String(serialised, "utf-8") shouldBe """{"name":"banana","tastinessQuotient":0.7}""" 88 | val deserialised = fruitCodec.decode(serialised) 89 | deserialised.toOption.get shouldBe banana 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-2/scalacache/memoization/Macros.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import scalacache.Cache 20 | import scalacache.Flags 21 | 22 | import scala.concurrent.duration.Duration 23 | import scala.reflect.macros.blackbox 24 | 25 | class Macros(val c: blackbox.Context) { 26 | import c.universe._ 27 | 28 | def memoizeImpl[F[_], V]( 29 | ttl: c.Expr[Option[Duration]] 30 | )(f: c.Tree)(cache: c.Expr[Cache[F, String, V]], config: c.Expr[MemoizationConfig], flags: c.Expr[Flags]): c.Tree = { 31 | commonMacroImpl( 32 | config, 33 | { keyName => 34 | q"""$cache.caching($keyName)($ttl)($f)($flags)""" 35 | } 36 | ) 37 | } 38 | 39 | def memoizeFImpl[F[_], V]( 40 | ttl: c.Expr[Option[Duration]] 41 | )(f: c.Tree)(cache: c.Expr[Cache[F, String, V]], config: c.Expr[MemoizationConfig], flags: c.Expr[Flags]): c.Tree = { 42 | commonMacroImpl( 43 | config, 44 | { keyName => 45 | q"""$cache.cachingF($keyName)($ttl)($f)($flags)""" 46 | } 47 | ) 48 | } 49 | 50 | private def commonMacroImpl[F[_], V]( 51 | config: c.Expr[MemoizationConfig], 52 | keyNameToCachingCall: (c.TermName) => c.Tree 53 | ): Tree = { 54 | 55 | val enclosingMethodSymbol = getMethodSymbol() 56 | val classSymbol = getClassSymbol() 57 | 58 | /* 59 | * Gather all the info needed to build the cache key: 60 | * class name, method name and the method parameters lists 61 | */ 62 | val classNameTree = getFullClassName(classSymbol) 63 | val classParamssTree = getConstructorParams(classSymbol) 64 | val methodNameTree = getMethodName(enclosingMethodSymbol) 65 | val methodParamssSymbols = c.internal.enclosingOwner.info.paramLists 66 | val methodParamssTree = paramListsToTree(methodParamssSymbols) 67 | 68 | val keyName = createKeyName() 69 | val cachingCall = keyNameToCachingCall(keyName) 70 | val tree = q""" 71 | val $keyName = $config.toStringConverter.toString($classNameTree, $classParamssTree, $methodNameTree, $methodParamssTree) 72 | $cachingCall 73 | """ 74 | // println(showCode(tree)) 75 | // println(showRaw(tree, printIds = true, printTypes = true)) 76 | tree 77 | } 78 | 79 | /** Get the symbol of the method that encloses the macro, or abort the compilation if we can't find one. 80 | */ 81 | private def getMethodSymbol(): c.Symbol = { 82 | 83 | def getMethodSymbolRecursively(sym: Symbol): Symbol = { 84 | if (sym == null || sym == NoSymbol || sym.owner == sym) 85 | c.abort( 86 | c.enclosingPosition, 87 | "This memoize block does not appear to be inside a method. " + 88 | "Memoize blocks must be placed inside methods, so that a cache key can be generated." 89 | ) 90 | else if (sym.isMethod) 91 | sym 92 | else 93 | getMethodSymbolRecursively(sym.owner) 94 | } 95 | 96 | getMethodSymbolRecursively(c.internal.enclosingOwner) 97 | } 98 | 99 | /** Convert the given method symbol to a tree representing the method name. 100 | */ 101 | private def getMethodName(methodSymbol: c.Symbol): c.Tree = { 102 | val methodName = methodSymbol.asMethod.name.toString 103 | // return a Tree 104 | q"$methodName" 105 | } 106 | 107 | private def getClassSymbol(): c.Symbol = { 108 | def getClassSymbolRecursively(sym: Symbol): Symbol = { 109 | if (sym == null) 110 | c.abort(c.enclosingPosition, "Encountered a null symbol while searching for enclosing class") 111 | else if (sym.isClass || sym.isModule) 112 | sym 113 | else 114 | getClassSymbolRecursively(sym.owner) 115 | } 116 | 117 | getClassSymbolRecursively(c.internal.enclosingOwner) 118 | } 119 | 120 | /** Convert the given class symbol to a tree representing the fully qualified class name. 121 | * 122 | * @param classSymbol 123 | * should be either a ClassSymbol or a ModuleSymbol 124 | */ 125 | private def getFullClassName(classSymbol: c.Symbol): c.Tree = { 126 | val className = classSymbol.fullName 127 | // return a Tree 128 | q"$className" 129 | } 130 | 131 | private def getConstructorParams(classSymbol: c.Symbol): c.Tree = { 132 | if (classSymbol.isClass) { 133 | val symbolss = classSymbol.asClass.primaryConstructor.asMethod.paramLists 134 | if (symbolss == List(Nil)) { 135 | q"_root_.scala.collection.immutable.Vector.empty" 136 | } else { 137 | paramListsToTree(symbolss) 138 | } 139 | } else { 140 | q"_root_.scala.collection.immutable.Vector.empty" 141 | } 142 | } 143 | 144 | private def paramListsToTree(symbolss: List[List[c.Symbol]]): c.Tree = { 145 | val cacheKeyExcludeType = c.typeOf[cacheKeyExclude] 146 | def shouldExclude(s: c.Symbol) = { 147 | s.annotations.exists(a => a.tree.tpe == cacheKeyExcludeType) 148 | } 149 | val identss: List[List[Ident]] = symbolss.map(ss => 150 | ss.collect { 151 | case s if !shouldExclude(s) => Ident(s.name) 152 | } 153 | ) 154 | listToTree(identss.map(is => listToTree(is))) 155 | } 156 | 157 | /** Convert a List[Tree] to a Tree representing `Vector` 158 | */ 159 | private def listToTree(ts: List[c.Tree]): c.Tree = { 160 | q"_root_.scala.collection.immutable.Vector(..$ts)" 161 | } 162 | 163 | private def createKeyName(): TermName = { 164 | // We must create a fresh name for any vals that we define, to ensure we don't clash with any user-defined terms. 165 | // See https://github.com/cb372/scalacache/issues/13 166 | // (Note that c.freshName("key") does not work as expected. 167 | // It causes quasiquotes to generate crazy code, resulting in a MatchError.) 168 | c.freshName(c.universe.TermName("key")) 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-2/scalacache/memoization/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import scala.concurrent.duration._ 20 | 21 | /** Utilities for memoizing the results of method calls in a cache. The cache key is generated from the method arguments 22 | * using a macro, so that you don't have to bother passing them manually. 23 | */ 24 | package object memoization { 25 | 26 | /** Perform the given operation and memoize its result to a cache before returning it. If the result is already in the 27 | * cache, return it without performing the operation. 28 | * 29 | * If a TTL is given, the result is stored in the cache with that TTL. It will be evicted when the TTL is up. 30 | * 31 | * Note that if the result is currently in the cache, changing the TTL has no effect. TTL is only set once, when the 32 | * result is added to the cache. 33 | * 34 | * @param ttl 35 | * Time-To-Live 36 | * @param f 37 | * A function that computes some result. This result is the value that will be cached. 38 | * @param cache 39 | * The cache 40 | * @param flags 41 | * Flags used to conditionally alter the behaviour of ScalaCache 42 | * @tparam F 43 | * The type of container in which the result will be wrapped. This is decided by the mode. 44 | * @tparam V 45 | * The type of the value to be cached 46 | * @return 47 | * A result, either retrieved from the cache or calculated by executing the function `f` 48 | */ 49 | def memoize[F[_], V](ttl: Option[Duration])( 50 | f: => V 51 | )(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] = 52 | macro Macros.memoizeImpl[F, V] 53 | 54 | /** Perform the given operation and memoize its result to a cache before returning it. If the result is already in the 55 | * cache, return it without performing the operation. 56 | * 57 | * If a TTL is given, the result is stored in the cache with that TTL. It will be evicted when the TTL is up. 58 | * 59 | * Note that if the result is currently in the cache, changing the TTL has no effect. TTL is only set once, when the 60 | * result is added to the cache. 61 | * 62 | * @param ttl 63 | * Time-To-Live 64 | * @param f 65 | * A function that computes some result wrapped in an `F`. This result is the value that will be cached. 66 | * @param cache 67 | * The cache 68 | * @param flags 69 | * Flags used to conditionally alter the behaviour of ScalaCache 70 | * @tparam F 71 | * The type of container in which the result will be wrapped. This is decided by the mode. 72 | * @tparam V 73 | * The type of the value to be cached 74 | * @return 75 | * A result, either retrieved from the cache or calculated by executing the function `f` 76 | */ 77 | def memoizeF[F[_], V]( 78 | ttl: Option[Duration] 79 | )(f: F[V])(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] = 80 | macro Macros.memoizeFImpl[F, V] 81 | } 82 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-3/scalacache/memoization/Macros.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import scala.concurrent.duration.Duration 20 | import scala.quoted.* 21 | 22 | import scalacache.{Cache, Flags} 23 | 24 | object Macros { 25 | def memoizeImpl[F[_], V]( 26 | ttl: Expr[Option[Duration]], 27 | f: Expr[V], 28 | cache: Expr[Cache[F, String, V]], 29 | config: Expr[MemoizationConfig], 30 | flags: Expr[Flags] 31 | )(using Quotes, Type[F], Type[V]): Expr[F[V]] = 32 | commonMacroImpl(config, keyName => '{ ${ cache }.caching(${ keyName })(${ ttl })(${ f })(${ flags }) }) 33 | 34 | def memoizeFImpl[F[_], V]( 35 | ttl: Expr[Option[Duration]], 36 | f: Expr[F[V]], 37 | cache: Expr[Cache[F, String, V]], 38 | config: Expr[MemoizationConfig], 39 | flags: Expr[Flags] 40 | )(using Quotes, Type[F], Type[V]): Expr[F[V]] = 41 | commonMacroImpl(config, keyName => '{ ${ cache }.cachingF(${ keyName })(${ ttl })(${ f })(${ flags }) }) 42 | 43 | private def commonMacroImpl[F[_], V]( 44 | config: Expr[MemoizationConfig], 45 | keyNameToCachingCall: Expr[String] => Expr[F[V]] 46 | )(using Quotes, Type[F], Type[V]): Expr[F[V]] = { 47 | import quotes.reflect.* 48 | val sym: Symbol = Symbol.spliceOwner 49 | 50 | def hasCacheKeyExcludeAnnotation(s: Symbol): Boolean = s.annotations.exists { 51 | case Apply(Select(New(TypeIdent("cacheKeyExclude")), _), _) => true 52 | case o => false 53 | } 54 | 55 | def getOwningDefSymbol(s: Symbol): Symbol = 56 | if (s.isDefDef || s == Symbol.noSymbol) s else getOwningDefSymbol(s.owner) 57 | 58 | val defdef: DefDef = getOwningDefSymbol(sym).tree.asInstanceOf[DefDef] 59 | 60 | def getOwningClassSymbol(s: Symbol): Symbol = if (s.isClassDef) s else getOwningClassSymbol(s.owner) 61 | 62 | val classdefSymbol = getOwningClassSymbol(sym) 63 | 64 | val classdef: ClassDef = classdefSymbol.tree.asInstanceOf[ClassDef] 65 | 66 | val defParams: Seq[Seq[Expr[Any]]] = defdef.termParamss.map( 67 | _.params 68 | .filterNot(p => hasCacheKeyExcludeAnnotation(p.symbol)) 69 | .map(p => Ref(p.symbol).asExpr) 70 | ) 71 | 72 | val classParams: Seq[Seq[Expr[Any]]] = classdef.constructor.termParamss.map( 73 | _.params 74 | .filterNot(p => hasCacheKeyExcludeAnnotation(p.symbol)) 75 | .map { p => Ref.term(TermRef(This(classdef.symbol).tpe, p.name)).asExpr } 76 | ) match { 77 | case seqseq if seqseq.forall(_.isEmpty) => Nil 78 | case seqseq => seqseq 79 | } 80 | 81 | def traverse[V](coll: Seq[Expr[V]])(using Type[V]): Expr[IndexedSeq[V]] = { 82 | val v = Varargs(coll) 83 | '{ IndexedSeq($v: _*) } 84 | } 85 | 86 | val defParamExpr: Expr[IndexedSeq[IndexedSeq[Any]]] = traverse(defParams map traverse) 87 | 88 | val classParamExpr: Expr[IndexedSeq[IndexedSeq[Any]]] = traverse(classParams map traverse) 89 | 90 | val fullName = Expr(classdefSymbol.fullName) 91 | val name = Expr(defdef.name) 92 | val keyValue: Expr[String] = '{ 93 | ${ config }.toStringConverter.toString( 94 | $fullName, 95 | $classParamExpr, 96 | $name, 97 | $defParamExpr 98 | ) 99 | } 100 | 101 | keyNameToCachingCall(keyValue) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-3/scalacache/memoization/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import scala.concurrent.duration._ 20 | 21 | /** Utilities for memoizing the results of method calls in a cache. The cache key is generated from the method arguments 22 | * using a macro, so that you don't have to bother passing them manually. 23 | */ 24 | package object memoization { 25 | 26 | /** Perform the given operation and memoize its result to a cache before returning it. If the result is already in the 27 | * cache, return it without performing the operation. 28 | * 29 | * If a TTL is given, the result is stored in the cache with that TTL. It will be evicted when the TTL is up. 30 | * 31 | * Note that if the result is currently in the cache, changing the TTL has no effect. TTL is only set once, when the 32 | * result is added to the cache. 33 | * 34 | * @param ttl 35 | * Time-To-Live 36 | * @param f 37 | * A function that computes some result. This result is the value that will be cached. 38 | * @param mode 39 | * The operation mode, which decides the type of container in which to wrap the result 40 | * @param cache 41 | * The cache 42 | * @param flags 43 | * Flags used to conditionally alter the behaviour of ScalaCache 44 | * @tparam F 45 | * The type of container in which the result will be wrapped. This is decided by the mode. 46 | * @tparam V 47 | * The type of the value to be cached 48 | * @return 49 | * A result, either retrieved from the cache or calculated by executing the function `f` 50 | */ 51 | inline def memoize[F[_], V](ttl: Option[Duration])( 52 | f: => V 53 | )(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] = 54 | ${ Macros.memoizeImpl[F, V]('ttl, 'f, 'cache, 'config, 'flags) } 55 | 56 | /** Perform the given operation and memoize its result to a cache before returning it. If the result is already in the 57 | * cache, return it without performing the operation. 58 | * 59 | * If a TTL is given, the result is stored in the cache with that TTL. It will be evicted when the TTL is up. 60 | * 61 | * Note that if the result is currently in the cache, changing the TTL has no effect. TTL is only set once, when the 62 | * result is added to the cache. 63 | * 64 | * @param ttl 65 | * Time-To-Live 66 | * @param f 67 | * A function that computes some result wrapped in an [[F]]. This result is the value that will be cached. 68 | * @param mode 69 | * The operation mode, which decides the type of container in which to wrap the result 70 | * @param cache 71 | * The cache 72 | * @param flags 73 | * Flags used to conditionally alter the behaviour of ScalaCache 74 | * @tparam F 75 | * The type of container in which the result will be wrapped. This is decided by the mode. 76 | * @tparam V 77 | * The type of the value to be cached 78 | * @return 79 | * A result, either retrieved from the cache or calculated by executing the function `f` 80 | */ 81 | inline def memoizeF[F[_], V](ttl: Option[Duration])( 82 | f: F[V] 83 | )(implicit cache: Cache[F, String, V], config: MemoizationConfig, flags: Flags): F[V] = 84 | ${ Macros.memoizeFImpl[F, V]('ttl, 'f, 'cache, 'config, 'flags) } 85 | } 86 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/AbstractCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import cats.effect.Sync 20 | import cats.implicits._ 21 | 22 | import scala.concurrent.duration.Duration 23 | 24 | /** An abstract implementation of [[Cache]] that takes care of some things that are common across all concrete 25 | * implementations. 26 | * 27 | * If you are writing a cache implementation, you probably want to extend this trait rather than extending [[Cache]] 28 | * directly. 29 | * 30 | * @tparam K 31 | * The type of keys stored in the cache. 32 | * @tparam V 33 | * The type of values stored in the cache. 34 | */ 35 | trait AbstractCache[F[_], K, V] extends Cache[F, K, V] with LoggingSupport[F, K] { 36 | 37 | protected implicit def F: Sync[F] 38 | // GET 39 | 40 | protected def doGet(key: K): F[Option[V]] 41 | 42 | private def checkFlagsAndGet(key: K)(implicit flags: Flags): F[Option[V]] = { 43 | if (flags.readsEnabled) { 44 | doGet(key) 45 | } else 46 | logger 47 | .ifDebugEnabled { 48 | logger.debug(s"Skipping cache GET because cache reads are disabled. Key: $key") 49 | } 50 | .as(None) 51 | } 52 | 53 | final override def get(key: K)(implicit flags: Flags): F[Option[V]] = { 54 | checkFlagsAndGet(key) 55 | } 56 | 57 | // PUT 58 | 59 | protected def doPut(key: K, value: V, ttl: Option[Duration]): F[Unit] 60 | 61 | private def checkFlagsAndPut(key: K, value: V, ttl: Option[Duration])(implicit 62 | flags: Flags 63 | ): F[Unit] = { 64 | if (flags.writesEnabled) { 65 | doPut(key, value, ttl) 66 | } else 67 | logger.ifDebugEnabled { 68 | logger.debug(s"Skipping cache PUT because cache writes are disabled. Key: $key") 69 | }.void 70 | } 71 | 72 | final override def put( 73 | key: K 74 | )(value: V, ttl: Option[Duration])(implicit flags: Flags): F[Unit] = { 75 | val finiteTtl = ttl.filter(_.isFinite) // discard Duration.Inf, Duration.Undefined 76 | checkFlagsAndPut(key, value, finiteTtl) 77 | } 78 | 79 | // REMOVE 80 | 81 | protected def doRemove(key: K): F[Unit] 82 | 83 | final override def remove(key: K): F[Unit] = 84 | doRemove(key) 85 | 86 | // REMOVE ALL 87 | 88 | protected def doRemoveAll: F[Unit] 89 | 90 | final override def removeAll: F[Unit] = 91 | doRemoveAll 92 | 93 | // CACHING 94 | 95 | final override def caching( 96 | key: K 97 | )(ttl: Option[Duration] = None)(f: => V)(implicit flags: Flags): F[V] = cachingF(key)(ttl)(Sync[F].delay(f)) 98 | 99 | override def cachingF( 100 | key: K 101 | )(ttl: Option[Duration] = None)(f: F[V])(implicit flags: Flags): F[V] = { 102 | checkFlagsAndGet(key) 103 | .handleErrorWith { e => 104 | logger 105 | .ifWarnEnabled(logger.warn(s"Failed to read from cache. Key = $key", e)) 106 | .as(None) 107 | } 108 | .flatMap { 109 | case Some(valueFromCache) => F.pure(valueFromCache) 110 | case None => 111 | f.flatTap { calculatedValue => 112 | checkFlagsAndPut(key, calculatedValue, ttl) 113 | .handleErrorWith { e => 114 | logger.ifWarnEnabled { 115 | logger.warn(s"Failed to write to cache. Key = $key", e) 116 | }.void 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/Cache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import scala.concurrent.duration.Duration 20 | 21 | /** Abstract algebra describing the operations a cache can perform 22 | * 23 | * @tparam F 24 | * The effect monad in which all cache operations will be performed. 25 | * @tparam K 26 | * The type of keys stored in the cache. 27 | * @tparam V 28 | * The type of values stored in the cache. 29 | */ 30 | trait Cache[F[_], K, V] { 31 | 32 | /** Get a value from the cache 33 | * 34 | * @param key 35 | * The cache key 36 | * @param flags 37 | * Flags used to conditionally alter the behaviour of ScalaCache 38 | * @return 39 | * The appropriate value, if it was found in the cache 40 | */ 41 | def get(key: K)(implicit flags: Flags): F[Option[V]] 42 | 43 | /** Insert a value into the cache, optionally setting a TTL (time-to-live) 44 | * 45 | * @param key 46 | * The cache key 47 | * @param value 48 | * The value to insert 49 | * @param ttl 50 | * The time-to-live. The cache entry will expire after this time has elapsed. 51 | * @param flags 52 | * Flags used to conditionally alter the behaviour of ScalaCache 53 | */ 54 | def put(key: K)(value: V, ttl: Option[Duration] = None)(implicit flags: Flags): F[Unit] 55 | 56 | /** Remove the given key and its associated value from the cache, if it exists. If the key is not in the cache, do 57 | * nothing. 58 | * 59 | * @param key 60 | * The cache key 61 | */ 62 | def remove(key: K): F[Unit] 63 | 64 | /** Delete the entire contents of the cache. Use wisely! 65 | */ 66 | def removeAll: F[Unit] 67 | 68 | /** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it. 69 | * 70 | * @param key 71 | * The cache key 72 | * @param ttl 73 | * The time-to-live to use when inserting into the cache. The cache entry will expire after this time has elapsed. 74 | * @param f 75 | * A block that computes the value 76 | * @param flags 77 | * Flags used to conditionally alter the behaviour of ScalaCache 78 | * @return 79 | * The value, either retrieved from the cache or computed 80 | */ 81 | def caching(key: K)(ttl: Option[Duration])(f: => V)(implicit flags: Flags): F[V] 82 | 83 | /** Get a value from the cache if it exists. Otherwise compute it, insert it into the cache, and return it. 84 | * 85 | * @param key 86 | * The cache key 87 | * @param ttl 88 | * The time-to-live to use when inserting into the cache. The cache entry will expire after this time has elapsed. 89 | * @param f 90 | * A block that computes the value wrapped in a container 91 | * @param flags 92 | * Flags used to conditionally alter the behaviour of ScalaCache 93 | * @return 94 | * The value, either retrieved from the cache or computed 95 | */ 96 | def cachingF(key: K)(ttl: Option[Duration])(f: F[V])(implicit flags: Flags): F[V] 97 | 98 | /** You should call this when you have finished using this Cache. (e.g. when your application shuts down) 99 | * 100 | * It will take care of gracefully shutting down the underlying cache client. 101 | * 102 | * Note that you should not try to use this Cache instance after you have called this method. 103 | */ 104 | // TODO: Replace with Resource-based API? 105 | def close: F[Unit] 106 | 107 | } 108 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/Entry.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import java.time.Instant 20 | import cats.effect.Clock 21 | import cats.implicits._ 22 | import cats.Applicative 23 | 24 | /** A cache entry with an optional expiry time 25 | */ 26 | case class Entry[+A](value: A, expiresAt: Option[Instant]) 27 | 28 | object Entry { 29 | 30 | def isBeforeExpiration[F[_], A](entry: Entry[A])(implicit clock: Clock[F], applicative: Applicative[F]): F[Boolean] = 31 | entry.expiresAt 32 | .traverse { expiration => 33 | clock.monotonic.map(m => Instant.ofEpochMilli(m.toMillis).isBefore(expiration)) 34 | } 35 | .map { 36 | case None => true // no expiration set for entry, never expires 37 | case Some(beforeExpiration) => beforeExpiration 38 | } 39 | 40 | def isExpired[F[_], A](entry: Entry[A])(implicit clock: Clock[F], applicative: Applicative[F]): F[Boolean] = 41 | isBeforeExpiration[F, A](entry).map(b => !b) 42 | } 43 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/Flags.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | /** Configuration flags for conditionally altering the behaviour of ScalaCache. 20 | * 21 | * @param readsEnabled 22 | * if false, cache GETs will be skipped (and will return `None`) 23 | * @param writesEnabled 24 | * if false, cache PUTs will be skipped 25 | */ 26 | case class Flags(readsEnabled: Boolean = true, writesEnabled: Boolean = true) 27 | 28 | object Flags { 29 | 30 | /** The default flag values. These can be overriden at the call site, e.g. 31 | * 32 | * {{{ 33 | * def foo() { 34 | * implicit val myCustomFlags = Flags(...) 35 | * val cachedValue = scalacache.get("wow") 36 | * ... 37 | * } 38 | * }}} 39 | */ 40 | implicit val defaultFlags: Flags = Flags() 41 | 42 | } 43 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/HashingAlgorithm.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import java.security.MessageDigest 20 | 21 | /** Sealed [[HashingAlgorithm]] trait to prevent users from shooting themselves in the foot at runtime by specifying a 22 | * crappy/unsupported algorithm name 23 | * 24 | * The name should be a valid MessageDigest algorithm name.Implementing child classes/objects should refer to this list 25 | * for proper names: 26 | * 27 | * http://docs.oracle.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest 28 | */ 29 | sealed trait HashingAlgorithm { 30 | 31 | /** Name of the algorithm 32 | */ 33 | def name: String 34 | 35 | private final val tLocalMessageDigest: ThreadLocal[MessageDigest] = 36 | new ThreadLocal[MessageDigest] { 37 | override protected def initialValue(): MessageDigest = 38 | java.security.MessageDigest.getInstance(name) 39 | } 40 | 41 | /** Returns a [[java.lang.ThreadLocal]] instance of [[java.security.MessageDigest]] that implements the hashing 42 | * algorithm specified by the "name" string. 43 | * 44 | * Since it is an unshared [[java.lang.ThreadLocal]] instance, calling various methods on the 45 | * [[java.security.MessageDigest]] returned by this method is "thread-safe". 46 | */ 47 | final def messageDigest: MessageDigest = tLocalMessageDigest.get() 48 | } 49 | 50 | /** MD5 returns 32 character long hexadecimal hash strings 51 | */ 52 | case object MD5 extends HashingAlgorithm { 53 | val name = "MD5" 54 | } 55 | 56 | /** SHA1 returns 40 character long hexadecimal hash strings 57 | */ 58 | case object SHA1 extends HashingAlgorithm { 59 | val name = "SHA-1" 60 | } 61 | 62 | /** SHA256 returns 64 character long hexadecimal hash strings 63 | */ 64 | case object SHA256 extends HashingAlgorithm { 65 | val name = "SHA-256" 66 | } 67 | 68 | /** SHA512 returns 128 character long hexadecimal hash strings 69 | */ 70 | case object SHA512 extends HashingAlgorithm { 71 | val name = "SHA-512" 72 | } 73 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/LoggingSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import scalacache.logging.Logger 20 | 21 | import scala.concurrent.duration.Duration 22 | import cats.Monad 23 | import cats.implicits._ 24 | 25 | /** Helper methods for logging 26 | */ 27 | trait LoggingSupport[F[_], K] { 28 | protected def logger: Logger[F] 29 | protected implicit def F: Monad[F] 30 | 31 | /** Output a debug log to record the result of a cache lookup 32 | * 33 | * @param key 34 | * the key that was looked up 35 | * @param result 36 | * the result of the cache lookup 37 | * @tparam A 38 | * the type of the cache value 39 | */ 40 | protected def logCacheHitOrMiss[A](key: K, result: Option[A]): F[Unit] = 41 | logger.ifDebugEnabled { 42 | val hitOrMiss = result.map(_ => "hit") getOrElse "miss" 43 | logger.debug(s"Cache $hitOrMiss for key $key") 44 | }.void 45 | 46 | /** Output a debug log to record a cache insertion/update 47 | * 48 | * @param key 49 | * the key that was inserted/updated 50 | * @param ttl 51 | * the TTL of the inserted entry 52 | */ 53 | protected def logCachePut(key: K, ttl: Option[Duration]): F[Unit] = 54 | logger.ifDebugEnabled { 55 | val ttlMsg = ttl.map(d => s" with TTL ${d.toMillis} ms") getOrElse "" 56 | logger.debug(s"Inserted value into cache with key $key$ttlMsg") 57 | }.void 58 | } 59 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/logging/Logger.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.logging 18 | 19 | import org.slf4j.{Logger => Slf4jLogger, LoggerFactory} 20 | import cats.effect.Sync 21 | import cats.Applicative 22 | import cats.implicits._ 23 | 24 | object Logger { 25 | def getLogger[F[_]: Sync](name: String): Logger[F] = new Logger[F](LoggerFactory.getLogger(name)) 26 | } 27 | 28 | final class Logger[F[_]: Sync](private val logger: Slf4jLogger) { 29 | private def whenM[A](fb: F[Boolean])(fa: => F[A]): F[Option[A]] = fb.ifM(fa.map(_.some), Applicative[F].pure(None)) 30 | 31 | def ifDebugEnabled[A](fa: => F[A]): F[Option[A]] = 32 | whenM(Sync[F].delay(logger.isDebugEnabled))(fa) 33 | 34 | def ifWarnEnabled[A](fa: => F[A]): F[Option[A]] = whenM(Sync[F].delay(logger.isWarnEnabled))(fa) 35 | 36 | def debug(message: String): F[Unit] = Sync[F].delay(logger.debug(message)) 37 | 38 | def warn(message: String): F[Unit] = Sync[F].delay(logger.warn(message)) 39 | 40 | def warn(message: String, e: Throwable): F[Unit] = Sync[F].delay(logger.warn(message, e)) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/memoization/MemoizationConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | /** Configuration related to the behaviour of the `scalacache.memoization.memoize{Sync}` methods. 20 | * 21 | * @param toStringConverter 22 | * converter for generating a String cache key from information about a method call 23 | */ 24 | case class MemoizationConfig(toStringConverter: MethodCallToStringConverter) 25 | 26 | object MemoizationConfig { 27 | implicit val defaultMemoizationConfig: MemoizationConfig = MemoizationConfig( 28 | MethodCallToStringConverter.excludeClassConstructorParams 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/memoization/MethodCallToStringConverter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | /** Converts information about a method call to a String for use in a cache key 20 | */ 21 | trait MethodCallToStringConverter { 22 | 23 | /** Convert the given method call information to a String for use in a cache key 24 | * 25 | * @param fullClassName 26 | * the name of the class whose method was called, including fully-qualified package name 27 | * @param constructorParamss 28 | * the values of the constructor parameters of the method's enclosing class, where applicable. This is a 29 | * `[IndexedSeq[IndexedSeq[Any]]` because there may be multiple parameter lists. If the method is inside an 30 | * `object`, a `trait` or a class with no constructor params, this will be empty. 31 | * @param methodName 32 | * the name of the called method 33 | * @param paramss 34 | * the values of the parameters that were passed to the method. This is a `[IndexedSeq[IndexedSeq[Any]]` because 35 | * there may be multiple parameter lists 36 | * @return 37 | */ 38 | def toString( 39 | fullClassName: String, 40 | constructorParamss: IndexedSeq[IndexedSeq[Any]], 41 | methodName: String, 42 | paramss: IndexedSeq[IndexedSeq[Any]] 43 | ): String 44 | 45 | } 46 | 47 | object MethodCallToStringConverter { 48 | import java.lang.{StringBuilder => JStringBuilder} 49 | 50 | private def appendClassNamePart(sb: JStringBuilder)(className: String): Unit = { 51 | if (className.nonEmpty) { 52 | sb.append(className.stripSuffix("$")) 53 | val _ = sb.append('.') 54 | } 55 | } 56 | 57 | private def appendClassNameAndParamsPart( 58 | sb: JStringBuilder 59 | )(className: String, constructorParamss: IndexedSeq[IndexedSeq[Any]]): Unit = { 60 | if (className.nonEmpty) { 61 | sb.append(className.stripSuffix("$")) 62 | appendParamssPart(sb)(constructorParamss) 63 | val _ = sb.append('.') 64 | } 65 | } 66 | 67 | private def appendParamssPart(sb: JStringBuilder)(paramss: IndexedSeq[IndexedSeq[Any]]): Unit = { 68 | var i = 0 69 | while (i < paramss.size) { 70 | val params = paramss(i) 71 | appendParamsPart(sb)(params) 72 | i += 1 73 | } 74 | } 75 | 76 | private def appendParamsPart(sb: JStringBuilder)(params: IndexedSeq[Any]): Unit = { 77 | sb.append('(') 78 | var i = 0 79 | // Add all params except the last one, with the separator after each one 80 | while (i < params.size - 1) { 81 | sb.append(params(i)) 82 | sb.append(", ") 83 | i += 1 84 | } 85 | // Add the final param 86 | if (i < params.size) { 87 | sb.append(params(i)) 88 | } 89 | val _ = sb.append(')') 90 | } 91 | 92 | /** A converter that builds keys of the form: "package.class.method(arg, ...)(arg, ...)..." e.g. 93 | * "com.foo.MyClass.doSomething(123, abc)(foo)" 94 | * 95 | * Note that this converter ignores the class's constructor params and does NOT include them in the cache key. 96 | */ 97 | val excludeClassConstructorParams: MethodCallToStringConverter = 98 | new MethodCallToStringConverter { 99 | def toString( 100 | fullClassName: String, 101 | constructorParamss: IndexedSeq[IndexedSeq[Any]], 102 | methodName: String, 103 | paramss: IndexedSeq[IndexedSeq[Any]] 104 | ): String = { 105 | val sb = new JStringBuilder(128) 106 | appendClassNamePart(sb)(fullClassName) 107 | sb.append(methodName) 108 | appendParamssPart(sb)(paramss) 109 | sb.toString 110 | } 111 | } 112 | 113 | /** A converter that builds keys of the form: "package.class(arg, ...)(arg, ...).method(arg, ...)(arg, ...)..." e.g. 114 | * "com.foo.MyClass(42, wow).doSomething(123, abc)(foo)" 115 | * 116 | * Note that this converter includes the class's constructor params in the cache key, where applicable. 117 | */ 118 | val includeClassConstructorParams = new MethodCallToStringConverter { 119 | def toString( 120 | fullClassName: String, 121 | constructorParamss: IndexedSeq[IndexedSeq[Any]], 122 | methodName: String, 123 | paramss: IndexedSeq[IndexedSeq[Any]] 124 | ): String = { 125 | val sb = new JStringBuilder(128) 126 | appendClassNameAndParamsPart(sb)(fullClassName, constructorParamss) 127 | sb.append(methodName) 128 | appendParamssPart(sb)(paramss) 129 | sb.toString 130 | } 131 | } 132 | 133 | /** A converter that includes only the method arguments in the cache key. It builds keys of the form: "(arg, ...)(arg, 134 | * ...)..." e.g. a call to `com.foo.MyClass(42, wow).doSomething(123, abc)(foo)` would be cached as "(123, 135 | * abc)(foo)". 136 | * 137 | * Warning: Do not use this key if you have multiple methods that you want to memoize, because cache keys can 138 | * collide. e.g. the results of `Foo.bar(123)` and `Baz.wow(123)` would be cached with the same key `123`. 139 | */ 140 | val onlyMethodParams = new MethodCallToStringConverter { 141 | def toString( 142 | fullClassName: String, 143 | constructorParamss: IndexedSeq[IndexedSeq[Any]], 144 | methodName: String, 145 | paramss: IndexedSeq[IndexedSeq[Any]] 146 | ): String = { 147 | val sb = new JStringBuilder(128) 148 | appendParamssPart(sb)(paramss) 149 | sb.toString 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/memoization/annotations.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import scala.annotation.StaticAnnotation 20 | 21 | /** Add this annotation to method or class constructor parameters in order to exclude them from auto-generated cache 22 | * keys. 23 | * 24 | * e.g. 25 | * 26 | * {{{ 27 | * def foo(a: Int, @cacheKeyExclude b: String, c: String): Int = memoize { ... } 28 | * }}} 29 | * 30 | * will not include the value of the `b` parameter in its cache keys. 31 | */ 32 | final class cacheKeyExclude extends StaticAnnotation 33 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import scala.concurrent.duration.Duration 18 | 19 | package object scalacache { 20 | 21 | /** Get the value corresponding to the given key from the cache. 22 | * 23 | * @param cache 24 | * The cache 25 | * @param flags 26 | * Flags used to conditionally alter the behaviour of ScalaCache 27 | * @tparam F 28 | * The type of container in which the result will be wrapped. 29 | * @tparam K 30 | * The type of the key 31 | * @tparam V 32 | * The type of the corresponding value 33 | * @return 34 | * the value, if there is one 35 | */ 36 | def get[F[_], K, V](key: K)(implicit cache: Cache[F, K, V], flags: Flags): F[Option[V]] = 37 | cache.get(key) 38 | 39 | /** Insert the given key-value pair into the cache, with an optional Time To Live. 40 | * 41 | * Depending on the cache implementation, this may be done synchronously or asynchronously, so it returns a Future. 42 | * 43 | * @param key 44 | * the key of the value to be cached 45 | * @param value 46 | * the value to be cached 47 | * @param ttl 48 | * Time To Live (optional, if not specified then the entry will be cached indefinitely) 49 | * @param cache 50 | * The cache 51 | * @param flags 52 | * Flags used to conditionally alter the behaviour of ScalaCache 53 | * @tparam F 54 | * The type of container in which the result will be wrapped. 55 | * @tparam K 56 | * The type of the corresponding key 57 | * @tparam V 58 | * The type of the corresponding value 59 | */ 60 | def put[F[_], K, V]( 61 | key: K 62 | )(value: V, ttl: Option[Duration] = None)(implicit cache: Cache[F, K, V], flags: Flags): F[Unit] = 63 | cache.put(key)(value, ttl) 64 | 65 | /** Remove the given key and its associated value from the cache, if it exists. If the key is not in the cache, do 66 | * nothing. 67 | * 68 | * Depending on the cache implementation, this may be done synchronously or asynchronously, so it returns a Future. 69 | * 70 | * @param key 71 | * The key that references the cached value 72 | * @param cache 73 | * The cache 74 | * @tparam F 75 | * The type of container in which the result will be wrapped. 76 | * @tparam K 77 | * The type of the value's key 78 | * @tparam V 79 | * The type of the value to be removed 80 | */ 81 | def remove[F[_], K, V](key: K)(implicit cache: Cache[F, K, V]): F[Unit] = 82 | cache.remove(key) 83 | 84 | /** Remove all values from the cache. 85 | * 86 | * @tparam V 87 | * The type of values to be removed 88 | */ 89 | def removeAll[K, V]: syntax.RemoveAll[K, V] = new syntax.RemoveAll[K, V] 90 | 91 | /** Wrap the given block with a caching decorator. First look in the cache. If the value is found, then return it 92 | * immediately. Otherwise run the block and save the result in the cache before returning it. 93 | * 94 | * Note: If ttl is set to None, the result will be stored in the cache indefinitely. 95 | * 96 | * @param key 97 | * Key to cache this item under 98 | * @param ttl 99 | * The time-to-live to use when inserting into the cache. If specified, the cache entry will expire after this time 100 | * has elapsed. 101 | * @param f 102 | * The block to run 103 | * @param cache 104 | * The cache 105 | * @param flags 106 | * Flags used to conditionally alter the behaviour of ScalaCache 107 | * @tparam F 108 | * The type of container in which the result will be wrapped. 109 | * @tparam K 110 | * The type of the key 111 | * @tparam V 112 | * the type of the block's result 113 | * @return 114 | * The result, either retrived from the cache or returned by the block 115 | */ 116 | def caching[F[_], K, V]( 117 | key: K 118 | )(ttl: Option[Duration])(f: => V)(implicit cache: Cache[F, K, V], flags: Flags): F[V] = 119 | cache.caching(key)(ttl)(f) 120 | 121 | /** Wrap the given block with a caching decorator. First look in the cache. If the value is found, then return it 122 | * immediately. Otherwise run the block and save the result in the cache before returning it. 123 | * 124 | * Note: If ttl is set to None, the result will be stored in the cache indefinitely. 125 | * 126 | * @param key 127 | * The key to cache under 128 | * @param ttl 129 | * The time-to-live to use when inserting into the cache. If specified, the cache entry will expire after this time 130 | * has elapsed. 131 | * @param f 132 | * The block to run 133 | * @param cache 134 | * The cache 135 | * @param flags 136 | * Flags used to conditionally alter the behaviour of ScalaCache 137 | * @tparam F 138 | * The type of container in which the result will be wrapped. 139 | * @tparam K 140 | * The type of the key 141 | * @tparam V 142 | * the type of the block's result 143 | * @return 144 | * The result, either retrived from the cache or returned by the block 145 | */ 146 | def cachingF[F[_], K, V]( 147 | key: K 148 | )(ttl: Option[Duration])(f: => F[V])(implicit cache: Cache[F, K, V], flags: Flags): F[V] = 149 | cache.cachingF(key)(ttl)(f) 150 | } 151 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/Codec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization 18 | 19 | import scala.annotation.implicitNotFound 20 | import scala.util.{Failure, Success, Try} 21 | 22 | trait Encoder[L, R] { 23 | def encode(left: L): R 24 | } 25 | 26 | trait Decoder[L, R] { 27 | def decode(right: R): Codec.DecodingResult[L] 28 | } 29 | 30 | /** Represents a type class that needs to be implemented for serialization/deserialization to work. 31 | */ 32 | @implicitNotFound(msg = """Could not find any Codecs for types ${L, R}. 33 | If you would like to serialize values in a binary format, please import the binary codec: 34 | 35 | import scalacache.serialization.binary._ 36 | 37 | If you would like to serialize values as JSON using circe, please import the circe codec 38 | and provide a circe Encoder[${L}] and Decoder[${L}], e.g.: 39 | 40 | import scalacache.serialization.circe._ 41 | import io.circe.generic.auto._ 42 | 43 | You will need a dependency on the scalacache-circe module. 44 | 45 | See the documentation for more details on codecs.""") 46 | trait Codec[L, R] extends Encoder[L, R] with Decoder[L, R] { 47 | override def encode(left: L): R 48 | override def decode(right: R): Codec.DecodingResult[L] 49 | } 50 | 51 | /** For simple primitives, we provide lightweight Codecs for ease of use. 52 | */ 53 | object Codec { 54 | 55 | type DecodingResult[T] = Either[FailedToDecode, T] 56 | 57 | def tryDecode[T](f: => T): DecodingResult[T] = 58 | Try(f) match { 59 | case Success(a) => Right(a) 60 | case Failure(e) => Left(FailedToDecode(e)) 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/FailedToDecode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization 18 | 19 | final case class FailedToDecode(cause: Throwable) extends Exception(cause) 20 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/GenericCodecObjectInputStream.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization 18 | 19 | import java.io.{InputStream, ObjectInputStream, ObjectStreamClass} 20 | 21 | import scala.reflect.ClassTag 22 | import scala.util.control.NonFatal 23 | 24 | /** Object input stream which tries the thread local class loader. 25 | * 26 | * Thread Local class loader is used by SBT to avoid polluting system class loader when running different tasks. 27 | * 28 | * This allows deserialization of classes from sub-projects during something like Play's test/run modes. 29 | */ 30 | private[serialization] class GenericCodecObjectInputStream(classTag: ClassTag[_], in: InputStream) 31 | extends ObjectInputStream(in) { 32 | 33 | private def classTagClassLoader = 34 | classTag.runtimeClass.getClassLoader 35 | private def threadLocalClassLoader = 36 | Thread.currentThread().getContextClassLoader 37 | 38 | override protected def resolveClass(desc: ObjectStreamClass): Class[_] = { 39 | try classTagClassLoader.loadClass(desc.getName) 40 | catch { 41 | case NonFatal(_) => 42 | try super.resolveClass(desc) 43 | catch { 44 | case NonFatal(_) => 45 | threadLocalClassLoader.loadClass(desc.getName) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/binary/BinaryAnyRefCodecs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization.binary 18 | 19 | import scala.reflect.ClassTag 20 | 21 | trait BinaryAnyRefCodecs_1 { 22 | 23 | /* 24 | String and Array[Byte] extend java.io.Serializable, 25 | so this implicit needs to be lower priority than those in BinaryPrimitiveCodecs 26 | */ 27 | implicit def anyRefBinaryCodec[S <: java.io.Serializable](implicit ev: ClassTag[S]): BinaryCodec[S] = 28 | new JavaSerializationAnyRefCodec[S](ev) 29 | 30 | } 31 | 32 | trait BinaryAnyRefCodecs_0 extends BinaryAnyRefCodecs_1 33 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/binary/BinaryCodec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization.binary 18 | 19 | import scalacache.serialization.{Codec, Decoder, Encoder} 20 | 21 | trait BinaryEncoder[T] extends Encoder[T, Array[Byte]] { 22 | override def encode(value: T): Array[Byte] 23 | } 24 | 25 | trait BinaryDecoder[T] extends Decoder[T, Array[Byte]] { 26 | override def decode(value: Array[Byte]): Codec.DecodingResult[T] 27 | } 28 | 29 | trait BinaryCodec[T] extends Codec[T, Array[Byte]] with BinaryEncoder[T] with BinaryDecoder[T] { 30 | override def encode(value: T): Array[Byte] 31 | override def decode(bytes: Array[Byte]): Codec.DecodingResult[T] 32 | } 33 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/binary/BinaryPrimitiveCodecs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization.binary 18 | 19 | import scalacache.serialization.Codec._ 20 | 21 | /** Codecs for all the Java primitive types, plus String and Array[Byte] 22 | * 23 | * Credit: Shade @ https://github.com/alexandru/shade/blob/master/src/main/scala/shade/memcached/Codec.scala 24 | */ 25 | trait BinaryPrimitiveCodecs { 26 | 27 | implicit object IntBinaryCodec extends BinaryCodec[Int] { 28 | def encode(value: Int): Array[Byte] = 29 | Array( 30 | (value >>> 24).asInstanceOf[Byte], 31 | (value >>> 16).asInstanceOf[Byte], 32 | (value >>> 8).asInstanceOf[Byte], 33 | value.asInstanceOf[Byte] 34 | ) 35 | 36 | def decode(data: Array[Byte]): DecodingResult[Int] = tryDecode( 37 | (data(0).asInstanceOf[Int] & 255) << 24 | 38 | (data(1).asInstanceOf[Int] & 255) << 16 | 39 | (data(2).asInstanceOf[Int] & 255) << 8 | 40 | data(3).asInstanceOf[Int] & 255 41 | ) 42 | } 43 | 44 | implicit object DoubleBinaryCodec extends BinaryCodec[Double] { 45 | import java.lang.{Double => JvmDouble} 46 | def encode(value: Double): Array[Byte] = { 47 | val l = JvmDouble.doubleToLongBits(value) 48 | LongBinaryCodec.encode(l) 49 | } 50 | 51 | def decode(data: Array[Byte]): DecodingResult[Double] = { 52 | LongBinaryCodec 53 | .decode(data) 54 | .map(l => JvmDouble.longBitsToDouble(l)) 55 | } 56 | } 57 | 58 | implicit object FloatBinaryCodec extends BinaryCodec[Float] { 59 | import java.lang.{Float => JvmFloat} 60 | def encode(value: Float): Array[Byte] = { 61 | val i = JvmFloat.floatToIntBits(value) 62 | IntBinaryCodec.encode(i) 63 | } 64 | 65 | def decode(data: Array[Byte]): DecodingResult[Float] = { 66 | IntBinaryCodec 67 | .decode(data) 68 | .map(i => JvmFloat.intBitsToFloat(i)) 69 | } 70 | } 71 | 72 | implicit object LongBinaryCodec extends BinaryCodec[Long] { 73 | def encode(value: Long): Array[Byte] = 74 | Array( 75 | (value >>> 56).asInstanceOf[Byte], 76 | (value >>> 48).asInstanceOf[Byte], 77 | (value >>> 40).asInstanceOf[Byte], 78 | (value >>> 32).asInstanceOf[Byte], 79 | (value >>> 24).asInstanceOf[Byte], 80 | (value >>> 16).asInstanceOf[Byte], 81 | (value >>> 8).asInstanceOf[Byte], 82 | value.asInstanceOf[Byte] 83 | ) 84 | 85 | def decode(data: Array[Byte]): DecodingResult[Long] = tryDecode( 86 | (data(0).asInstanceOf[Long] & 255) << 56 | 87 | (data(1).asInstanceOf[Long] & 255) << 48 | 88 | (data(2).asInstanceOf[Long] & 255) << 40 | 89 | (data(3).asInstanceOf[Long] & 255) << 32 | 90 | (data(4).asInstanceOf[Long] & 255) << 24 | 91 | (data(5).asInstanceOf[Long] & 255) << 16 | 92 | (data(6).asInstanceOf[Long] & 255) << 8 | 93 | data(7).asInstanceOf[Long] & 255 94 | ) 95 | } 96 | 97 | implicit object BooleanBinaryCodec extends BinaryCodec[Boolean] { 98 | def encode(value: Boolean): Array[Byte] = 99 | Array((if (value) 1 else 0).asInstanceOf[Byte]) 100 | 101 | def decode(data: Array[Byte]): DecodingResult[Boolean] = 102 | tryDecode(data.isDefinedAt(0) && data(0) == 1) 103 | } 104 | 105 | implicit object CharBinaryCodec extends BinaryCodec[Char] { 106 | def encode(value: Char): Array[Byte] = Array( 107 | (value >>> 8).asInstanceOf[Byte], 108 | value.asInstanceOf[Byte] 109 | ) 110 | 111 | def decode(data: Array[Byte]): DecodingResult[Char] = tryDecode( 112 | ((data(0).asInstanceOf[Int] & 255) << 8 | 113 | data(1).asInstanceOf[Int] & 255) 114 | .asInstanceOf[Char] 115 | ) 116 | } 117 | 118 | implicit object ShortBinaryCodec extends BinaryCodec[Short] { 119 | def encode(value: Short): Array[Byte] = Array( 120 | (value >>> 8).asInstanceOf[Byte], 121 | value.asInstanceOf[Byte] 122 | ) 123 | 124 | def decode(data: Array[Byte]): DecodingResult[Short] = tryDecode( 125 | ((data(0).asInstanceOf[Short] & 255) << 8 | 126 | data(1).asInstanceOf[Short] & 255) 127 | .asInstanceOf[Short] 128 | ) 129 | } 130 | 131 | implicit object StringBinaryCodec extends BinaryCodec[String] { 132 | def encode(value: String): Array[Byte] = value.getBytes("UTF-8") 133 | def decode(data: Array[Byte]): DecodingResult[String] = tryDecode(new String(data, "UTF-8")) 134 | } 135 | 136 | implicit object ArrayByteBinaryCodec extends BinaryCodec[Array[Byte]] { 137 | def encode(value: Array[Byte]): Array[Byte] = value 138 | def decode(data: Array[Byte]): DecodingResult[Array[Byte]] = Right(data) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/binary/JavaSerializationAnyRefCodec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization.binary 18 | 19 | import java.io._ 20 | 21 | import scala.reflect.ClassTag 22 | import scala.util.control.NonFatal 23 | import scalacache.serialization.Codec.DecodingResult 24 | import scalacache.serialization.{Codec, GenericCodecObjectInputStream} 25 | 26 | /** Codec that uses Java serialization to serialize objects 27 | * 28 | * Credit: Shade @ https://github.com/alexandru/shade/blob/master/src/main/scala/shade/memcached/Codec.scala 29 | */ 30 | class JavaSerializationAnyRefCodec[S <: Serializable](classTag: ClassTag[S]) extends BinaryCodec[S] { 31 | 32 | def using[T <: Closeable, R](obj: T)(f: T => R): R = 33 | try f(obj) 34 | finally try obj.close() 35 | catch { 36 | case NonFatal(_) => // does nothing 37 | } 38 | 39 | def encode(value: S): Array[Byte] = 40 | using(new ByteArrayOutputStream()) { buf => 41 | using(new ObjectOutputStream(buf)) { out => 42 | out.writeObject(value) 43 | out.close() 44 | buf.toByteArray 45 | } 46 | } 47 | 48 | def decode(data: Array[Byte]): DecodingResult[S] = 49 | Codec.tryDecode { 50 | using(new ByteArrayInputStream(data)) { buf => 51 | val in = new GenericCodecObjectInputStream(classTag, buf) 52 | using(in) { inp => 53 | inp.readObject().asInstanceOf[S] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/binary/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization 18 | 19 | package object binary extends BinaryPrimitiveCodecs with BinaryAnyRefCodecs_0 20 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/gzip/GZippingJavaSerializationAnyRefCodec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization.gzip 18 | 19 | import java.io.Serializable 20 | 21 | import scala.reflect.ClassTag 22 | import scalacache.serialization.binary.JavaSerializationAnyRefCodec 23 | 24 | object GZippingJavaSerializationAnyRefCodec { 25 | 26 | /** Compressing Java generic codec with a threshold of 16K 27 | */ 28 | implicit def default[S <: Serializable](implicit ev: ClassTag[S]): GZippingJavaSerializationAnyRefCodec[S] = 29 | new GZippingJavaSerializationAnyRefCodec(CompressingCodec.DefaultSizeThreshold)(ev) 30 | 31 | } 32 | 33 | class GZippingJavaSerializationAnyRefCodec[S <: Serializable](override val sizeThreshold: Int)(implicit 34 | classTag: ClassTag[S] 35 | ) extends JavaSerializationAnyRefCodec[S](classTag) 36 | with GZippingBinaryCodec[S] 37 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/serialization/gzip/GzippingBinaryCodec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.serialization.gzip 18 | 19 | import java.io.{ByteArrayInputStream, ByteArrayOutputStream} 20 | import java.util.zip.{GZIPInputStream, GZIPOutputStream} 21 | import scalacache.serialization.Codec.DecodingResult 22 | import scalacache.serialization.binary.BinaryCodec 23 | import scalacache.serialization.{Codec, FailedToDecode} 24 | 25 | object CompressingCodec { 26 | 27 | /** Default threshold for compression is is 16k 28 | */ 29 | val DefaultSizeThreshold: Int = 16384 30 | 31 | /** Headers aka magic numbers to let us know if something has been compressed or not 32 | */ 33 | object Headers { 34 | val Uncompressed: Byte = 0 35 | val Gzipped: Byte = 1 36 | } 37 | 38 | } 39 | 40 | /** Mixing this into any Codec will automatically GZip the resulting Byte Array when serialising and handle un-Gzipping 41 | * when deserialising 42 | */ 43 | trait GZippingBinaryCodec[A] extends BinaryCodec[A] { 44 | 45 | import CompressingCodec._ 46 | 47 | /** Size above which data will get compressed 48 | */ 49 | protected def sizeThreshold: Int = CompressingCodec.DefaultSizeThreshold 50 | 51 | abstract override def encode(value: A): Array[Byte] = { 52 | val serialised = super.encode(value) 53 | if (serialised.length > sizeThreshold) { 54 | Headers.Gzipped +: compress(serialised) 55 | } else { 56 | Headers.Uncompressed +: serialised 57 | } 58 | } 59 | 60 | abstract override def decode(data: Array[Byte]): DecodingResult[A] = { 61 | val firstByte = data.headOption 62 | firstByte match { 63 | case Some(Headers.Uncompressed) => 64 | super.decode(data.tail) 65 | case Some(Headers.Gzipped) => 66 | val bytes = Codec.tryDecode(decompress(data)) 67 | bytes.flatMap(super.decode) 68 | case unexpected => 69 | Left( 70 | FailedToDecode( 71 | new RuntimeException(s"Expected either ${Headers.Uncompressed} or ${Headers.Gzipped} but got $unexpected") 72 | ) 73 | ) 74 | } 75 | } 76 | 77 | // Port of compress in SpyMemcached 78 | private def compress(data: Array[Byte]): Array[Byte] = { 79 | val byteOutputStream = new ByteArrayOutputStream() 80 | val gzipOutputStream = new GZIPOutputStream(byteOutputStream) 81 | try { 82 | gzipOutputStream.write(data) 83 | } finally { 84 | gzipOutputStream.close() 85 | byteOutputStream.close() 86 | } 87 | byteOutputStream.toByteArray 88 | } 89 | 90 | // Port of decompress in SpyMemcached 91 | private def decompress(data: Array[Byte]): Array[Byte] = { 92 | val bis = new ByteArrayInputStream(data, 1, data.length - 1) 93 | val gis = new GZIPInputStream(bis) 94 | val bos = new ByteArrayOutputStream 95 | val buf = new Array[Byte](4 * 1024) 96 | try { 97 | var r = gis.read(buf) 98 | while (r > 0) { 99 | bos.write(buf, 0, r) 100 | r = gis.read(buf) 101 | } 102 | } finally { 103 | gis.close() 104 | bis.close() 105 | bos.close() 106 | } 107 | bos.toByteArray 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/scalacache/syntax.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | object syntax { 20 | 21 | final class RemoveAll[K, V] { 22 | def apply[F[_]]()(implicit cache: Cache[F, K, V]): F[Unit] = cache.removeAll 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/issue42/Issue42Spec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package issue42 18 | 19 | import scala.util.Random 20 | import cats.effect.SyncIO 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | import org.scalatest.matchers.should.Matchers 23 | 24 | class Issue42Spec extends AnyFlatSpec with Matchers { 25 | 26 | case class User(id: Int, name: String) 27 | 28 | import scalacache._ 29 | import memoization._ 30 | 31 | import concurrent.duration._ 32 | 33 | implicit val cache: Cache[SyncIO, String, User] = new MockCache() 34 | 35 | def generateNewName() = Random.alphanumeric.take(10).mkString 36 | 37 | def getUser(id: Int)(implicit flags: Flags): User = 38 | memoize(None) { 39 | User(id, generateNewName()) 40 | }.unsafeRunSync() 41 | 42 | def getUserWithTtl(id: Int)(implicit flags: Flags): User = 43 | memoize(Some(1 days)) { 44 | User(id, generateNewName()) 45 | }.unsafeRunSync() 46 | 47 | "memoize without TTL" should "respect implicit flags" in { 48 | val user1before = getUser(1) 49 | val user1after = { 50 | implicit val flags = Flags(readsEnabled = false) 51 | getUser(1) 52 | } 53 | user1before should not be user1after 54 | } 55 | 56 | "memoize with TTL" should "respect implicit flags" in { 57 | val user1before = getUserWithTtl(1) 58 | val user1after = { 59 | implicit val flags = Flags(readsEnabled = false) 60 | getUserWithTtl(1) 61 | } 62 | user1before should not be user1after 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/sample/Sample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package sample 18 | 19 | import scalacache._ 20 | import memoization._ 21 | 22 | import scala.concurrent.duration._ 23 | 24 | import cats.effect.IO 25 | 26 | case class User(id: Int, name: String) 27 | 28 | /** Sample showing how to use ScalaCache. 29 | */ 30 | object Sample extends App { 31 | 32 | class UserRepository { 33 | implicit val cache: Cache[IO, String, User] = new MockCache() 34 | 35 | def getUser(id: Int): IO[User] = memoizeF(None) { 36 | // Do DB lookup here... 37 | IO { User(id, s"user$id") } 38 | } 39 | 40 | def withExpiry(id: Int): IO[User] = memoizeF(Some(60 seconds)) { 41 | // Do DB lookup here... 42 | IO { User(id, s"user$id") } 43 | } 44 | 45 | def withOptionalExpiry(id: Int): IO[User] = memoizeF(Some(60 seconds)) { 46 | IO { User(id, s"user$id") } 47 | } 48 | 49 | def withOptionalExpiryNone(id: Int): IO[User] = memoizeF(None) { 50 | IO { User(id, s"user$id") } 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/memoization/CacheKeyExcludingConstructorParamsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import cats.effect.SyncIO 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import scalacache.memoization.MethodCallToStringConverter.excludeClassConstructorParams 22 | 23 | class CacheKeyExcludingConstructorParamsSpec extends AnyFlatSpec with CacheKeySpecCommon { self => 24 | 25 | behavior of "cache key generation for method memoization (not including constructor params in cache key)" 26 | 27 | override implicit lazy val config: MemoizationConfig = 28 | MemoizationConfig(toStringConverter = excludeClassConstructorParams) 29 | 30 | it should "not include the enclosing class's constructor params in the cache key" in { 31 | val instance1 = new ClassWithConstructorParams[SyncIO](50) 32 | instance1.cache = cache 33 | instance1.config = config 34 | 35 | val instance2 = new ClassWithConstructorParams[SyncIO](100) 36 | instance2.cache = cache 37 | instance2.config = config 38 | 39 | checkCacheKey("scalacache.memoization.ClassWithConstructorParams.foo(42)") { 40 | instance1.foo(42).unsafeRunSync() 41 | } 42 | 43 | checkCacheKey("scalacache.memoization.ClassWithConstructorParams.foo(42)") { 44 | instance2.foo(42).unsafeRunSync() 45 | } 46 | } 47 | 48 | it should "include values of all arguments for all argument lists" in { 49 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.multipleArgLists(1, 2)(3, 4)") { 50 | multipleArgLists(1, "2")("3", 4) 51 | } 52 | } 53 | 54 | it should "call toString on arguments to convert them into a string" in { 55 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.takesCaseClass(custom toString)") { 56 | takesCaseClass(CaseClass(1)).unsafeRunSync() 57 | } 58 | } 59 | 60 | it should "include values of lazy arguments" in { 61 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.lazyArg(1)") { 62 | lazyArg(1).unsafeRunSync() 63 | } 64 | } 65 | 66 | it should "exclude values of arguments annotated with @cacheKeyExclude" in { 67 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.withExcludedParams(1, 3)()") { 68 | withExcludedParams(1, "2", "3")(4).unsafeRunSync() 69 | } 70 | } 71 | 72 | it should "work for a method inside a class" in { 73 | checkCacheKey("scalacache.memoization.AClass.insideClass(1)") { 74 | new AClass[SyncIO]().insideClass(1).unsafeRunSync() 75 | } 76 | } 77 | 78 | it should "work for a method inside a trait" in { 79 | checkCacheKey("scalacache.memoization.ATrait.insideTrait(1)") { 80 | new ATrait[SyncIO] { val cache = self.cache; val config = self.config }.insideTrait(1).unsafeRunSync() 81 | } 82 | } 83 | 84 | it should "work for a method inside an object" in { 85 | AnObject.cache = this.cache 86 | checkCacheKey("scalacache.memoization.AnObject.insideObject(1)") { 87 | AnObject.insideObject(1).unsafeRunSync() 88 | } 89 | } 90 | 91 | it should "work for a method inside a class inside a class" in { 92 | checkCacheKey("scalacache.memoization.AClass.InnerClass.insideInnerClass(1)") { 93 | new AClass[SyncIO]().inner.insideInnerClass(1).unsafeRunSync() 94 | } 95 | } 96 | 97 | it should "work for a method inside an object inside a class" in { 98 | checkCacheKey("scalacache.memoization.AClass.InnerObject.insideInnerObject(1)") { 99 | new AClass[SyncIO]().InnerObject.insideInnerObject(1).unsafeRunSync() 100 | } 101 | } 102 | 103 | it should "work for a method inside a package object" in { 104 | pkg.cache = this.cache 105 | checkCacheKey("scalacache.memoization.pkg.package.insidePackageObject(1)") { 106 | pkg.insidePackageObject(1).unsafeRunSync() 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/memoization/CacheKeyIncludingConstructorParamsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import cats.effect.SyncIO 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import scalacache.memoization.MethodCallToStringConverter._ 22 | 23 | class CacheKeyIncludingConstructorParamsSpec extends AnyFlatSpec with CacheKeySpecCommon { self => 24 | 25 | behavior of "cache key generation for method memoization (when including constructor params in cache key)" 26 | 27 | implicit override lazy val config: MemoizationConfig = 28 | MemoizationConfig(toStringConverter = includeClassConstructorParams) 29 | 30 | it should "include the enclosing class's constructor params in the cache key" in { 31 | val instance = new ClassWithConstructorParams[SyncIO](50) 32 | instance.cache = cache 33 | instance.config = config 34 | 35 | checkCacheKey("scalacache.memoization.ClassWithConstructorParams(50).foo(42)") { 36 | instance.foo(42).unsafeRunSync() 37 | } 38 | } 39 | 40 | it should "exclude values of constructor params annotated with @cacheKeyExclude" in { 41 | val instance = new ClassWithExcludedConstructorParam[SyncIO](50, 10) 42 | instance.cache = cache 43 | instance.config = config 44 | 45 | checkCacheKey("scalacache.memoization.ClassWithExcludedConstructorParam(50).foo(42)") { 46 | instance.foo(42).unsafeRunSync() 47 | } 48 | } 49 | 50 | it should "include values of all arguments for all argument lists" in { 51 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.multipleArgLists(1, 2)(3, 4)") { 52 | multipleArgLists(1, "2")("3", 4) 53 | } 54 | } 55 | 56 | it should "call toString on arguments to convert them into a string" in { 57 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.takesCaseClass(custom toString)") { 58 | takesCaseClass(CaseClass(1)).unsafeRunSync() 59 | } 60 | } 61 | 62 | it should "include values of lazy arguments" in { 63 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.lazyArg(1)") { 64 | lazyArg(1).unsafeRunSync() 65 | } 66 | } 67 | 68 | it should "exclude values of arguments annotated with @cacheKeyExclude" in { 69 | checkCacheKey("scalacache.memoization.CacheKeySpecCommon.withExcludedParams(1, 3)()") { 70 | withExcludedParams(1, "2", "3")(4).unsafeRunSync() 71 | } 72 | } 73 | 74 | it should "work for a method inside a class" in { 75 | // The class's implicit param (the Cache) should be included in the cache key) 76 | checkCacheKey(s"scalacache.memoization.AClass()(${cache.toString}, ${config.toString}).insideClass(1)") { 77 | new AClass[SyncIO]().insideClass(1).unsafeRunSync() 78 | } 79 | } 80 | 81 | it should "work for a method inside a trait" in { 82 | checkCacheKey("scalacache.memoization.ATrait.insideTrait(1)") { 83 | new ATrait[SyncIO] { val cache = self.cache; val config = self.config }.insideTrait(1).unsafeRunSync() 84 | } 85 | } 86 | 87 | it should "work for a method inside an object" in { 88 | AnObject.cache = this.cache 89 | AnObject.config = this.config 90 | checkCacheKey("scalacache.memoization.AnObject.insideObject(1)") { 91 | AnObject.insideObject(1).unsafeRunSync() 92 | } 93 | } 94 | 95 | it should "work for a method inside a class inside a class" in { 96 | checkCacheKey("scalacache.memoization.AClass.InnerClass.insideInnerClass(1)") { 97 | new AClass[SyncIO]().inner.insideInnerClass(1).unsafeRunSync() 98 | } 99 | } 100 | 101 | it should "work for a method inside an object inside a class" in { 102 | checkCacheKey("scalacache.memoization.AClass.InnerObject.insideInnerObject(1)") { 103 | new AClass[SyncIO]().InnerObject.insideInnerObject(1).unsafeRunSync() 104 | } 105 | } 106 | 107 | it should "work for a method inside a package object" in { 108 | pkg.cache = this.cache 109 | checkCacheKey("scalacache.memoization.pkg.package.insidePackageObject(1)") { 110 | pkg.insidePackageObject(1).unsafeRunSync() 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/memoization/CacheKeyIncludingOnlyMethodParamsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import org.scalatest.flatspec.AnyFlatSpec 20 | import scalacache.memoization.MethodCallToStringConverter.onlyMethodParams 21 | 22 | class CacheKeyIncludingOnlyMethodParamsSpec extends AnyFlatSpec with CacheKeySpecCommon { 23 | 24 | behavior of "cache key generation for method memoization (only including method params in cache key)" 25 | 26 | override implicit lazy val config: MemoizationConfig = MemoizationConfig(toStringConverter = onlyMethodParams) 27 | 28 | it should "include values of all arguments for all argument lists" in { 29 | checkCacheKey("(1, 2)(3, 4)") { 30 | multipleArgLists(1, "2")("3", 4) 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/memoization/CacheKeySpecCommon.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import org.scalatest._ 20 | 21 | import scalacache._ 22 | import cats.effect.SyncIO 23 | import org.scalatest.matchers.should.Matchers 24 | import scala.annotation.nowarn 25 | 26 | trait CacheKeySpecCommon extends Suite with Matchers with BeforeAndAfter { 27 | 28 | implicit lazy val config: MemoizationConfig = scalacache.memoization.MemoizationConfig.defaultMemoizationConfig 29 | 30 | implicit lazy val cache: MockCache[SyncIO, Int] = new MockCache() 31 | 32 | before { 33 | cache.mmap.clear() 34 | } 35 | 36 | def checkCacheKey(expectedKey: String)(call: => Int): Assertion = { 37 | // Run the memoize block, putting some value into the cache 38 | val value = call 39 | 40 | // Check that the value is in the cache, with the expected key 41 | cache.get(expectedKey).unsafeRunSync() should be(Some(value)) 42 | } 43 | 44 | @nowarn 45 | def multipleArgLists(a: Int, b: String)(c: String, d: Int): Int = 46 | memoize(None) { 47 | 123 48 | }.unsafeRunSync() 49 | 50 | case class CaseClass(a: Int) { override def toString = "custom toString" } 51 | 52 | @nowarn 53 | def takesCaseClass(cc: CaseClass): SyncIO[Int] = memoize(None) { 54 | 123 55 | } 56 | 57 | @nowarn 58 | def lazyArg(a: => Int): SyncIO[Int] = memoize(None) { 59 | 123 60 | } 61 | 62 | @nowarn 63 | def functionArg(a: String => Int): SyncIO[Int] = memoize(None) { 64 | 123 65 | } 66 | 67 | @nowarn 68 | def withExcludedParams(a: Int, @cacheKeyExclude b: String, c: String)(@cacheKeyExclude d: Int): SyncIO[Int] = 69 | memoize(None) { 70 | 123 71 | } 72 | 73 | } 74 | 75 | class AClass[F[_]]()(implicit cache: Cache[F, String, Int], config: MemoizationConfig) { 76 | @nowarn 77 | def insideClass(a: Int): F[Int] = memoize(None) { 78 | 123 79 | } 80 | 81 | class InnerClass { 82 | @nowarn 83 | def insideInnerClass(a: Int): F[Int] = memoize(None) { 84 | 123 85 | } 86 | } 87 | val inner = new InnerClass 88 | 89 | object InnerObject { 90 | @nowarn 91 | def insideInnerObject(a: Int): F[Int] = memoize(None) { 92 | 123 93 | } 94 | } 95 | } 96 | 97 | trait ATrait[F[_]] { 98 | implicit val cache: Cache[F, String, Int] 99 | implicit val config: MemoizationConfig 100 | 101 | @nowarn 102 | def insideTrait(a: Int): F[Int] = memoize(None) { 103 | 123 104 | } 105 | } 106 | 107 | object AnObject { 108 | implicit var cache: Cache[SyncIO, String, Int] = null 109 | implicit var config: MemoizationConfig = null 110 | @nowarn 111 | def insideObject(a: Int): SyncIO[Int] = memoize(None) { 112 | 123 113 | } 114 | } 115 | 116 | class ClassWithConstructorParams[F[_]](b: Int) { 117 | implicit var cache: Cache[F, String, Int] = null 118 | implicit var config: MemoizationConfig = null 119 | def foo(a: Int): F[Int] = memoize(None) { 120 | a + b 121 | } 122 | } 123 | 124 | class ClassWithExcludedConstructorParam[F[_]](b: Int, @cacheKeyExclude c: Int) { 125 | implicit var cache: Cache[F, String, Int] = null 126 | implicit var config: MemoizationConfig = null 127 | def foo(a: Int): F[Int] = memoize(None) { 128 | a + b + c 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/memoization/MethodCallToStringConverterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import scalacache.memoization.MethodCallToStringConverter._ 20 | import org.scalatest.flatspec.AnyFlatSpec 21 | import org.scalatest.matchers.should.Matchers 22 | 23 | class MethodCallToStringConverterSpec extends AnyFlatSpec with Matchers { 24 | 25 | behavior of "excludeClassConstructorParams" 26 | 27 | it should "build a key for a no-arg method with no class" in { 28 | excludeClassConstructorParams.toString("", Vector.empty, "myMethod", Vector.empty) should be("myMethod") 29 | } 30 | 31 | it should "build a key for a no-arg method" in { 32 | excludeClassConstructorParams.toString("MyClass", Vector.empty, "myMethod", Vector.empty) should be( 33 | "MyClass.myMethod" 34 | ) 35 | } 36 | 37 | it should "build a key for a one-arg method" in { 38 | excludeClassConstructorParams.toString("MyClass", Vector.empty, "myMethod", Vector(Vector("foo"))) should be( 39 | "MyClass.myMethod(foo)" 40 | ) 41 | } 42 | 43 | it should "build a key for a two-arg method" in { 44 | excludeClassConstructorParams.toString("MyClass", Vector.empty, "myMethod", Vector(Vector("foo", 123))) should be( 45 | "MyClass.myMethod(foo, 123)" 46 | ) 47 | } 48 | 49 | it should "build a key for a method with multiple argument lists" in { 50 | excludeClassConstructorParams.toString( 51 | "MyClass", 52 | Vector.empty, 53 | "myMethod", 54 | Vector(Vector("foo", 123), Vector(3.4)) 55 | ) should be( 56 | "MyClass.myMethod(foo, 123)(3.4)" 57 | ) 58 | } 59 | 60 | it should "ignore class constructor arguments" in { 61 | excludeClassConstructorParams.toString("MyClass", Vector(Vector("foo", "bar")), "myMethod", Vector.empty) should be( 62 | "MyClass.myMethod" 63 | ) 64 | } 65 | 66 | behavior of "includeClassConstructorParams" 67 | 68 | it should "build a key for a method with multiple argument lists" in { 69 | includeClassConstructorParams.toString( 70 | "MyClass", 71 | Vector(Vector("foo", "bar"), Vector("baz")), 72 | "myMethod", 73 | Vector(Vector("foo", 123), Vector(3.4)) 74 | ) should be("MyClass(foo, bar)(baz).myMethod(foo, 123)(3.4)") 75 | } 76 | 77 | it should "build a key for a method in an object" in { 78 | includeClassConstructorParams.toString("MyObject", Vector.empty, "myMethod", Vector.empty) should be( 79 | "MyObject.myMethod" 80 | ) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/memoization/pkg/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memoization 18 | 19 | import scalacache._ 20 | import scala.annotation.nowarn 21 | 22 | package object pkg { 23 | 24 | import cats.effect.SyncIO 25 | 26 | implicit var cache: Cache[SyncIO, String, Int] = null 27 | 28 | @nowarn 29 | def insidePackageObject(a: Int): SyncIO[Int] = memoize(None) { 30 | 123 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/scalacache/mocks.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache 18 | 19 | import cats.effect.Sync 20 | import scalacache.logging.Logger 21 | import scalacache.memoization.MemoizationConfig 22 | 23 | import scala.collection.mutable.ArrayBuffer 24 | import scala.concurrent.duration.Duration 25 | import cats.syntax.functor._ 26 | 27 | class EmptyCache[F[_], V](implicit val F: Sync[F], val config: MemoizationConfig) extends AbstractCache[F, String, V] { 28 | 29 | override protected def logger = Logger.getLogger("EmptyCache") 30 | 31 | override protected def doGet(key: String) = 32 | F.pure(None) 33 | 34 | override protected def doPut(key: String, value: V, ttl: Option[Duration]) = 35 | F.unit 36 | 37 | override protected def doRemove(key: String) = 38 | F.unit 39 | 40 | override protected val doRemoveAll = 41 | F.unit 42 | 43 | override val close = F.unit 44 | 45 | } 46 | 47 | class FullCache[F[_], V](value: V)(implicit val F: Sync[F], val config: MemoizationConfig) 48 | extends AbstractCache[F, String, V] { 49 | 50 | override protected def logger = Logger.getLogger("FullCache") 51 | 52 | override protected def doGet(key: String) = 53 | F.pure(Some(value)) 54 | 55 | override protected def doPut(key: String, value: V, ttl: Option[Duration]) = 56 | F.unit 57 | 58 | override protected def doRemove(key: String) = 59 | F.unit 60 | 61 | override protected val doRemoveAll = 62 | F.unit 63 | 64 | override val close = F.unit 65 | 66 | } 67 | 68 | class ErrorRaisingCache[F[_], V](implicit val F: Sync[F], val config: MemoizationConfig) 69 | extends AbstractCache[F, String, V] { 70 | 71 | override protected val logger = Logger.getLogger("FullCache") 72 | 73 | override protected def doGet(key: String) = 74 | F.raiseError(new RuntimeException("failed to read")) 75 | 76 | override protected def doPut(key: String, value: V, ttl: Option[Duration]) = 77 | F.raiseError(new RuntimeException("failed to write")) 78 | 79 | override protected def doRemove(key: String) = 80 | F.unit 81 | 82 | override protected val doRemoveAll = 83 | F.unit 84 | 85 | override val close = F.unit 86 | 87 | } 88 | 89 | /** A mock cache for use in tests and samples. Does not support TTL. 90 | */ 91 | class MockCache[F[_], V](implicit val F: Sync[F], val config: MemoizationConfig) extends AbstractCache[F, String, V] { 92 | 93 | override protected def logger = Logger.getLogger("MockCache") 94 | 95 | val mmap = collection.mutable.Map[String, V]() 96 | 97 | override protected def doGet(key: String) = 98 | F.delay(mmap.get(key)) 99 | 100 | override protected def doPut(key: String, value: V, ttl: Option[Duration]) = 101 | F.delay(mmap.put(key, value)).void 102 | 103 | override protected def doRemove(key: String) = 104 | F.delay(mmap.remove(key)).void 105 | 106 | override protected val doRemoveAll = 107 | F.delay(mmap.clear()) 108 | 109 | override val close = F.unit 110 | 111 | } 112 | 113 | /** A cache that keeps track of the arguments it was called with. Useful for tests. Designed to be mixed in as a 114 | * stackable trait. 115 | */ 116 | trait LoggingCache[F[_], V] extends AbstractCache[F, String, V] { 117 | val F: Sync[F] 118 | 119 | var (getCalledWithArgs, putCalledWithArgs, removeCalledWithArgs) = 120 | (ArrayBuffer.empty[String], ArrayBuffer.empty[(String, Any, Option[Duration])], ArrayBuffer.empty[String]) 121 | 122 | protected abstract override def doGet(key: String): F[Option[V]] = F.defer { 123 | getCalledWithArgs.append(key) 124 | super.doGet(key) 125 | } 126 | 127 | protected abstract override def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = F.defer { 128 | putCalledWithArgs.append((key, value, ttl)) 129 | super.doPut(key, value, ttl) 130 | } 131 | 132 | protected abstract override def doRemove(key: String): F[Unit] = F.defer { 133 | removeCalledWithArgs.append(key) 134 | super.doRemove(key) 135 | } 136 | 137 | val reset: F[Unit] = F.delay { 138 | getCalledWithArgs.clear() 139 | putCalledWithArgs.clear() 140 | removeCalledWithArgs.clear() 141 | } 142 | 143 | } 144 | 145 | /** A mock cache that keeps track of the arguments it was called with. 146 | */ 147 | class LoggingMockCache[F[_]: Sync, V] extends MockCache[F, V] with LoggingCache[F, V] 148 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/docs/cache-implementations.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Cache implementations 4 | --- 5 | 6 | ## Cache implementations 7 | 8 | ### Memcached 9 | 10 | SBT: 11 | 12 | ``` 13 | libraryDependencies += "com.github.cb372" %% "scalacache-memcached" % "0.28.0" 14 | ``` 15 | 16 | Usage: 17 | 18 | ```scala mdoc:silent 19 | import scalacache._ 20 | import scalacache.memcached._ 21 | import scalacache.serialization.binary._ 22 | import cats.effect.IO 23 | 24 | implicit val memcachedCache: Cache[IO, String, String] = MemcachedCache("localhost:11211") 25 | ``` 26 | 27 | or provide your own Memcached client, like this: 28 | 29 | ```scala mdoc:silent 30 | import scalacache._ 31 | import scalacache.memcached._ 32 | import scalacache.serialization.binary._ 33 | import net.spy.memcached._ 34 | 35 | val memcachedClient = new MemcachedClient( 36 | new BinaryConnectionFactory(), 37 | AddrUtil.getAddresses("localhost:11211") 38 | ) 39 | implicit val customisedMemcachedCache: Cache[IO, String, String] = MemcachedCache(memcachedClient) 40 | ``` 41 | 42 | #### Keys 43 | 44 | Memcached only accepts ASCII keys with length <= 250 characters (see the [spec](https://github.com/memcached/memcached/blob/1.4.20/doc/protocol.txt#L41) for more details). 45 | 46 | ScalaCache provides two `KeySanitizer` implementations that convert your cache keys into valid Memcached keys. 47 | 48 | * `ReplaceAndTruncateSanitizer` simply replaces non-ASCII characters with underscores and truncates long keys to 250 chars. This sanitizer is convenient because it keeps your keys human-readable. Use it if you only expect ASCII characters to appear in cache keys and you don't use any massively long keys. 49 | 50 | * `HashingMemcachedKeySanitizer` uses a hash of your cache key, so it can turn any string into a valid Memcached key. The only downside is that it turns your keys into gobbledigook, which can make debugging a pain. 51 | 52 | ### Redis 53 | 54 | SBT: 55 | 56 | ``` 57 | libraryDependencies += "com.github.cb372" %% "scalacache-redis" % "0.28.0" 58 | ``` 59 | 60 | Usage: 61 | 62 | ```scala mdoc:silent 63 | import scalacache._ 64 | import scalacache.redis._ 65 | import scalacache.serialization.binary._ 66 | import cats.effect.IO 67 | 68 | implicit val redisCache: Cache[IO, String, String] = RedisCache("host1", 6379) 69 | ``` 70 | 71 | or provide your own [Jedis](https://github.com/xetorthio/jedis) client, like this: 72 | 73 | ```scala mdoc:silent 74 | import scalacache._ 75 | import scalacache.redis._ 76 | import scalacache.serialization.binary._ 77 | import _root_.redis.clients.jedis._ 78 | import cats.effect.IO 79 | 80 | val jedisPool = new JedisPool("localhost", 6379) 81 | implicit val customisedRedisCache: Cache[IO, String, String] = RedisCache(jedisPool) 82 | ``` 83 | 84 | ScalaCache also supports [sharded Redis](https://github.com/xetorthio/jedis/wiki/AdvancedUsage#shardedjedis) and [Redis Sentinel](http://redis.io/topics/sentinel). Just create a `ShardedRedisCache` or `SentinelRedisCache` respectively. 85 | 86 | ### Caffeine 87 | 88 | SBT: 89 | 90 | ``` 91 | libraryDependencies += "com.github.cb372" %% "scalacache-caffeine" % "0.28.0" 92 | ``` 93 | 94 | Usage: 95 | 96 | ```scala mdoc:silent 97 | import scalacache._ 98 | import scalacache.caffeine._ 99 | import cats.effect.{Clock, IO} 100 | import cats.effect.unsafe.implicits.global 101 | 102 | implicit val clock: Clock[IO] = Clock[IO] 103 | 104 | implicit val caffeineCache: Cache[IO, String, String] = CaffeineCache[IO, String, String].unsafeRunSync() 105 | ``` 106 | 107 | This will build a Caffeine cache with all the default settings. If you want to customize your Caffeine cache, then build it yourself and pass it to `CaffeineCache` like this: 108 | 109 | ```scala mdoc:silent 110 | import scalacache._ 111 | import scalacache.caffeine._ 112 | import com.github.benmanes.caffeine.cache.Caffeine 113 | import cats.effect.IO 114 | import cats.effect.unsafe.implicits.global 115 | 116 | val underlyingCaffeineCache = Caffeine.newBuilder().maximumSize(10000L).build[String, Entry[String]] 117 | implicit val customisedCaffeineCache: Cache[IO, String, String] = CaffeineCache(underlyingCaffeineCache) 118 | ``` 119 | 120 | ```scala mdoc:invisible 121 | for (cache <- List(redisCache, customisedRedisCache, memcachedCache, customisedMemcachedCache)) { 122 | cache.close.unsafeRunSync() 123 | } 124 | ``` 125 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/docs/flags.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Flags 4 | --- 5 | 6 | ### Flags 7 | 8 | Cache GETs and/or PUTs can be temporarily disabled using flags. This can be useful if for example you want to skip the cache and read a value from the DB under certain conditions. 9 | 10 | You can set flags by defining a [scalacache.Flags](https://github.com/cb372/scalacache/blob/master/modules/core/src/main/scala/scalacache/Flags.scala) instance in implicit scope. 11 | 12 | The detailed behaviour of the flags is as follows: 13 | 14 | * If `readsEnabled` = false, the cache will not be read, and ScalaCache will behave as if it was a cache miss. This means that memoization will compute the value (e.g. read it from a DB) and then write it to the cache. 15 | * If `writesEnabled` = false, in the case of a cache miss, the value will be computed (e.g. read from a DB) but it will not be written to the cache. 16 | * If both flags are false, memoization will not read from the cache or write to the cache. 17 | 18 | Note that your memoized method must take an implicit parameter of type `Flags`. Otherwise any flags you try to set using an implicit will be silently ignored. 19 | 20 | Example: 21 | 22 | ```scala mdoc:silent:reset-object 23 | import scalacache._ 24 | import scalacache.memcached._ 25 | import scalacache.memoization._ 26 | import scalacache.serialization.binary._ 27 | import cats.effect.IO 28 | import cats.effect.unsafe.implicits.global 29 | 30 | final case class Cat(id: Int, name: String, colour: String) 31 | 32 | implicit val catsCache: Cache[IO, String, Cat] = MemcachedCache("localhost:11211") 33 | 34 | def getCatWithFlags(id: Int)(implicit flags: Flags): Cat = memoize(None) { 35 | // Do DB lookup here... 36 | Cat(id, s"cat ${id}", "black") 37 | }.unsafeRunSync() 38 | 39 | def getCatMaybeSkippingCache(id: Int, skipCache: Boolean): Cat = { 40 | implicit val flags = Flags(readsEnabled = !skipCache) 41 | getCatWithFlags(id) 42 | } 43 | ``` 44 | 45 | Tip: Because the flags are passed as a parameter to your method, they will be included in the generated cache key. This means the cache key will vary depending on the value of the flags, which is probably not what you want. In that case, you should exclude the `implicit flags: Flags` parameter from cache key generation by annotating it with `@cacheKeyExclude`. 46 | 47 | ```scala mdoc:invisible 48 | for (cache <- List(catsCache)) { 49 | cache.close.unsafeRunSync() 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Getting started 4 | --- 5 | 6 | ## Getting Started 7 | 8 | ### Imports 9 | 10 | At the very least you will need to import the ScalaCache API. 11 | 12 | ```scala mdoc:silent:reset-object 13 | import scalacache._ 14 | import cats.effect.IO 15 | ``` 16 | 17 | Note that this import also brings a bunch of useful implicit magic into scope. 18 | 19 | ### Create a cache 20 | 21 | You'll need to choose a cache implementation. If you want a high performance in-memory cache, Caffeine is a good choice. For a distributed cache, shared between multiple instances of your application, you might want Redis or Memcached. 22 | 23 | Let's go with Memcached for this example, assuming that there is a Memcached server running on localhost. 24 | 25 | The constructor takes a type parameter, which is the type of the values you want to store in the cache. 26 | 27 | ```scala mdoc:silent 28 | import scalacache.memcached._ 29 | 30 | // We'll use the binary serialization codec - more on that later 31 | import scalacache.serialization.binary._ 32 | 33 | final case class Cat(id: Int, name: String, colour: String) 34 | 35 | implicit val catsCache: Cache[IO, String, Cat] = MemcachedCache("localhost:11211") 36 | ``` 37 | 38 | Note that we made the cache `implicit` so that the ScalaCache API can find it. 39 | 40 | ### Basic cache operations 41 | 42 | ```scala mdoc 43 | val ericTheCat = Cat(1, "Eric", "tuxedo") 44 | val doraemon = Cat(99, "Doraemon", "blue") 45 | 46 | // Add an item to the cache 47 | put("eric")(ericTheCat) 48 | 49 | // Add an item to the cache with a Time To Live 50 | import scala.concurrent.duration._ 51 | put("doraemon")(doraemon, ttl = Some(10.seconds)) 52 | 53 | // Retrieve the added item 54 | get("eric") 55 | 56 | // Remove it from the cache 57 | remove("doraemon") 58 | 59 | // Flush the cache 60 | removeAll[String, Cat] 61 | 62 | // Wrap any block with caching: if the key is not present in the cache, 63 | // the block will be executed and the value will be cached and returned 64 | caching("benjamin")(ttl = None) { 65 | // e.g. call an external API ... 66 | Cat(2, "Benjamin", "ginger") 67 | } 68 | 69 | // If the result of the block is wrapped in an effect, use cachingF 70 | cachingF("benjamin")(ttl = None) { 71 | IO.pure { 72 | // e.g. call an external API ... 73 | Cat(2, "Benjamin", "ginger") 74 | } 75 | } 76 | ``` 77 | 78 | ```scala mdoc:invisible 79 | import cats.effect.unsafe.implicits.global 80 | for (cache <- List(catsCache)) { 81 | cache.close.unsafeRunSync() 82 | } 83 | ``` 84 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/docs/memoization.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Memoization 4 | --- 5 | 6 | ### Memoization of method results 7 | 8 | ```scala mdoc:reset-object 9 | import scalacache._ 10 | import scalacache.memcached._ 11 | import scalacache.memoization._ 12 | 13 | import scalacache.serialization.binary._ 14 | 15 | import scala.concurrent.duration._ 16 | import cats.effect.IO 17 | 18 | final case class Cat(id: Int, name: String, colour: String) 19 | 20 | implicit val catsCache: Cache[IO, String, Cat] = MemcachedCache("localhost:11211") 21 | 22 | def getCat(id: Int): IO[Cat] = memoize(Some(10.seconds)) { 23 | // Retrieve data from a remote API here ... 24 | Cat(id, s"cat ${id}", "black") 25 | } 26 | 27 | getCat(123) 28 | ``` 29 | 30 | Did you spot the magic word 'memoize' in the `getCat` method? Just adding this keyword will cause the result of the method to be memoized to a cache. 31 | The next time you call the method with the same arguments the result will be retrieved from the cache and returned immediately. 32 | 33 | If the result of your block is wrapped in an effect container, use `memoizeF`: 34 | 35 | ```scala mdoc 36 | def getCatF(id: Int): IO[Cat] = memoizeF(Some(10.seconds)) { 37 | IO { 38 | // Retrieve data from a remote API here ... 39 | Cat(id, s"cat ${id}", "black") 40 | } 41 | } 42 | 43 | getCatF(123) 44 | ``` 45 | 46 | #### How it works 47 | 48 | `memoize` automatically builds a cache key based on the method being called, and the values of the arguments being passed to that method. 49 | 50 | Under the hood it makes use of Scala macros, so most of the information needed to build the cache key is gathered at compile time. No reflection or AOP magic is required at runtime. 51 | 52 | #### Cache key generation 53 | 54 | The cache key is built automatically from the class name, the name of the enclosing method, and the values of all of the method's parameters. 55 | 56 | For example, given the following method: 57 | 58 | ```scala 59 | 60 | object Bar { 61 | def baz(a: Int, b: String)(c: String): Int = memoizeF(None) { 62 | IO { 63 | // Reticulating splines... 64 | 123 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | the result of the method call 71 | 72 | ```scala 73 | val result = Bar.baz(1, "hello")("world") 74 | ``` 75 | 76 | would be cached with the key: `foo.bar.Baz(1, hello)(world)`. 77 | 78 | Note that the cache key generation logic is customizable. Just provide your own implementation of [MethodCallToStringConverter](https://github.com/cb372/scalacache/blob/master/modules/core/src/main/scala/scalacache/memoization/MethodCallToStringConverter.scala) 79 | 80 | #### Enclosing class's constructor arguments 81 | 82 | If your memoized method is inside a class, rather than an object, then the method's result might depend on values passed to that class's constructor. 83 | 84 | For example, if your code looks like this: 85 | 86 | ```scala 87 | package foo 88 | 89 | class Bar(a: Int) { 90 | 91 | def baz(b: Int): Int = memoizeSync(None) { 92 | a + b 93 | } 94 | 95 | } 96 | ``` 97 | 98 | then you want the cache key to depend on the values of both `a` and `b`. In that case, you need to use a different implementation of [MethodCallToStringConverter](https://github.com/cb372/scalacache/blob/master/modules/core/src/main/scala/scalacache/memoization/MethodCallToStringConverter.scala), like this: 99 | 100 | ```scala 101 | implicit val cacheConfig: CacheConfig = CacheConfig( 102 | memoization = MemoizationConfig(MethodCallToStringConverter.includeClassConstructorParams) 103 | ) 104 | ``` 105 | 106 | Doing this will ensure that both the constructor arguments and the method arguments are included in the cache key: 107 | 108 | ```scala 109 | new Bar(10).baz(42) // cached as "foo.Bar(10).baz(42)" -> 52 110 | new Bar(20).baz(42) // cached as "foo.Bar(20).baz(42)" -> 62 111 | ``` 112 | 113 | #### Excluding parameters from the generated cache key 114 | 115 | If there are any parameters (either method arguments or class constructor arguments) that you don't want to include in the auto-generated cache key for memoization, you can exclude them using the `@cacheKeyExclude` annotation. 116 | 117 | For example: 118 | 119 | ```scala 120 | def doSomething(userId: UserId)(implicit @cacheKeyExclude db: DBConnection): String = memoize { 121 | ... 122 | } 123 | ``` 124 | 125 | will only include the `userId` argument's value in its cache keys. 126 | 127 | ```scala mdoc:invisible 128 | import cats.effect.unsafe.implicits.global 129 | for (cache <- List(catsCache)) { 130 | cache.close.unsafeRunSync() 131 | } 132 | ``` 133 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/docs/restrictions.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Troubleshooting/Restrictions 4 | --- 5 | 6 | ## Troubleshooting/Restrictions 7 | 8 | Methods containing `memoize` blocks must have an explicit return type. 9 | If you don't specify the return type, you'll get a confusing compiler error along the lines of `recursive method withExpiry needs result type`. 10 | 11 | For example, this is OK 12 | 13 | ```scala 14 | def getUser(id: Int): Future[User] = memoize { 15 | // Do stuff... 16 | } 17 | ``` 18 | 19 | but this is not 20 | 21 | ```scala 22 | def getUser(id: Int) = memoize { 23 | // Do stuff... 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/docs/serialization.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: Serialization 4 | --- 5 | 6 | ## Serialization 7 | 8 | If you are using a cache implementation that does not store its data locally (like [Memcached](cache-implementations.html#memcached) and [Redis](cache-implementations.html#redis)), you will need to choose a codec in order to serialize your data to bytes. 9 | 10 | ### Binary codec 11 | 12 | ScalaCache provides efficient `Codec` instances for all primitive types, and also an implementation for objects based on Java serialization. 13 | 14 | To use this codec, you need one import: 15 | 16 | ```scala mdoc:fail:silent 17 | import scalacache.serialization.binary._ 18 | ``` 19 | 20 | ### JSON codec 21 | 22 | If you want to serialize your values as JSON, you can use ScalaCache's [circe](https://circe.github.io/circe/) integration. 23 | 24 | You will need to add a dependency on the scalacache-circe module: 25 | 26 | ``` 27 | libraryDependencies += "com.github.cb372" %% "scalacache-circe" % "0.28.0" 28 | ``` 29 | 30 | Then import the codec: 31 | 32 | ```scala mdoc:fail:silent 33 | import scalacache.serialization.circe._ 34 | ``` 35 | 36 | If your cache holds values of type `Cat`, you will also need a Circe `Encoder[Cat]` and `Decoder[Cat]` in implicit scope. The easiest way to do this is to ask circe to automatically derive them: 37 | 38 | ```scala 39 | import io.circe.generic.auto._ 40 | ``` 41 | 42 | but if you are worried about performance, it's better to derive them semi-automatically: 43 | 44 | ```scala 45 | import io.circe._ 46 | import io.circe.generic.semiauto._ 47 | implicit val catEncoder: Encoder[Cat] = deriveEncoder[Cat] 48 | implicit val catDecoder: Decoder[Cat] = deriveDecoder[Cat] 49 | ``` 50 | 51 | For more information, please consult the [circe docs](https://circe.github.io/circe/). 52 | 53 | ### Custom Codec 54 | 55 | If you want to use a custom `Codec` for your object of type `A`, simply implement an instance of `Codec[A]` and make sure it 56 | is in scope at your `get`/`put` call site. 57 | 58 | ### Compression of `Codec[A]` 59 | 60 | If you want to compress your serialized data before sending it to your cache, ScalaCache has a built-in `GZippingBinaryCodec[A]` mix-in 61 | trait that can be used to decorate another codec. It will automatically apply GZip compression to the encoded value if the `Array[Byte]` representation is above a `sizeThreshold`. It also takes care of properly decompressing data upon retrieval. 62 | 63 | To use it, simply extend your `Codec[A]` with `GZippingBinaryCodec[A]` **last** (it should be the right-most extended trait). 64 | 65 | If you want to use GZip compression with the standard ScalaCache binary codec can either `import scalacache.serialization.gzip.GZippingJavaSerializationCodec._` or provide an implicit `GZippingJavaAnyBinaryCodec` at the cache call site. 66 | -------------------------------------------------------------------------------- /modules/docs/src/main/mdoc/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: "ScalaCache" 4 | technologies: 5 | - first: ["Scala", "As the name implies ScalaCache is written in Scala."] 6 | --- 7 | 8 | [![Build Status](https://github.com/cb372/scalacache/workflows/Continuous%20Integration/badge.svg)](https://github.com/cb372/scalacache/actions) [![Maven Central](https://img.shields.io/maven-central/v/com.github.cb372/scalacache-core_2.12.svg)](http://search.maven.org/#search%7Cga%7C1%7Cscalacache) 9 | 10 | A facade for the most popular cache implementations, with a simple, idiomatic Scala API. 11 | 12 | Use ScalaCache to add caching to any Scala app with the minimum of fuss. 13 | 14 | The following cache implementations are supported, and it's easy to plugin your own implementation: 15 | * Memcached 16 | * Redis 17 | * [Caffeine](https://github.com/ben-manes/caffeine) 18 | 19 | ## Compatibility 20 | 21 | ScalaCache is available for Scala 2.11.x, 2.12.x, and 2.13.x. 22 | 23 | The JVM must be Java 8 or newer. 24 | -------------------------------------------------------------------------------- /modules/docs/src/main/resources/microsite/data/menu.yml: -------------------------------------------------------------------------------- 1 | options: 2 | - title: Getting started 3 | url: docs/index.html 4 | 5 | - title: Modes 6 | url: docs/modes.html 7 | 8 | - title: Synchronous API 9 | url: docs/sync-api.html 10 | 11 | - title: Memoization 12 | url: docs/memoization.html 13 | 14 | - title: Flags 15 | url: docs/flags.html 16 | 17 | - title: Serialization 18 | url: docs/serialization.html 19 | 20 | - title: Cache implementations 21 | url: docs/cache-implementations.html 22 | 23 | - title: Troubleshooting/Restrictions 24 | url: docs/restrictions.html 25 | -------------------------------------------------------------------------------- /modules/memcached/src/main/scala/scalacache/memcached/MemcachedCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memcached 18 | 19 | import cats.effect.Async 20 | import net.spy.memcached.internal.{GetCompletionListener, GetFuture, OperationCompletionListener, OperationFuture} 21 | import net.spy.memcached.ops.StatusCode 22 | import net.spy.memcached.{AddrUtil, BinaryConnectionFactory, MemcachedClient} 23 | import scalacache.AbstractCache 24 | import scalacache.logging.Logger 25 | import scalacache.serialization.binary.BinaryCodec 26 | 27 | import scala.concurrent.duration.Duration 28 | import scala.util.control.NonFatal 29 | 30 | class MemcachedException(message: String) extends Exception(message) 31 | 32 | /** Wrapper around spymemcached 33 | */ 34 | class MemcachedCache[F[_]: Async, V]( 35 | val client: MemcachedClient, 36 | val keySanitizer: MemcachedKeySanitizer = ReplaceAndTruncateSanitizer() 37 | )(implicit val codec: BinaryCodec[V]) 38 | extends AbstractCache[F, String, V] 39 | with MemcachedTTLConverter { 40 | 41 | protected def F: Async[F] = Async[F] 42 | 43 | override protected final val logger = 44 | Logger.getLogger[F](getClass.getName) 45 | 46 | override protected def doGet(key: String): F[Option[V]] = { 47 | F.async_ { cb => 48 | val f = client.asyncGet(keySanitizer.toValidMemcachedKey(key)) 49 | val _ = f.addListener(new GetCompletionListener { 50 | def onComplete(g: GetFuture[_]): Unit = { 51 | if (g.getStatus.isSuccess) { 52 | try { 53 | val bytes = g.get() 54 | val value = codec.decode(bytes.asInstanceOf[Array[Byte]]).map(Some(_)) 55 | cb(value) 56 | } catch { 57 | case NonFatal(e) => cb(Left(e)) 58 | } 59 | } else { 60 | g.getStatus.getStatusCode match { 61 | case StatusCode.ERR_NOT_FOUND => cb(Right(None)) 62 | case _ => cb(Left(new MemcachedException(g.getStatus.getMessage))) 63 | } 64 | 65 | } 66 | } 67 | }) 68 | } 69 | } 70 | 71 | override protected def doPut(key: String, value: V, ttl: Option[Duration]): F[Unit] = { 72 | F.async_ { cb => 73 | val valueToSend = codec.encode(value) 74 | val f = client.set(keySanitizer.toValidMemcachedKey(key), toMemcachedExpiry(ttl), valueToSend) 75 | val _ = f.addListener(new OperationCompletionListener { 76 | def onComplete(g: OperationFuture[_]): Unit = { 77 | if (g.getStatus.isSuccess) { 78 | logCachePut(key, ttl) 79 | cb(Right(())) 80 | } else { 81 | cb(Left(new MemcachedException(g.getStatus.getMessage))) 82 | } 83 | } 84 | }) 85 | } 86 | } 87 | 88 | override protected def doRemove(key: String): F[Unit] = { 89 | F.async_ { cb => 90 | val f = client.delete(key) 91 | val _ = f.addListener(new OperationCompletionListener { 92 | def onComplete(g: OperationFuture[_]): Unit = { 93 | if (g.getStatus.isSuccess) 94 | cb(Right(())) 95 | else 96 | cb(Left(new MemcachedException(g.getStatus.getMessage))) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | override protected def doRemoveAll: F[Unit] = { 103 | F.async_ { cb => 104 | val f = client.flush() 105 | val _ = f.addListener(new OperationCompletionListener { 106 | def onComplete(g: OperationFuture[_]): Unit = { 107 | if (g.getStatus.isSuccess) 108 | cb(Right(())) 109 | else 110 | cb(Left(new MemcachedException(g.getStatus.getMessage))) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | override def close: F[Unit] = F.delay(client.shutdown()) 117 | 118 | } 119 | 120 | object MemcachedCache { 121 | 122 | /** Create a Memcached client connecting to localhost:11211 and use it for caching 123 | */ 124 | def apply[F[_]: Async, V](implicit codec: BinaryCodec[V]): MemcachedCache[F, V] = 125 | apply("localhost:11211") 126 | 127 | /** Create a Memcached client connecting to the given host(s) and use it for caching 128 | * 129 | * @param addressString 130 | * Address string, with addresses separated by spaces, e.g. "host1:11211 host2:22322" 131 | */ 132 | def apply[F[_]: Async, V]( 133 | addressString: String 134 | )(implicit codec: BinaryCodec[V]): MemcachedCache[F, V] = 135 | apply(new MemcachedClient(new BinaryConnectionFactory(), AddrUtil.getAddresses(addressString))) 136 | 137 | /** Create a cache that uses the given Memcached client 138 | * 139 | * @param client 140 | * Memcached client 141 | */ 142 | def apply[F[_]: Async, V]( 143 | client: MemcachedClient 144 | )(implicit codec: BinaryCodec[V]): MemcachedCache[F, V] = 145 | new MemcachedCache[F, V](client) 146 | 147 | } 148 | -------------------------------------------------------------------------------- /modules/memcached/src/main/scala/scalacache/memcached/MemcachedKeySanitizer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memcached 18 | 19 | import scalacache._ 20 | 21 | /** Trait that you can use to define your own Memcached key sanitiser 22 | */ 23 | trait MemcachedKeySanitizer { 24 | 25 | /** Converts a string to a valid Memcached key 26 | */ 27 | def toValidMemcachedKey(key: String): String 28 | } 29 | 30 | /** Sanitizer that replaces characters invalid for Memcached and truncates keys if they are over a certain limit. 31 | * 32 | * Convenient because it creates human-readable keys, but only safe for ASCII chars. 33 | * 34 | * @param replacementChar 35 | * optional, defaults to an underscore 36 | * @param maxKeyLength 37 | * optional, defaults to 250, which is the max length of a Memcached key 38 | */ 39 | case class ReplaceAndTruncateSanitizer(replacementChar: String = "_", maxKeyLength: Int = 250) 40 | extends MemcachedKeySanitizer { 41 | 42 | val invalidCharsRegex = "[^\u0021-\u007e]".r 43 | 44 | /** Convert the given string to a valid Memcached key by: 45 | * - replacing all invalid characters with underscores 46 | * - truncating the string to 250 characters 47 | * 48 | * From the Memcached protocol spec: 49 | * 50 | * Data stored by memcached is identified with the help of a key. A key is a text string which should uniquely 51 | * identify the data for clients that are interested in storing and retrieving it. Currently the length limit of a 52 | * key is set at 250 characters (of course, normally clients wouldn't need to use such long keys); the key must not 53 | * include control characters or whitespace. 54 | * 55 | * Because of the structure of cache keys, the most useful information is likely to be at the right hand end, so 56 | * truncation is performed from the left. 57 | */ 58 | def toValidMemcachedKey(key: String): String = { 59 | val replacedKey = invalidCharsRegex.replaceAllIn(key, replacementChar) 60 | if (replacedKey.size <= maxKeyLength) replacedKey 61 | else replacedKey.substring(replacedKey.size - maxKeyLength) 62 | } 63 | 64 | } 65 | 66 | /** [[HashingMemcachedKeySanitizer]] uses the provided [[scalacache.HashingAlgorithm]] to create a valid Memcached key 67 | * using characters in hexadecimal. You may want to use this [[MemcachedKeySanitizer]] if there is a possibility that 68 | * your keys will contain non-ASCII characters. 69 | * 70 | * Make sure that the [[scalacache.HashingAlgorithm]] you provide does not produce strings that are beyond 250 71 | * characters when combined with any additional namespacing that your MemcachedClient or proxy automatically inserts 72 | * for you. 73 | */ 74 | case class HashingMemcachedKeySanitizer(algorithm: HashingAlgorithm = MD5) extends MemcachedKeySanitizer { 75 | 76 | /** Uses the specified hashing algorithm to digest a key and spit out a hexidecimal representation of the hashed key 77 | */ 78 | def toValidMemcachedKey(key: String): String = 79 | algorithm.messageDigest 80 | .digest(key.getBytes("UTF-8")) 81 | .map("%02x".format(_)) 82 | .mkString 83 | } 84 | -------------------------------------------------------------------------------- /modules/memcached/src/main/scala/scalacache/memcached/MemcachedTTLConverter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memcached 18 | 19 | import java.time.{Clock, Instant} 20 | 21 | import org.slf4j.LoggerFactory 22 | 23 | import scala.concurrent.duration._ 24 | 25 | trait MemcachedTTLConverter { 26 | private final val logger = LoggerFactory.getLogger(getClass.getName) 27 | 28 | /** Convert an optional `Duration` to an int suitable for passing to Memcached. 29 | * 30 | * From the Memcached protocol spec: 31 | * 32 | *

The actual value sent may either be Unix time (number of seconds since January 1, 1970, as a 33 | * 32-bit value), or a number of seconds starting from current time. In the latter case, this number of seconds may 34 | * not exceed 60*60*24*30 (number of seconds in 30 days); if the number sent by a client is larger than that, the 35 | * server will consider it to be real Unix time value rather than an offset from current time.

36 | * 37 | * @param ttl 38 | * optional TTL 39 | * @return 40 | * corresponding Memcached expiry 41 | */ 42 | def toMemcachedExpiry(ttl: Option[Duration])(implicit clock: Clock = Clock.systemUTC()): Int = { 43 | ttl.map(durationToExpiry).getOrElse(0) 44 | } 45 | 46 | private def durationToExpiry(duration: Duration)(implicit clock: Clock): Int = duration match { 47 | case Duration.Zero => 0 48 | 49 | case d if d < 1.second => { 50 | if (logger.isWarnEnabled) { 51 | logger.warn(s"Because Memcached does not support sub-second expiry, TTL of $d will be rounded up to 1 second") 52 | } 53 | 1 54 | } 55 | 56 | case d if d <= 30.days => d.toSeconds.toInt 57 | 58 | case d => { 59 | val expiryTime = Instant.now(clock).plusSeconds(d.toSeconds.toLong) 60 | (expiryTime.toEpochMilli / 1000).toInt 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /modules/memcached/src/test/scala/scalacache/memcached/MemcachedCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memcached 18 | 19 | import cats.effect.IO 20 | import net.spy.memcached._ 21 | import org.scalatest._ 22 | import org.scalatest.concurrent.{Eventually, IntegrationPatience, ScalaFutures} 23 | import org.scalatest.flatspec.AnyFlatSpec 24 | import org.scalatest.matchers.should.Matchers 25 | import org.scalatest.time.{Seconds, Span} 26 | import scalacache.serialization.binary._ 27 | 28 | import scala.concurrent.duration._ 29 | 30 | class MemcachedCacheSpec 31 | extends AnyFlatSpec 32 | with Matchers 33 | with Eventually 34 | with BeforeAndAfter 35 | with BeforeAndAfterAll 36 | with ScalaFutures 37 | with IntegrationPatience { 38 | 39 | val client = new MemcachedClient(AddrUtil.getAddresses("localhost:11211")) 40 | 41 | override def afterAll() = { 42 | client.shutdown() 43 | } 44 | 45 | import cats.effect.unsafe.implicits.global 46 | 47 | def memcachedIsRunning = { 48 | try { 49 | client.get("foo") 50 | true 51 | } catch { case _: Exception => false } 52 | } 53 | 54 | def serialise[A](v: A)(implicit codec: BinaryCodec[A]): Array[Byte] = 55 | codec.encode(v) 56 | 57 | if (!memcachedIsRunning) { 58 | alert("Skipping tests because Memcached does not appear to be running on localhost.") 59 | } else { 60 | 61 | before { 62 | client.flush() 63 | } 64 | 65 | behavior of "get" 66 | 67 | it should "return the value stored in Memcached" in { 68 | client.set("key1", 0, serialise(123)) 69 | whenReady(MemcachedCache[IO, Int](client).get("key1").unsafeToFuture()) { 70 | _ should be(Some(123)) 71 | } 72 | } 73 | 74 | it should "return None if the given key does not exist in the underlying cache" in { 75 | whenReady(MemcachedCache[IO, Int](client).get("non-existent-key").unsafeToFuture()) { 76 | _ should be(None) 77 | } 78 | } 79 | 80 | behavior of "put" 81 | 82 | it should "store the given key-value pair in the underlying cache" in { 83 | whenReady(MemcachedCache[IO, Int](client).put("key2")(123, None).unsafeToFuture()) { _ => 84 | client.get("key2") should be(serialise(123)) 85 | } 86 | } 87 | 88 | behavior of "put with TTL" 89 | 90 | it should "store the given key-value pair in the underlying cache" in { 91 | whenReady(MemcachedCache[IO, Int](client).put("key3")(123, Some(3.seconds)).unsafeToFuture()) { _ => 92 | client.get("key3") should be(serialise(123)) 93 | 94 | // Should expire after 3 seconds 95 | eventually(timeout(Span(4, Seconds))) { 96 | client.get("key3") should be(null) 97 | } 98 | } 99 | } 100 | 101 | behavior of "remove" 102 | 103 | it should "delete the given key and its value from the underlying cache" in { 104 | client.set("key1", 0, 123) 105 | client.get("key1") should be(123) 106 | 107 | whenReady(MemcachedCache[IO, Int](client).remove("key1").unsafeToFuture()) { _ => 108 | client.get("key1") should be(null) 109 | } 110 | } 111 | 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /modules/memcached/src/test/scala/scalacache/memcached/MemcachedKeySanitizerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memcached 18 | 19 | import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} 20 | 21 | import scala.concurrent.{ExecutionContext, Future} 22 | import scalacache._ 23 | import org.scalatest.flatspec.AnyFlatSpec 24 | import org.scalatest.matchers.should.Matchers 25 | 26 | class ReplaceAndTruncateSanitizerSpec extends AnyFlatSpec with Matchers { 27 | behavior of "ReplaceAndTruncateSanitizer" 28 | 29 | val sanitizer = new ReplaceAndTruncateSanitizer(maxKeyLength = 10) 30 | 31 | it should "truncate keys from the left if they are longer than maxKeyLength" in { 32 | val longKey = "0123456789A" 33 | sanitizer.toValidMemcachedKey(longKey) should be("123456789A") 34 | } 35 | 36 | it should "replace invalid chars with underscores" in { 37 | val invalidKey = "abc \t\r\nダメ" 38 | sanitizer.toValidMemcachedKey(invalidKey) should be("abc______") 39 | } 40 | 41 | it should "allow symbols in key" in { 42 | val validKey = "~`!@$" 43 | sanitizer.toValidMemcachedKey(validKey) should be(validKey) 44 | } 45 | 46 | } 47 | 48 | class HashingMemcachedKeySanitizerSpec extends AnyFlatSpec with Matchers with ScalaFutures with IntegrationPatience { 49 | behavior of "HashingMemcachedKeySanitizer" 50 | 51 | val longString = "lolol&%'(%$)$ほげほげ野郎123**+" * 500 52 | 53 | def hexToBytes(s: String): Array[Byte] = 54 | s.sliding(2, 2).map(Integer.parseInt(_, 16).toByte).toArray 55 | 56 | it should "return a hexadecimal hashed representation of the argument string" in { 57 | val hashedValues = for { 58 | algo <- Seq(MD5, SHA1, SHA256, SHA512) 59 | hashingSanitizer = HashingMemcachedKeySanitizer(algo) 60 | hashed = hashingSanitizer.toValidMemcachedKey(longString) 61 | } yield hashed 62 | hashedValues.foreach(hexToBytes) // should work 63 | hashedValues.forall(_.length < 250) should be(true) 64 | } 65 | 66 | it should "differentiate between strings made up of non-ASCII characters" in { 67 | val s1 = "毛泽东" 68 | val s2 = "김정일" 69 | val hashedPairs = for { 70 | algo <- Seq(MD5, SHA1, SHA256, SHA512) 71 | hashingSanitizer = HashingMemcachedKeySanitizer(algo) 72 | h1 = hashingSanitizer.toValidMemcachedKey(s1) 73 | h2 = hashingSanitizer.toValidMemcachedKey(s2) 74 | } yield (h1, h2) 75 | hashedPairs.forall(pair => pair._1 != pair._2) should be(true) 76 | } 77 | 78 | it should "return consistent sanitised keys for the same input even when used concurrently across multiple threads" in { 79 | implicit val ec = ExecutionContext.Implicits.global 80 | val seqFHashes = for { 81 | algo <- Seq(MD5, SHA1, SHA256, SHA512) 82 | hashingSanitizer = HashingMemcachedKeySanitizer(algo) 83 | } yield { 84 | Future.sequence((1 to 300).map(_ => Future { hashingSanitizer.toValidMemcachedKey(longString) })) 85 | } 86 | val fSeqHashes = Future.sequence(seqFHashes) 87 | whenReady(fSeqHashes) { hashess => 88 | hashess.foreach(hashes => hashes.distinct.size should be(1)) 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /modules/memcached/src/test/scala/scalacache/memcached/MemcachedTTLConverterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.memcached 18 | 19 | import java.time.temporal.ChronoUnit 20 | import java.time.{Clock, Instant, ZoneOffset} 21 | 22 | import scala.concurrent.duration._ 23 | import org.scalatest.flatspec.AnyFlatSpec 24 | import org.scalatest.matchers.should.Matchers 25 | 26 | class MemcachedTTLConverterSpec extends AnyFlatSpec with Matchers with MemcachedTTLConverter { 27 | behavior of "MemcachedTTLConverter" 28 | 29 | it should "convert None to 0" in { 30 | toMemcachedExpiry(None) should be(0) 31 | } 32 | 33 | it should "convert Some(Duration.Zero) to 0" in { 34 | toMemcachedExpiry(Some(Duration.Zero)) should be(0) 35 | } 36 | 37 | it should "round up Some(1.millisecond) to 1 second" in { 38 | toMemcachedExpiry(Some(1.millisecond)) should be(1) 39 | } 40 | 41 | it should "convert Some(1.second) to 1 second" in { 42 | toMemcachedExpiry(Some(1.second)) should be(1) 43 | } 44 | 45 | it should "convert Some(3.hours) to seconds" in { 46 | toMemcachedExpiry(Some(3.hours)) should be(3 * 60 * 60) 47 | } 48 | 49 | it should "convert Some(30.days) to seconds" in { 50 | toMemcachedExpiry(Some(30.days)) should be(30 * 24 * 60 * 60) 51 | } 52 | 53 | it should "convert a duration longer than 30 days to the expiry time expressed as UNIX epoch seconds" in { 54 | val now = Instant.now() 55 | val clock = Clock.fixed(now, ZoneOffset.UTC) 56 | toMemcachedExpiry(Some(31.days))(clock) should be(now.plus(31, ChronoUnit.DAYS).toEpochMilli / 1000) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /modules/redis/src/main/scala/scalacache/redis/RedisCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import cats.effect.Sync 20 | import cats.syntax.functor._ 21 | import redis.clients.jedis._ 22 | import scalacache.serialization.binary.BinaryCodec 23 | import scalacache.serialization.binary.BinaryEncoder 24 | 25 | /** Thin wrapper around Jedis 26 | */ 27 | class RedisCache[F[_]: Sync, K, V](val jedisPool: JedisPool)(implicit 28 | val keyEncoder: BinaryEncoder[K], 29 | val codec: BinaryCodec[V] 30 | ) extends RedisCacheBase[F, K, V] { 31 | 32 | protected def F: Sync[F] = Sync[F] 33 | type JClient = Jedis 34 | 35 | protected val doRemoveAll: F[Unit] = withJedis { jedis => 36 | F.delay(jedis.flushDB()).void 37 | } 38 | } 39 | 40 | object RedisCache { 41 | 42 | /** Create a Redis client connecting to the given host and use it for caching 43 | */ 44 | def apply[F[_]: Sync, K, V]( 45 | host: String, 46 | port: Int 47 | )(implicit keyEncoder: BinaryEncoder[K], codec: BinaryCodec[V]): RedisCache[F, K, V] = 48 | apply(new JedisPool(host, port)) 49 | 50 | /** Create a cache that uses the given Jedis client pool 51 | * @param jedisPool 52 | * a Jedis pool 53 | */ 54 | def apply[F[_]: Sync, K, V]( 55 | jedisPool: JedisPool 56 | )(implicit keyEncoder: BinaryEncoder[K], codec: BinaryCodec[V]): RedisCache[F, K, V] = 57 | new RedisCache[F, K, V](jedisPool) 58 | 59 | } 60 | -------------------------------------------------------------------------------- /modules/redis/src/main/scala/scalacache/redis/RedisCacheBase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import redis.clients.jedis.commands.BinaryJedisCommands 20 | import redis.clients.jedis.util.Pool 21 | 22 | import scalacache.logging.Logger 23 | import scalacache.serialization.Codec 24 | import scalacache.serialization.binary.{BinaryCodec, BinaryEncoder} 25 | 26 | import java.io.Closeable 27 | import scala.concurrent.duration._ 28 | import cats.effect.Resource 29 | import cats.syntax.all._ 30 | import scalacache.AbstractCache 31 | 32 | /** Contains implementations of all methods that can be implemented independent of the type of Redis client. This is 33 | * everything apart from `removeAll`, which needs to be implemented differently for sharded Redis. 34 | */ 35 | trait RedisCacheBase[F[_], K, V] extends AbstractCache[F, K, V] { 36 | 37 | override protected final val logger = Logger.getLogger[F](getClass.getName) 38 | 39 | protected type JClient <: BinaryJedisCommands with Closeable 40 | 41 | protected def jedisPool: Pool[JClient] 42 | 43 | protected def keyEncoder: BinaryEncoder[K] 44 | protected def codec: BinaryCodec[V] 45 | 46 | protected def doGet(key: K): F[Option[V]] = 47 | withJedis { jedis => 48 | val bytes = jedis.get(keyEncoder.encode(key)) 49 | val result: Codec.DecodingResult[Option[V]] = { 50 | if (bytes != null) 51 | codec.decode(bytes).map(Some(_)) 52 | else 53 | Right(None) 54 | } 55 | 56 | result match { 57 | case Left(e) => 58 | F.raiseError[Option[V]](e) 59 | case Right(maybeValue) => 60 | logCacheHitOrMiss(key, maybeValue).as(maybeValue) 61 | } 62 | } 63 | 64 | protected def doPut(key: K, value: V, ttl: Option[Duration]): F[Unit] = { 65 | withJedis { jedis => 66 | val keyBytes = keyEncoder.encode(key) 67 | val valueBytes = codec.encode(value) 68 | ttl match { 69 | case None => F.delay(jedis.set(keyBytes, valueBytes)) 70 | case Some(Duration.Zero) => F.delay(jedis.set(keyBytes, valueBytes)) 71 | case Some(d) if d < 1.second => 72 | logger.ifWarnEnabled { 73 | logger.warn( 74 | s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" 75 | ) 76 | } *> F.delay { 77 | jedis.setex(keyBytes, 1L, valueBytes) 78 | } 79 | case Some(d) => 80 | F.delay(jedis.setex(keyBytes, d.toSeconds.toLong, valueBytes)) 81 | } 82 | } *> logCachePut(key, ttl) 83 | } 84 | 85 | protected def doRemove(key: K): F[Unit] = { 86 | withJedis { jedis => 87 | F.delay(jedis.del(keyEncoder.encode(key))).void 88 | } 89 | } 90 | 91 | val close: F[Unit] = F.delay(jedisPool.close()) 92 | 93 | /** Borrow a Jedis client from the pool, perform some operation and then return the client to the pool. 94 | * 95 | * @param f 96 | * block that uses the Jedis client. 97 | * @tparam T 98 | * return type of the block 99 | * @return 100 | * the result of executing the block 101 | */ 102 | protected final def withJedis[T](f: JClient => F[T]): F[T] = { 103 | Resource.fromAutoCloseable(F.delay(jedisPool.getResource())).use(jedis => F.defer(f(jedis))) 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /modules/redis/src/main/scala/scalacache/redis/RedisClusterCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import scalacache.logging.Logger 20 | import scalacache.serialization.Codec 21 | import scalacache.serialization.binary.{BinaryCodec, BinaryEncoder} 22 | 23 | import scala.concurrent.duration.{Duration, _} 24 | import cats.implicits._ 25 | import cats.effect.Sync 26 | import redis.clients.jedis.JedisCluster 27 | import redis.clients.jedis.exceptions.JedisClusterException 28 | import scalacache.AbstractCache 29 | 30 | class RedisClusterCache[F[_]: Sync, K, V](val jedisCluster: JedisCluster)(implicit 31 | val keyEncoder: BinaryEncoder[K], 32 | val codec: BinaryCodec[V] 33 | ) extends AbstractCache[F, K, V] { 34 | 35 | protected def F: Sync[F] = Sync[F] 36 | 37 | override protected final val logger = Logger.getLogger(getClass.getName) 38 | 39 | override protected def doGet(key: K): F[Option[V]] = F.defer { 40 | val bytes = jedisCluster.get(keyEncoder.encode(key)) 41 | val result: Codec.DecodingResult[Option[V]] = { 42 | if (bytes != null) 43 | codec.decode(bytes).map(Some(_)) 44 | else 45 | Right(None) 46 | } 47 | 48 | result match { 49 | case Left(e) => 50 | F.raiseError[Option[V]](e) 51 | case Right(maybeValue) => 52 | logCacheHitOrMiss(key, maybeValue).as(maybeValue) 53 | } 54 | } 55 | 56 | override protected def doPut(key: K, value: V, ttl: Option[Duration]): F[Unit] = { 57 | val keyBytes = keyEncoder.encode(key) 58 | val valueBytes = codec.encode(value) 59 | ttl match { 60 | case None => F.delay(jedisCluster.set(keyBytes, valueBytes)).void 61 | case Some(Duration.Zero) => F.delay(jedisCluster.set(keyBytes, valueBytes)).void 62 | case Some(d) if d < 1.second => 63 | logger.ifWarnEnabled { 64 | logger.warn( 65 | s"Because Redis (pre 2.6.12) does not support sub-second expiry, TTL of $d will be rounded up to 1 second" 66 | ) 67 | } *> F.delay(jedisCluster.setex(keyBytes, 1L, valueBytes)).void 68 | case Some(d) => 69 | F.delay(jedisCluster.setex(keyBytes, d.toSeconds, valueBytes)).void 70 | } 71 | } 72 | 73 | override protected def doRemove(key: K): F[Unit] = F.delay { 74 | jedisCluster.del(keyEncoder.encode(key)) 75 | }.void 76 | 77 | @deprecated( 78 | "JedisCluster doesn't support this operation, scheduled to be removed with the next jedis major release", 79 | "0.28.0" 80 | ) 81 | override protected def doRemoveAll: F[Unit] = F.raiseError { 82 | new JedisClusterException("No way to dispatch this command to Redis Cluster.") 83 | } 84 | 85 | override val close: F[Unit] = F.delay(jedisCluster.close()) 86 | } 87 | -------------------------------------------------------------------------------- /modules/redis/src/main/scala/scalacache/redis/RedisSerialization.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import scalacache.serialization.Codec 20 | import scalacache.serialization.binary.BinaryCodec 21 | 22 | /** Custom serialization for caching arbitrary objects in Redis. Ints, Longs, Doubles, Strings and byte arrays are 23 | * treated specially. Everything else is serialized using standard Java serialization. 24 | */ 25 | trait RedisSerialization { 26 | 27 | def serialize[A](value: A)(implicit codec: BinaryCodec[A]): Array[Byte] = 28 | codec.encode(value) 29 | 30 | def deserialize[A](bytes: Array[Byte])(implicit codec: BinaryCodec[A]): Codec.DecodingResult[A] = 31 | codec.decode(bytes) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules/redis/src/main/scala/scalacache/redis/SentinelRedisCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import cats.effect.Sync 20 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig 21 | import redis.clients.jedis._ 22 | import scalacache.serialization.binary.BinaryCodec 23 | import scalacache.serialization.binary.BinaryEncoder 24 | 25 | import scala.jdk.CollectionConverters._ 26 | import cats.syntax.functor._ 27 | 28 | /** Thin wrapper around Jedis that works with Redis Sentinel. 29 | */ 30 | class SentinelRedisCache[F[_]: Sync, K, V](val jedisPool: JedisSentinelPool)(implicit 31 | val keyEncoder: BinaryEncoder[K], 32 | val codec: BinaryCodec[V] 33 | ) extends RedisCacheBase[F, K, V] { 34 | 35 | protected def F: Sync[F] = Sync[F] 36 | 37 | type JClient = Jedis 38 | 39 | protected def doRemoveAll: F[Unit] = 40 | withJedis { jedis => 41 | F.delay(jedis.flushDB()).void 42 | } 43 | 44 | } 45 | 46 | object SentinelRedisCache { 47 | 48 | /** Create a `SentinelRedisCache` that uses a `JedisSentinelPool` with a default pool config. 49 | * 50 | * @param clusterName 51 | * Name of the redis cluster 52 | * @param sentinels 53 | * set of sentinels in format [host1:port, host2:port] 54 | * @param password 55 | * password of the cluster 56 | */ 57 | def apply[F[_]: Sync, K, V](clusterName: String, sentinels: Set[String], password: String)(implicit 58 | keyEncoder: BinaryEncoder[K], 59 | codec: BinaryCodec[V] 60 | ): SentinelRedisCache[F, K, V] = 61 | apply(new JedisSentinelPool(clusterName, sentinels.asJava, new GenericObjectPoolConfig[Jedis], password)) 62 | 63 | /** Create a `SentinelRedisCache` that uses a `JedisSentinelPool` with a custom pool config. 64 | * 65 | * @param clusterName 66 | * Name of the redis cluster 67 | * @param sentinels 68 | * set of sentinels in format [host1:port, host2:port] 69 | * @param password 70 | * password of the cluster 71 | * @param poolConfig 72 | * config of the underlying pool 73 | */ 74 | def apply[F[_]: Sync, K, V]( 75 | clusterName: String, 76 | sentinels: Set[String], 77 | poolConfig: GenericObjectPoolConfig[Jedis], 78 | password: String 79 | )(implicit 80 | keyEncoder: BinaryEncoder[K], 81 | codec: BinaryCodec[V] 82 | ): SentinelRedisCache[F, K, V] = 83 | apply(new JedisSentinelPool(clusterName, sentinels.asJava, poolConfig, password)) 84 | 85 | /** Create a `SentinelRedisCache` that uses the given JedisSentinelPool 86 | * 87 | * @param jedisSentinelPool 88 | * a JedisSentinelPool 89 | */ 90 | def apply[F[_]: Sync, K, V]( 91 | jedisSentinelPool: JedisSentinelPool 92 | )(implicit keyEncoder: BinaryEncoder[K], codec: BinaryCodec[V]): SentinelRedisCache[F, K, V] = 93 | new SentinelRedisCache[F, K, V](jedisSentinelPool) 94 | 95 | } 96 | -------------------------------------------------------------------------------- /modules/redis/src/main/scala/scalacache/redis/ShardedRedisCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import cats.effect.Sync 20 | import redis.clients.jedis._ 21 | import scalacache.serialization.binary.{BinaryCodec, BinaryEncoder} 22 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig 23 | 24 | import scala.jdk.CollectionConverters._ 25 | 26 | /** Thin wrapper around Jedis that works with sharded Redis. 27 | */ 28 | class ShardedRedisCache[F[_]: Sync, K, V](val jedisPool: ShardedJedisPool)(implicit 29 | val keyEncoder: BinaryEncoder[K], 30 | val codec: BinaryCodec[V] 31 | ) extends RedisCacheBase[F, K, V] { 32 | 33 | protected def F: Sync[F] = Sync[F] 34 | 35 | type JClient = ShardedJedis 36 | 37 | protected val doRemoveAll: F[Unit] = withJedis { jedis => 38 | F.delay { 39 | jedis.getAllShards.asScala.foreach(_.flushDB()) 40 | } 41 | } 42 | 43 | } 44 | 45 | object ShardedRedisCache { 46 | 47 | /** Create a sharded Redis client connecting to the given hosts and use it for caching 48 | */ 49 | def apply[F[_]: Sync, K, V]( 50 | hosts: (String, Int)* 51 | )(implicit keyEncoder: BinaryEncoder[K], codec: BinaryCodec[V]): ShardedRedisCache[F, K, V] = { 52 | val shards = hosts.map { case (host, port) => 53 | new JedisShardInfo(host, port) 54 | } 55 | val pool = new ShardedJedisPool(new GenericObjectPoolConfig[ShardedJedis], shards.asJava) 56 | apply(pool) 57 | } 58 | 59 | /** Create a cache that uses the given ShardedJedis client pool 60 | * @param jedisPool 61 | * a ShardedJedis pool 62 | */ 63 | def apply[F[_]: Sync, K, V]( 64 | jedisPool: ShardedJedisPool 65 | )(implicit keyEncoder: BinaryEncoder[K], codec: BinaryCodec[V]): ShardedRedisCache[F, K, V] = 66 | new ShardedRedisCache[F, K, V](jedisPool) 67 | 68 | } 69 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/CaseClass.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | case class CaseClass(a: Int, b: String) extends Serializable 20 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/Issue32Spec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import org.scalatest.BeforeAndAfter 20 | import scalacache.memoization._ 21 | import scalacache.serialization.binary._ 22 | import cats.effect.IO 23 | import cats.effect.unsafe.implicits.global 24 | import org.scalatest.flatspec.AnyFlatSpec 25 | import org.scalatest.matchers.should.Matchers 26 | import scalacache.memoization.MemoizationConfig.defaultMemoizationConfig 27 | import scalacache.serialization.binary.StringBinaryCodec 28 | 29 | case class User(id: Int, name: String) 30 | 31 | /** Test to check the sample code in issue #32 runs OK (just to isolate the use of the List[User] type from the Play 32 | * classloader problem) 33 | */ 34 | class Issue32Spec extends AnyFlatSpec with Matchers with BeforeAndAfter with RedisTestUtil { 35 | 36 | assumingRedisIsRunning { (pool, _) => 37 | implicit val cache: RedisCache[IO, String, List[User]] = new RedisCache[IO, String, List[User]](pool) 38 | 39 | def getUser(id: Int): List[User] = 40 | memoize(None) { 41 | List(User(id, "Taro")) 42 | }.unsafeRunSync() 43 | 44 | "memoize and Redis" should "work with List[User]" in { 45 | getUser(1) should be(List(User(1, "Taro"))) 46 | getUser(1) should be(List(User(1, "Taro"))) 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/RedisCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import redis.clients.jedis._ 20 | import scalacache._ 21 | 22 | import cats.effect.IO 23 | import scalacache.serialization.binary.BinaryCodec 24 | import scalacache.serialization.binary.StringBinaryCodec 25 | 26 | class RedisCacheSpec extends RedisCacheSpecBase with RedisTestUtil { 27 | 28 | type JClient = JedisClient 29 | type JPool = JedisPool 30 | 31 | val withJedis = assumingRedisIsRunning _ 32 | 33 | def constructCache[V](pool: JPool)(implicit codec: BinaryCodec[V]): Cache[IO, String, V] = 34 | new RedisCache[IO, String, V](jedisPool = pool) 35 | 36 | def flushRedis(client: JClient): Unit = client.underlying.flushDB(): Unit 37 | 38 | runTestsIfPossible() 39 | 40 | } 41 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/RedisCacheSpecBase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import org.scalatest.concurrent.{Eventually, IntegrationPatience, ScalaFutures} 20 | import org.scalatest.time.{Seconds, Span} 21 | import org.scalatest.{BeforeAndAfter, Inside} 22 | import scalacache._ 23 | import scalacache.serialization.Codec.DecodingResult 24 | import scalacache.serialization.binary._ 25 | import scalacache.serialization.FailedToDecode 26 | 27 | import cats.effect.unsafe.implicits.global 28 | import scala.concurrent.Future 29 | import scala.concurrent.duration._ 30 | import cats.effect.IO 31 | import org.scalatest.flatspec.AnyFlatSpec 32 | import org.scalatest.matchers.should.Matchers 33 | 34 | trait RedisCacheSpecBase 35 | extends AnyFlatSpec 36 | with Matchers 37 | with Eventually 38 | with Inside 39 | with BeforeAndAfter 40 | with RedisSerialization 41 | with ScalaFutures 42 | with IntegrationPatience { 43 | 44 | type JPool 45 | type JClient <: BaseJedisClient 46 | 47 | case object AlwaysFailing 48 | implicit val alwaysFailingCodec: BinaryCodec[AlwaysFailing.type] = new BinaryCodec[AlwaysFailing.type] { 49 | override def encode(value: AlwaysFailing.type): Array[Byte] = Array(0) 50 | override def decode(bytes: Array[Byte]): DecodingResult[AlwaysFailing.type] = 51 | Left(FailedToDecode(new Exception("Failed to decode"))) 52 | } 53 | 54 | def withJedis: ((JPool, JClient) => Unit) => Unit 55 | def constructCache[V](pool: JPool)(implicit codec: BinaryCodec[V]): Cache[IO, String, V] 56 | def flushRedis(client: JClient): Unit 57 | 58 | def runTestsIfPossible() = { 59 | 60 | withJedis { (pool, client) => 61 | val cache = constructCache[Int](pool) 62 | val failingCache = constructCache[AlwaysFailing.type](pool) 63 | 64 | before { 65 | flushRedis(client) 66 | } 67 | 68 | behavior of "get" 69 | 70 | it should "return the value stored in Redis" in { 71 | client.set(bytes("key1"), serialize(123)) 72 | whenReady(cache.get("key1").unsafeToFuture()) { _ should be(Some(123)) } 73 | } 74 | 75 | it should "return None if the given key does not exist in the underlying cache" in { 76 | whenReady(cache.get("non-existent-key").unsafeToFuture()) { _ should be(None) } 77 | } 78 | 79 | it should "raise an error if decoding fails" in { 80 | client.set(bytes("key1"), serialize(123)) 81 | whenReady(failingCache.get("key1").unsafeToFuture().failed) { t => 82 | inside(t) { case FailedToDecode(e) => e.getMessage should be("Failed to decode") } 83 | } 84 | } 85 | 86 | behavior of "put" 87 | 88 | it should "store the given key-value pair in the underlying cache" in { 89 | whenReady(cache.put("key2")(123, None).unsafeToFuture()) { _ => 90 | deserialize[Int](client.get(bytes("key2"))) should be(Right(123)) 91 | } 92 | } 93 | 94 | behavior of "put with TTL" 95 | 96 | it should "store the given key-value pair in the underlying cache" in { 97 | whenReady(cache.put("key3")(123, Some(1 second)).unsafeToFuture()) { _ => 98 | deserialize[Int](client.get(bytes("key3"))) should be(Right(123)) 99 | 100 | // Should expire after 1 second 101 | eventually(timeout(Span(2, Seconds))) { 102 | client.get(bytes("key3")) should be(null) 103 | } 104 | } 105 | } 106 | 107 | behavior of "put with TTL of zero" 108 | 109 | it should "store the given key-value pair in the underlying cache with no expiry" in { 110 | whenReady(cache.put("key4")(123, Some(Duration.Zero)).unsafeToFuture()) { _ => 111 | deserialize[Int](client.get(bytes("key4"))) should be(Right(123)) 112 | client.ttl(bytes("key4")) should be(-1L) 113 | } 114 | } 115 | 116 | behavior of "put with TTL of less than 1 second" 117 | 118 | it should "store the given key-value pair in the underlying cache" in { 119 | whenReady(cache.put("key5")(123, Some(100 milliseconds)).unsafeToFuture()) { _ => 120 | deserialize[Int](client.get(bytes("key5"))) should be(Right(123)) 121 | client.pttl("key5").toLong should be > 0L 122 | 123 | // Should expire after 1 second 124 | eventually(timeout(Span(2, Seconds))) { 125 | client.get(bytes("key5")) should be(null) 126 | } 127 | } 128 | } 129 | 130 | behavior of "caching with serialization" 131 | 132 | def roundTrip[V](key: String, value: V)(implicit codec: BinaryCodec[V]): Future[Option[V]] = { 133 | val c = constructCache[V](pool) 134 | c.put(key)(value, None).flatMap(_ => c.get(key)).unsafeToFuture() 135 | } 136 | 137 | it should "round-trip a String" in { 138 | whenReady(roundTrip("string", "hello")) { _ should be(Some("hello")) } 139 | } 140 | 141 | it should "round-trip a byte array" in { 142 | whenReady(roundTrip("bytearray", bytes("world"))) { result => 143 | new String(result.get, "UTF-8") should be("world") 144 | } 145 | } 146 | 147 | it should "round-trip an Int" in { 148 | whenReady(roundTrip("int", 345)) { _ should be(Some(345)) } 149 | } 150 | 151 | it should "round-trip a Double" in { 152 | whenReady(roundTrip("double", 1.23)) { _ should be(Some(1.23)) } 153 | } 154 | 155 | it should "round-trip a Long" in { 156 | whenReady(roundTrip("long", 3456L)) { _ should be(Some(3456L)) } 157 | } 158 | 159 | it should "round-trip a Serializable case class" in { 160 | val cc = CaseClass(123, "wow") 161 | whenReady(roundTrip("caseclass", cc)) { _ should be(Some(cc)) } 162 | } 163 | 164 | behavior of "remove" 165 | 166 | it should "delete the given key and its value from the underlying cache" in { 167 | client.set(bytes("key1"), serialize(123)) 168 | deserialize[Int](client.get(bytes("key1"))) should be(Right(123)) 169 | 170 | whenReady(cache.remove("key1").unsafeToFuture()) { _ => 171 | client.get("key1") should be(null) 172 | } 173 | } 174 | 175 | } 176 | 177 | } 178 | 179 | def bytes(s: String) = s.getBytes("utf-8") 180 | 181 | } 182 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/RedisClusterCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import redis.clients.jedis._ 20 | import scalacache._ 21 | 22 | import scala.jdk.CollectionConverters._ 23 | import scala.util.{Failure, Success, Try} 24 | import cats.effect.IO 25 | import scalacache.serialization.binary.BinaryCodec 26 | import scalacache.serialization.binary.StringBinaryCodec 27 | import scala.annotation.nowarn 28 | 29 | class RedisClusterCacheSpec extends RedisCacheSpecBase with RedisTestUtil { 30 | 31 | type JClient = JedisClusterClient 32 | type JPool = JedisCluster 33 | 34 | override val withJedis = assumingRedisClusterIsRunning _ 35 | 36 | def constructCache[V](jedisCluster: JedisCluster)(implicit codec: BinaryCodec[V]): Cache[IO, String, V] = 37 | new RedisClusterCache[IO, String, V](jedisCluster) 38 | 39 | def flushRedis(client: JClient): Unit = { 40 | val _ = client.underlying.getClusterNodes.asScala.mapValues(_.getResource.flushDB()): @nowarn 41 | } 42 | 43 | def assumingRedisClusterIsRunning(f: (JPool, JClient) => Unit): Unit = { 44 | Try { 45 | val jedisCluster = new JedisCluster( 46 | Set( 47 | new HostAndPort("localhost", 7000), 48 | new HostAndPort("localhost", 7001), 49 | new HostAndPort("localhost", 7002), 50 | new HostAndPort("localhost", 7003), 51 | new HostAndPort("localhost", 7004), 52 | new HostAndPort("localhost", 7005) 53 | ).asJava 54 | ) 55 | 56 | if (jedisCluster.getClusterNodes.asScala.isEmpty) 57 | throw new IllegalStateException("No connections initialized") 58 | else jedisCluster.getClusterNodes.asScala.mapValues(_.getResource.ping()): @nowarn 59 | 60 | (jedisCluster, new JedisClusterClient(jedisCluster)) 61 | } match { 62 | case Failure(_) => alert("Skipping tests because it does not appear Redis Cluster is running on localhost.") 63 | case Success((pool, client)) => f(pool, client) 64 | } 65 | } 66 | 67 | runTestsIfPossible() 68 | 69 | } 70 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/RedisSerializationSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import java.nio.charset.StandardCharsets 20 | 21 | import org.scalatest.flatspec.AnyFlatSpec 22 | import org.scalatest.matchers.should.Matchers 23 | 24 | class RedisSerializationSpec extends AnyFlatSpec with Matchers with RedisSerialization { 25 | 26 | behavior of "serialization" 27 | 28 | import scalacache.serialization.binary._ 29 | 30 | it should "round-trip a String" in { 31 | val bytes = serialize("hello") 32 | deserialize[String](bytes) should be(Right("hello")) 33 | } 34 | 35 | it should "round-trip a byte array" in { 36 | val bytes = serialize("world".getBytes("UTF-8")) 37 | deserialize[Array[Byte]](bytes).map(new String(_, StandardCharsets.UTF_8)) should be(Right("world")) 38 | } 39 | 40 | it should "round-trip an Int" in { 41 | val bytes = serialize(345) 42 | deserialize[Int](bytes) should be(Right(345)) 43 | } 44 | 45 | it should "round-trip a Double" in { 46 | val bytes = serialize(1.23) 47 | deserialize[Double](bytes) should be(Right(1.23)) 48 | } 49 | 50 | it should "round-trip a Long" in { 51 | val bytes = serialize(3456L) 52 | deserialize[Long](bytes) should be(Right(3456L)) 53 | } 54 | 55 | it should "round-trip a Serializable case class" in { 56 | val cc = CaseClass(123, "wow") 57 | val bytes = serialize(cc) 58 | deserialize[CaseClass](bytes) should be(Right(cc)) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/RedisTestUtil.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import org.scalatest.Alerting 20 | import redis.clients.jedis._ 21 | 22 | import scala.util.{Success, Failure, Try} 23 | 24 | trait RedisTestUtil { self: Alerting => 25 | 26 | def assumingRedisIsRunning(f: (JedisPool, JedisClient) => Unit): Unit = { 27 | Try { 28 | val jedisPool = new JedisPool("localhost", 6379) 29 | val jedis = jedisPool.getResource() 30 | jedis.ping() 31 | (jedisPool, new JedisClient(jedis)) 32 | } match { 33 | case Failure(_) => 34 | alert("Skipping tests because Redis does not appear to be running on localhost.") 35 | case Success((pool, client)) => f(pool, client) 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/SentinelRedisCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig 20 | import redis.clients.jedis._ 21 | 22 | import scala.jdk.CollectionConverters._ 23 | import scala.util.{Failure, Success, Try} 24 | import scalacache._ 25 | import cats.effect.IO 26 | import scalacache.serialization.binary.BinaryCodec 27 | import scalacache.serialization.binary.StringBinaryCodec 28 | 29 | class SentinelRedisCacheSpec extends RedisCacheSpecBase { 30 | 31 | type JClient = JedisClient 32 | type JPool = JedisSentinelPool 33 | 34 | val withJedis = assumingRedisSentinelIsRunning _ 35 | 36 | def constructCache[V](pool: JPool)(implicit codec: BinaryCodec[V]): Cache[IO, String, V] = 37 | new SentinelRedisCache[IO, String, V](jedisPool = pool) 38 | 39 | def flushRedis(client: JClient): Unit = client.underlying.flushDB(): Unit 40 | 41 | /** This assumes that Redis master with name "master" and password "master-local" is running, and a sentinel is also 42 | * running with to monitor this master on port 26379. 43 | */ 44 | def assumingRedisSentinelIsRunning(f: (JedisSentinelPool, JedisClient) => Unit): Unit = { 45 | Try { 46 | val jedisPool = new JedisSentinelPool("master", Set("127.0.0.1:26379").asJava, new GenericObjectPoolConfig[Jedis]) 47 | val jedis = jedisPool.getResource() 48 | jedis.ping() 49 | (jedisPool, new JedisClient(jedis)) 50 | } match { 51 | case Failure(_) => 52 | alert("Skipping tests because Redis master and sentinel does not appear to be running on localhost.") 53 | case Success((pool, client)) => f(pool, client) 54 | } 55 | } 56 | 57 | runTestsIfPossible() 58 | 59 | } 60 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/ShardedRedisCacheSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import redis.clients.jedis._ 20 | import scalacache._ 21 | 22 | import scala.jdk.CollectionConverters._ 23 | import scala.util.{Failure, Success, Try} 24 | import cats.effect.IO 25 | import scalacache.serialization.binary.BinaryCodec 26 | import org.apache.commons.pool2.impl.GenericObjectPoolConfig 27 | import scalacache.serialization.binary.StringBinaryCodec 28 | 29 | class ShardedRedisCacheSpec extends RedisCacheSpecBase { 30 | 31 | type JClient = ShardedJedisClient 32 | type JPool = ShardedJedisPool 33 | 34 | val withJedis = assumingMultipleRedisAreRunning _ 35 | 36 | def constructCache[V](pool: JPool)(implicit codec: BinaryCodec[V]): Cache[IO, String, V] = 37 | new ShardedRedisCache[IO, String, V](jedisPool = pool) 38 | 39 | def flushRedis(client: JClient): Unit = 40 | client.underlying.getAllShards.asScala.foreach(_.flushDB()) 41 | 42 | def assumingMultipleRedisAreRunning(f: (ShardedJedisPool, ShardedJedisClient) => Unit): Unit = { 43 | Try { 44 | val shard1 = new JedisShardInfo("localhost", 6379) 45 | val shard2 = new JedisShardInfo("localhost", 6380) 46 | 47 | val jedisPool = 48 | new ShardedJedisPool(new GenericObjectPoolConfig[ShardedJedis], java.util.Arrays.asList(shard1, shard2)) 49 | val jedis = jedisPool.getResource 50 | 51 | jedis.getAllShards.asScala.foreach(_.ping()) 52 | 53 | (jedisPool, new ShardedJedisClient(jedis)) 54 | } match { 55 | case Failure(_) => 56 | alert("Skipping tests because it does not appear that multiple instances of Redis are running on localhost.") 57 | case Success((pool, client)) => f(pool, client) 58 | } 59 | } 60 | 61 | runTestsIfPossible() 62 | 63 | } 64 | -------------------------------------------------------------------------------- /modules/redis/src/test/scala/scalacache/redis/jedisWrappers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package scalacache.redis 18 | 19 | import redis.clients.jedis._ 20 | 21 | /** Scala wrapper for Jedis implementations. This allows an implementation of [[RedisCacheSpecBase]] to choose the 22 | * specific client required for running the tests. 23 | */ 24 | trait BaseJedisClient { 25 | 26 | def set(key: Array[Byte], value: Array[Byte]): Unit 27 | 28 | def get(key: Array[Byte]): Array[Byte] 29 | 30 | def get(key: String): String 31 | 32 | def ttl(key: Array[Byte]): Long 33 | 34 | def pttl(key: String): Long 35 | 36 | } 37 | 38 | class JedisClusterClient(val underlying: JedisCluster) extends BaseJedisClient { 39 | 40 | override def set(key: Array[Byte], value: Array[Byte]): Unit = 41 | underlying.set(key, value): Unit 42 | 43 | override def get(key: Array[Byte]): Array[Byte] = 44 | underlying.get(key) 45 | 46 | override def get(key: String): String = 47 | underlying.get(key) 48 | 49 | override def ttl(key: Array[Byte]): Long = 50 | underlying.ttl(key) 51 | 52 | override def pttl(key: String): Long = 53 | underlying.pttl(key) 54 | } 55 | 56 | class JedisClient(val underlying: Jedis) extends BaseJedisClient { 57 | 58 | override def set(key: Array[Byte], value: Array[Byte]): Unit = 59 | underlying.set(key, value): Unit 60 | 61 | override def get(key: Array[Byte]): Array[Byte] = 62 | underlying.get(key) 63 | 64 | override def get(key: String): String = 65 | underlying.get(key) 66 | 67 | override def ttl(key: Array[Byte]): Long = 68 | underlying.ttl(key) 69 | 70 | override def pttl(key: String): Long = 71 | underlying.pttl(key) 72 | } 73 | 74 | class ShardedJedisClient(val underlying: ShardedJedis) extends BaseJedisClient { 75 | 76 | override def set(key: Array[Byte], value: Array[Byte]): Unit = 77 | underlying.set(key, value): Unit 78 | 79 | override def get(key: Array[Byte]): Array[Byte] = 80 | underlying.get(key) 81 | 82 | override def get(key: String): String = 83 | underlying.get(key) 84 | 85 | override def ttl(key: Array[Byte]): Long = 86 | underlying.ttl(key) 87 | 88 | override def pttl(key: String): Long = 89 | underlying.pttl(key) 90 | } 91 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/integrationtests/IntegrationTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 scalacache 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package integrationtests 18 | 19 | import java.util.UUID 20 | 21 | import org.scalatest._ 22 | import cats.effect.{IO => CatsIO} 23 | import net.spy.memcached.{AddrUtil, MemcachedClient} 24 | import redis.clients.jedis.JedisPool 25 | 26 | import scala.util.control.NonFatal 27 | import scalacache._ 28 | import scalacache.caffeine.CaffeineCache 29 | import scalacache.memcached.MemcachedCache 30 | import scalacache.redis.RedisCache 31 | import cats.effect.Clock 32 | import cats.effect.unsafe.implicits.global 33 | import org.scalatest.flatspec.AnyFlatSpec 34 | import org.scalatest.matchers.should.Matchers 35 | 36 | class IntegrationTests extends AnyFlatSpec with Matchers with BeforeAndAfterAll { 37 | 38 | implicit val catsClock: Clock[CatsIO] = Clock[CatsIO] 39 | 40 | private val memcachedClient = new MemcachedClient(AddrUtil.getAddresses("localhost:11211")) 41 | private val jedisPool = new JedisPool("localhost", 6379) 42 | 43 | override def afterAll(): Unit = { 44 | memcachedClient.shutdown() 45 | jedisPool.close() 46 | } 47 | 48 | private def memcachedIsRunning: Boolean = { 49 | try { 50 | memcachedClient.get("foo") 51 | true 52 | } catch { case _: Exception => false } 53 | } 54 | 55 | private def redisIsRunning: Boolean = { 56 | try { 57 | val jedis = jedisPool.getResource() 58 | try { 59 | jedis.ping() 60 | true 61 | } finally { 62 | jedis.close() 63 | } 64 | } catch { 65 | case NonFatal(_) => false 66 | } 67 | } 68 | 69 | case class CacheBackend(name: String, cache: Cache[CatsIO, String, String]) 70 | 71 | private val caffeine = CacheBackend("Caffeine", CaffeineCache[CatsIO, String, String].unsafeRunSync()) 72 | private val memcached: Seq[CacheBackend] = 73 | if (memcachedIsRunning) { 74 | Seq( 75 | { 76 | import scalacache.serialization.binary._ 77 | CacheBackend("(Memcached) ⇔ (binary codec)", MemcachedCache[CatsIO, String](memcachedClient)) 78 | }, { 79 | import scalacache.serialization.circe._ 80 | CacheBackend("(Memcached) ⇔ (circe codec)", MemcachedCache[CatsIO, String](memcachedClient)) 81 | } 82 | ) 83 | } else { 84 | alert("Skipping Memcached integration tests because Memcached does not appear to be running on localhost.") 85 | Nil 86 | } 87 | 88 | private val redis: Seq[CacheBackend] = 89 | if (redisIsRunning) 90 | Seq( 91 | { 92 | import scalacache.serialization.binary._ 93 | CacheBackend("(Redis) ⇔ (binary codec)", RedisCache[CatsIO, String, String](jedisPool)) 94 | }, { 95 | import scalacache.serialization.circe._ 96 | CacheBackend("(Redis) ⇔ (circe codec)", RedisCache[CatsIO, String, String](jedisPool)) 97 | } 98 | ) 99 | else { 100 | alert("Skipping Redis integration tests because Redis does not appear to be running on localhost.") 101 | Nil 102 | } 103 | 104 | val backends: List[CacheBackend] = List(caffeine) ++ memcached ++ redis 105 | 106 | for (CacheBackend(name, cache) <- backends) { 107 | 108 | s"$name ⇔ (cats-effect IO)" should "defer the computation and give the correct result" in { 109 | implicit val theCache: Cache[CatsIO, String, String] = cache 110 | 111 | val key = UUID.randomUUID().toString 112 | val initialValue = UUID.randomUUID().toString 113 | 114 | val program = 115 | for { 116 | _ <- put(key)(initialValue) 117 | readFromCache <- get(key) 118 | updatedValue = "prepended " + readFromCache.getOrElse("couldn't find in cache!") 119 | _ <- put(key)(updatedValue) 120 | finalValueFromCache <- get(key) 121 | } yield finalValueFromCache 122 | 123 | checkComputationHasNotRun(key) 124 | 125 | val result: Option[String] = program.unsafeRunSync() 126 | assert(result.contains("prepended " + initialValue)) 127 | } 128 | } 129 | 130 | private def checkComputationHasNotRun(key: String)(implicit cache: Cache[CatsIO, String, String]): Assertion = { 131 | Thread.sleep(1000) 132 | assert(cache.get(key).unsafeRunSync().isEmpty) 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /project/build.sbt: -------------------------------------------------------------------------------- 1 | scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") 2 | addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.4") 3 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") 4 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") 5 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.24") 6 | addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.22.1") 7 | addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") 8 | --------------------------------------------------------------------------------