Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MongoDB as a cache implementation #661

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
79b09d2
Added new Mongo module & implemented GET method
rabinarai1 Feb 21, 2022
2949389
Avoid directly throwing an exception when decoding Mongo documents
DavidGregory084 Feb 21, 2022
4956e9c
Make the collection private in MongoCache
DavidGregory084 Feb 21, 2022
1582b98
Remove unused imports and add test copyright header
DavidGregory084 Feb 21, 2022
0709fcf
Implemented PUT method
rabinarai1 Feb 21, 2022
a6a24d8
Add Mongo Circe BSON serialization module
DavidGregory084 Feb 22, 2022
48bb3b5
Flesh out testing of Circe BSON codecs
DavidGregory084 Feb 22, 2022
456c823
Test BSON encoding of Option and BigDecimal values which must be repr…
DavidGregory084 Feb 22, 2022
5d0c153
Refactoring MongoCache method implementations
DavidGregory084 Feb 22, 2022
40868e5
Implemented Remove methods & Integration Tests
rabinarai1 Feb 24, 2022
6b009f2
Update Mongo driver version and fix warnings
DavidGregory084 Feb 24, 2022
4d7a5f9
Allow MongoCache keys to be polymorphic; add Resource constructors fo…
DavidGregory084 Feb 24, 2022
3a3062c
Update the Codec implicitNotFound message
DavidGregory084 Feb 24, 2022
01e2e8b
Fix compilation error due to Circe Scala 3 auto derivation support
DavidGregory084 Feb 25, 2022
7b1be9e
Switch to the MongoDB Reactive Streams driver because the Scala drive…
DavidGregory084 Feb 25, 2022
8ebbcfe
Update documentation to reflect the new MongoDB cache implementation
DavidGregory084 Feb 25, 2022
bd81552
Ensure that the Mongo cache implementations are closed in the doc exa…
DavidGregory084 Feb 25, 2022
f3e36dc
Be consistent with case
DavidGregory084 Feb 25, 2022
4665f1b
Provide a method for users to get a BsonEncoder from a circe Encoder
DavidGregory084 Feb 25, 2022
81f342d
Merge branch 'master' of github.com:opencastsoftware/scalacache into …
DavidGregory084 Mar 1, 2022
b53c892
Merge branch 'master' into MongoDb-Support
DavidGregory084 Mar 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ logs
.ensime
.ensime_lucene
.ensime_cache
.bloop/
.bsp/
.metals/
.vscode/
target/
metals.sbt
*.worksheet.sc
46 changes: 38 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ inThisBuild(
)
)

val CatsEffectVersion = "3.3.7"
val CatsEffectVersion = "3.3.7"
val CirceVersion = "0.14.1"
val MongoDriverVersion = "4.5.0"

scalafmtOnCompile in ThisBuild := true

Expand All @@ -33,7 +35,9 @@ lazy val root: Project = Project(id = "scalacache", base = file("."))
redis,
caffeine,
circe,
tests
tests,
mongo,
mongoCirce
)

lazy val core =
Expand Down Expand Up @@ -72,6 +76,25 @@ lazy val memcached = createModule("memcached")
)
)

lazy val mongo = createModule("mongo")
.settings(
libraryDependencies ++= Seq(
"org.mongodb" % "mongodb-driver-reactivestreams" % MongoDriverVersion,
"org.mongodb" % "mongodb-driver-sync" % MongoDriverVersion % Test
)
)

lazy val mongoCirce = createModule("mongo-circe")
.settings(
libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % CirceVersion,
"io.circe" %% "circe-generic" % CirceVersion % Test,
scalacheck,
scalatestplus
)
)
.dependsOn(mongo)

lazy val redis = createModule("redis")
.settings(
libraryDependencies ++= Seq(
Expand All @@ -95,9 +118,9 @@ lazy val caffeine = createModule("caffeine")
lazy val circe = createModule("circe")
.settings(
libraryDependencies ++= Seq(
"io.circe" %% "circe-core" % "0.14.1",
"io.circe" %% "circe-parser" % "0.14.1",
"io.circe" %% "circe-generic" % "0.14.1" % Test,
"io.circe" %% "circe-core" % CirceVersion,
"io.circe" %% "circe-parser" % CirceVersion,
"io.circe" %% "circe-generic" % CirceVersion % Test,
scalacheck,
scalatestplus
),
Expand All @@ -106,8 +129,13 @@ lazy val circe = createModule("circe")
)

lazy val tests = createModule("tests")
.settings(publishArtifact := false)
.dependsOn(caffeine, memcached, redis, circe)
.settings(
publishArtifact := false,
libraryDependencies ++= Seq(
"org.mongodb" % "mongodb-driver-sync" % MongoDriverVersion % Test
)
)
.dependsOn(caffeine, memcached, redis, circe, mongo, mongoCirce)

lazy val docs = createModule("docs")
.enablePlugins(MicrositesPlugin)
Expand All @@ -131,7 +159,9 @@ lazy val docs = createModule("docs")
memcached,
redis,
caffeine,
circe
circe,
mongo,
mongoCirce
)

lazy val benchmarks = createModule("benchmarks")
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ services:
image: memcached:latest
ports:
- '11211:11211'
mongo:
image: mongo:4.0
ports:
- '27017-27019:27017-27019'
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ trait Decoder[L, R] {

/** Represents a type class that needs to be implemented for serialization/deserialization to work.
*/
@implicitNotFound(msg = """Could not find any Codecs for types ${L, R}.
@implicitNotFound(msg = """Could not find any Codec to serialize type ${L} as ${R}.
If you would like to serialize values in a binary format, please import the binary codec:

import scalacache.serialization.binary._
Expand All @@ -42,6 +42,14 @@ import io.circe.generic.auto._

You will need a dependency on the scalacache-circe module.

If you would like to serialize values as MongoDB BSON using circe, please import the circe codec
and provide a circe Encoder[${L}] and Decoder[${L}], e.g.:

import scalacache.serialization.bson.circe._
import io.circe.generic.auto._

You will need a dependency on the scalacache-mongo-circe module.

See the documentation for more details on codecs.""")
trait Codec[L, R] extends Encoder[L, R] with Decoder[L, R] {
override def encode(left: L): R
Expand Down
51 changes: 47 additions & 4 deletions modules/docs/src/main/mdoc/docs/cache-implementations.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import scalacache.serialization.binary._
import net.spy.memcached._

val memcachedClient = new MemcachedClient(
new BinaryConnectionFactory(),
new BinaryConnectionFactory(),
AddrUtil.getAddresses("localhost:11211")
)
implicit val customisedMemcachedCache: Cache[IO, String, String] = MemcachedCache(memcachedClient)
Expand All @@ -47,7 +47,7 @@ ScalaCache provides two `KeySanitizer` implementations that convert your cache k

* `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.

* `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.
* `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.

### Redis

Expand Down Expand Up @@ -83,6 +83,49 @@ implicit val customisedRedisCache: Cache[IO, String, String] = RedisCache(jedisP

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.

### MongoDB

SBT:

```
libraryDependencies ++= Seq(
"com.github.cb372" %% "scalacache-mongo" % "0.28.0",
"com.github.cb372" %% "scalacache-mongo-circe" % "0.28.0",
)
```

Usage:

```scala mdoc:silent
import scalacache._
import scalacache.mongo._
import scalacache.serialization.bson.circe._
import cats.effect.IO
import cats.effect.unsafe.implicits.global

val databaseName = "scalacache"
val collectionName = "cache"

implicit val mongoCache: Cache[IO, String, String] =
MongoCache[IO, String, String]("mongodb://localhost:27017", databaseName, collectionName).unsafeRunSync()
```

or provide your own Mongo client, like this:

```scala mdoc:silent
import scalacache._
import scalacache.mongo._
import scalacache.serialization.bson.circe._
import cats.effect.IO
import cats.effect.unsafe.implicits.global
import com.mongodb.reactivestreams.client.MongoClients

val mongoClient = MongoClients.create("mongodb://localhost:27017")

implicit val customClientMongoCache: Cache[IO, String, String] =
MongoCache[IO, String, String](mongoClient, databaseName, collectionName).unsafeRunSync()
```

### Caffeine

SBT:
Expand Down Expand Up @@ -118,7 +161,7 @@ implicit val customisedCaffeineCache: Cache[IO, String, String] = CaffeineCache(
```

```scala mdoc:invisible
for (cache <- List(redisCache, customisedRedisCache, memcachedCache, customisedMemcachedCache)) {
for (cache <- List(redisCache, customisedRedisCache, mongoCache, customClientMongoCache, memcachedCache, customisedMemcachedCache)) {
cache.close.unsafeRunSync()
}
}
```
35 changes: 35 additions & 0 deletions modules/docs/src/main/mdoc/docs/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,41 @@ implicit val catDecoder: Decoder[Cat] = deriveDecoder[Cat]

For more information, please consult the [circe docs](https://circe.github.io/circe/).

### BSON codec

If you are using the MongoDB cache implementation, you must provide a `BsonCodec` for serializing your data as BSON.

You can do this manually, or use the ScalaCache integration module which converts [circe](https://circe.github.io/circe/) JSON into BSON.

To do the latter you can add a dependency on the scalacache-mongo-circe module:

```
libraryDependencies += "com.github.cb372" %% "scalacache-mongo-circe" % "0.28.0"
```

Then import the codec:

```scala mdoc:fail:silent
import scalacache.serialization.bson.circe._
```

As with the circe JSON integration, 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:

```scala
import io.circe.generic.auto._
```

but if you are worried about performance, it's better to derive them semi-automatically:

```scala
import io.circe._
import io.circe.generic.semiauto._
implicit val catEncoder: Encoder[Cat] = deriveEncoder[Cat]
implicit val catDecoder: Decoder[Cat] = deriveDecoder[Cat]
```

For more information, please consult the [circe docs](https://circe.github.io/circe/).

### Custom Codec

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
Expand Down
1 change: 1 addition & 0 deletions modules/docs/src/main/mdoc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Use ScalaCache to add caching to any Scala app with the minimum of fuss.
The following cache implementations are supported, and it's easy to plugin your own implementation:
* Memcached
* Redis
* MongoDB
* [Caffeine](https://github.com/ben-manes/caffeine)

## Compatibility
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2021 scalacache
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package scalacache.serialization.bson

import io.circe.Json
import io.circe.JsonNumber
import io.circe.JsonObject
import org.bson.BsonArray
import org.bson.BsonBoolean
import org.bson.BsonDecimal128
import org.bson.BsonDocument
import org.bson.BsonDouble
import org.bson.BsonInt32
import org.bson.BsonInt64
import org.bson.BsonNull
import org.bson.BsonString
import org.bson.BsonValue
import scalacache.serialization.Codec
import scalacache.serialization.FailedToDecode

import scala.jdk.CollectionConverters._

package object circe extends BsonCirceCodec

trait BsonCirceCodec {
implicit def scalaCacheBsonEncoderFromCirceEncoder[A](implicit
encoder: io.circe.Encoder[A]
): BsonEncoder[A] =
new BsonEncoder[A] {
override def encode(value: A): BsonValue = {
val json = encoder(value)
jsonToBson(json)
}
}

implicit def scalaCacheBsonCodecFromCirceCodec[A](implicit
encoder: io.circe.Encoder[A],
decoder: io.circe.Decoder[A]
): BsonCodec[A] =
new BsonCodec[A] {

override def encode(value: A): BsonValue = {
val json = encoder(value)
jsonToBson(json)
}

override def decode(bytes: BsonValue): Codec.DecodingResult[A] = {
val json = bsonToJson(bytes)

decoder
.decodeJson(json)
.left
.map(FailedToDecode.apply)
}

}

private def bsonToJson(bson: BsonValue): Json = bson match {
case _: BsonNull => Json.Null
case b: BsonBoolean => Json.fromBoolean(b.getValue)
case i: BsonInt32 => Json.fromInt(i.getValue)
case l: BsonInt64 => Json.fromLong(l.getValue)
case d: BsonDouble => Json.fromDoubleOrNull(d.getValue)
case bd: BsonDecimal128 => Json.fromBigDecimal(bd.getValue.bigDecimalValue)
case s: BsonString => Json.fromString(s.getValue)
case a: BsonArray => Json.fromValues(a.getValues.asScala.map(bsonToJson))
case d: BsonDocument =>
Json.fromJsonObject(
JsonObject.fromIterable(
d.entrySet.asScala.map { entry =>
entry.getKey -> bsonToJson(entry.getValue)
}
)
)
}

private def jsonToBson(json: Json): BsonValue = {
val toBsonFolder = new Json.Folder[BsonValue] {
def onNull: BsonValue =
new BsonNull()

def onBoolean(value: Boolean): BsonValue =
new BsonBoolean(value)

def onNumber(value: JsonNumber): BsonValue = {
value.toBigDecimal
.map { bd =>
if (bd.isValidInt) new BsonInt32(bd.intValue)
else if (bd.isValidLong) new BsonInt64(bd.longValue)
else if (bd.isDecimalDouble) new BsonDouble(bd.doubleValue)
else new BsonString(bd.toString)
}
.getOrElse {
// Should not be possible; these numbers are the result of encoding Java primitives
throw new NumberFormatException("For input string \"" + value.toString + "\"")
}
}

def onString(value: String): BsonValue =
new BsonString(value)

def onArray(value: Vector[Json]): BsonValue = {
val bsonArray = new BsonArray()
value.foreach { v =>
bsonArray.add(v.foldWith(this))
}
bsonArray
}

def onObject(value: JsonObject): BsonValue = {
val bsonDocument = new BsonDocument()
value.toIterable.foreach { case (k, v) =>
bsonDocument.append(k, v.foldWith(this))
}
bsonDocument
}

}

json.foldWith(toBsonFolder)
}
}
Loading