From 4134c5271756d2fbc754f61147c357928579e443 Mon Sep 17 00:00:00 2001 From: Karel Cemus Date: Sat, 21 Apr 2018 20:11:20 +0200 Subject: [PATCH] Setting version to 2.0.2 Migrated documentation of version 2.0.2 from wiki --- .gitignore | 1 - README.md | 120 +++++++++++++-- doc/10-integration.md | 37 +++++ doc/20-configuration.md | 196 ++++++++++++++++++++++++ doc/30-how-to-use.md | 271 ++++++++++++++++++++++++++++++++++ doc/40-migration.md | 48 ++++++ doc/images/redis-settings.png | Bin 0 -> 27442 bytes doc/images/redis-settings.xml | 1 + version.sbt | 2 +- 9 files changed, 662 insertions(+), 14 deletions(-) create mode 100644 doc/10-integration.md create mode 100644 doc/20-configuration.md create mode 100644 doc/30-how-to-use.md create mode 100644 doc/40-migration.md create mode 100644 doc/images/redis-settings.png create mode 100644 doc/images/redis-settings.xml diff --git a/.gitignore b/.gitignore index 4f1103c4..ab86efbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Compilation output -doc/ bin/ lib/ out/ diff --git a/README.md b/README.md index 53989778..5182b4fe 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,105 @@ -## About the project +## About the Project + +[Play framework 2](http://playframework.com/) is delivered with EHCache module implementing +[SyncCacheApi and AsyncCacheApi](https://playframework.com/documentation/2.6.x/ScalaCache). +This module adds **support of Redis cache** server, i.e., key/value storage. + +Besides the compatibility with all Play's cache APIs, +it introduces more evolved API providing lots of handful +operations. Besides the basic methods such as `get`, `set` +and `remove`, it provides more convenient methods such as +`expire`, `exists`, `invalidate` and much more. + +The implementation builds on the top of Akka actor system, +it is **completely non-blocking and asynchronous** under +the hood, though it also provides blocking APIs to ease +the use. Furthermore, the library supports several configuration +providers to let you easily use `play-redis` on localhost, Heroku, +as well as on your premise. + + +## Features + +- [synchronous and asynchronous APIs](#provided-apis) +- [implements standard APIs defined by Play's `cacheApi` project](#provided-apis) +- support of [named caches](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/20-configuration.md#named-caches) +- [works with Guice](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/40-migration.md#runtime-time-dependency-injection) as well as [compile-time DI](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/40-migration.md#compile-time-dependency-injection) +- [getOrElse and getOrFuture operations](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#use-of-cacheapi) easing the use +- [wildcards in remove operation](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#use-of-cacheapi) +- support of collections: [sets](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#use-of-sets), [lists](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#use-of-lists), and [maps](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#use-of-maps) +- [increment and decrement operations](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#use-of-cacheapi) +- [eager and lazy invocation policies](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#eager-and-lazy-invocation) waiting or not waiting for the result +- several [recovery policies](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/20-configuration.md#recovery-policy) and possibility of further customization +- support of [several configuration sources](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/20-configuration.md#running-in-different-environments) + - static in the configuration file + - from the connection string optionally in the environmental variable + - custom implementation of the configuration provider +- support of [both standalone and cluster modes](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/20-configuration.md#standalone-vs-cluster) +- build on the top of Akka actors and serializers, [agnostic to the serialization mechanism](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md#limitations) + - for simplicity, it uses deprecated Java serialization by default + - it is recommended to use [Kryo library](https://github.com/romix/akka-kryo-serialization) or any other mechanism + + +## Provided APIs + +This library delivers a single module with following implementations of the API. While the core +of the framework is **fully non-blocking**, most of the provided facades are **only blocking wrappers**. + +
+ +| | Trait | Language | Blocking | Features | +| -- | ------------------------------------ | :------: | :----------: | :------: | +| 1. | `play.api.cache.redis.CacheApi` | Scala | *blocking* | advanced | +| 2. | `play.api.cache.redis.CacheAsyncApi` | Scala | non-blocking | advanced | +| 3. | `play.api.cache.SyncCacheApi` | Scala | *blocking* | basic | +| 4. | `play.api.cache.AsyncCacheApi` | Scala | non-blocking | basic | +| 5. | `play.cache.SyncCacheApi` | Java | *blocking* | basic | +| 6. | `play.cache.AsyncCacheApi` | Java | non-blocking | basic | + +
+ +First, the `CacheAsyncApi` provides extended API to work with Redis and enables **non-blocking** +connection providing results through `scala.concurrent.Future`. +Second, the `CacheApi` is a thin **blocking** wrapper around the asynchronous implementation. +Third, there are other implementations supporting contemporary versions of the `CacheApi`s +bundled within Play framework. Finally, `play-redis` also supports Java version of the API. + + +## Documentation and Getting Started + +**[The full documentation](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc) for the upcoming version** +is in the `doc` directory on `master` branch. **The documentation for a released version** +is under [the particular tag in the Git history](https://github.com/KarelCemus/play-redis/releases) +or you can use shortcuts in the table below. + +To use this module: + +1. [Add this library into your project](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/10-integration.md) and expose APIs +1. See the [configuration options](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/20-configuration.md) +1. [Browse examples of use](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/30-how-to-use.md) + +If you come from older version, you might check the [Migration Guide](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/40-migration.md) + + +## Samples + +To ease the initial learning, there are +[several sample projects](https://github.com/KarelCemus/play-redis-samples) +intended to demonstrate the most common configurations. Feel free +to study, copy or fork them to better understand the `play-redis` use. + + +1. [**Getting Started**](https://github.com/KarelCemus/play-redis-samples/tree/master/hello_world) is a very basic example showing the +minimal configuration required to use the redis cache + +1. [**Named Caches**](https://github.com/KarelCemus/play-redis-samples/tree/master/named_caches) is the advanced example with custom recovery policy and multiple named caches. + +1. [**EhCache and Redis**](https://github.com/KarelCemus/play-redis-samples/tree/master/redis_and_ehcache) shows a combination of both caching provides used at once. +While the EhCache is bound to unqualified APIs, the Redis cache uses named APIs. -This module for Play framework 2 adds support of Redis cache server and provides -a set of handful APIs. For more details and **full documentation for the version `2.x.x`** and newer please **see -[the wiki](https://github.com/KarelCemus/play-redis/wiki)**. For the documentation of older versions see -README at corresponding tag in git history. ## How to add the module into the project @@ -26,23 +119,26 @@ To your SBT `build.sbt` add the following lines: // enable Play cache API (based on your Play version) libraryDependencies += play.sbt.PlayImport.cacheApi // include play-redis library -libraryDependencies += "com.github.karelcemus" %% "play-redis" % "2.0.1" +libraryDependencies += "com.github.karelcemus" %% "play-redis" % "2.0.2" ``` + ## Compatibility matrix -| play framework | play-redis | -|-----------------|---------------:| -| 2.6.x | 2.0.1 ([Migration Guide](https://github.com/KarelCemus/play-redis/wiki/Migration-Guide)) | -| 2.5.x | 1.4.2 | -| 2.4.x | 1.0.0 | -| 2.3.x | 0.2.1 | +| play framework | play-redis | documentation | +|-----------------|---------------:|-----------------:| +| 2.6.x | 2.0.2 | [see here](https://github.com/KarelCemus/play-redis/blob/2.0.2/README.md) ([Migration Guide](https://github.com/KarelCemus/play-redis/blob/2.0.2/doc/40-migration.md)) | +| 2.5.x | 1.4.2 | [see here](https://github.com/KarelCemus/play-redis/blob/1.4.2/README.md) | +| 2.4.x | 1.0.0 | [see here](https://github.com/KarelCemus/play-redis/blob/1.0.0/README.md) | +| 2.3.x | 0.2.1 | [see here](https://github.com/KarelCemus/play-redis/blob/0.2.1/README.md) | + ## Contribution If you encounter any issue, have a feature request, or just like this library, please feel free to report it or contact me. + ## Changelog For the list of changes and migration guide please see diff --git a/doc/10-integration.md b/doc/10-integration.md new file mode 100644 index 00000000..89e4b10c --- /dev/null +++ b/doc/10-integration.md @@ -0,0 +1,37 @@ +# Integration Guide + +The library comes with the support of both compile-time and runtime-time dependency injection. +Although the use of runtime-time injection is preferred, both options are equal and fully implemented. + +## Runtime-time Dependency Injection + +You **must enable the redis cache module** in `application.conf`: + +``` +# enable redis cache module +play.modules.enabled += "play.api.cache.redis.RedisCacheModule" +``` + +It will bind all required components and make them available through runtime DI according to the configuration. + +## Compile-time Dependency Injection + +To use compile-time DI, mix `play.api.cache.redis.RedisCacheComponents` +into your `BuiltInComponentsFromContext` subclass. It exposes `cacheApi` method +accepting a redis instance (or just cache name, if configured) and returns an instance +of `RedisCaches` encapsulating all available APIs for this particular cache. Then you +can access and expose them yourself. Next, it provides a few methods to override and +provide customized configuration. For more details, see directly `RedisCacheComponents` source. + +``` + // 'play' is the name of the named cache + // (play is default name of the default cache) + // + // the 'play' literal is implicitly converted into + // the instance but has to be configured in 'application.conf' + val playCache: RedisCaches = cacheApi( "play" ) + + // expose `play.api.cache.redis.CacheAsyncApi` + val asynchronousRedis = playCache.async + +``` diff --git a/doc/20-configuration.md b/doc/20-configuration.md new file mode 100644 index 00000000..5b22b123 --- /dev/null +++ b/doc/20-configuration.md @@ -0,0 +1,196 @@ +# Configuration + +Default configuration and very detailed manual is available in [reference.conf](https://github.com/KarelCemus/play-redis/blob/master/src/main/resources/reference.conf). It can be overwritten in your `conf/application.conf` file. + +There are several features supported in the configuration, they are discussed below. However, by default, there is no need for any further configuration. Default settings are set to the standalone instance running on `localhost:6379?db=0`, which is default for redis server. This instance is named `play` but is also exposed as a default implementation. + +**The configuration root is `play.cache.redis`** + +## Standalone vs. Cluster + +This implementation supports both standalone and cluster instances. By default, the standalone mode is enabled. It is configured like this: + +``` + play.cache.redis { + host: localhost + # redis server: port + port: 6379 + # redis server: database number (optional) + database: 0 + # authentication password (optional) + password: null + } +``` + +To enable cluster mode instead, use `source` property. Valid values are `standalone` (default), `cluster`, `connection-string`, and `custom`. For more details, see below. Example of cluster settings: + +``` +play.cache.redis { + # enable cluster mode + source: cluster + + # nodes are defined as a sequence of objects: + cluster: [ + { + # required string, defining a host the node is running on + host: localhost + # required integer, defining a port the node is running on + port: 6379 + # optional string, defines a password to use + password: null + } + ] +} +``` + + +## Named caches + +Play framework supports [named caches](https://www.playframework.com/documentation/2.6.x/ScalaCache#Accessing-different-caches) through a qualifier. For a simplicity, the default cache is also exposed without a qualifier to ease the access. This feature can be disabled by `bind-default` property, which defaults to true. The name of the default cache is defined in `default-cache` property, which defaults to `play` to keep consistency with Play framework. + +The configuration of each instance is inherited from the `play.cache.redis` configuration. Inherited values may be locally overridden by the instance's own configuration (e.g., `play.cache.redis.instances.myNamedCache`) + +Named caches are defined under the `play.cache.redis.instances` node, which automatically **disables and ignores** the default cache defined directly under the root `play.cache.redis`. The instances are defined as a map with name-definition pairs. + +``` +play.cache.redis { + + # source property; standalone is default + source: standalone + + instances { + play { + # source property fallbacks to the value under play.cache.redis + host: localhost + port: 6379 + } + myNamedCache { + source: cluster + cluster: [ + { host: localhost, port: 6380 } + ] + } + } + +} +``` + + +### Namespace prefix + +Each cache can optionally define a namespace for keys. It means that each key is automatically prefixed. For example, having `prefix: "my-prefix"` and the key `my-key`, the cache operates with `my-prefix:my-key`. This behavior is especially useful to avoid collisions, when: + +1. either we have multiple named caches working with the same redis database, +2. or there is another application working with the same redis database. + +This property may be locally overridden for each named cache. + + +### Timeout + +The `play-redis` is designed fully asynchronously and there is no timeout applied +by this library itself. However, there are other timeouts you might be interested in. +First, when you use `SyncAPI` instead of `AsyncAPI`, the **internal `Future[T]` has to be converted +into `T`**. It uses [`Await.result`](https://www.scala-lang.org/api/current/scala/concurrent/Await$.html#result%5BT%5D(awaitable:scala.concurrent.Awaitable%5BT%5D,atMost:scala.concurrent.duration.Duration):T) +from standard Scala library, which requires a timeout definition. This is the `play.cache.redis.timeout`. + +This timeout applies on the invocation of the whole request including the communication to Redis, data serialization, +and invocation `orElse` parts. If you don't want any timeouts and your application logic has to never timeout, +just set it to something really high or use asynchronous API to be absolutely sure. + +The other timeouts you might be interested in are related to the communication to Redis, e.g., connection timeout +and receive timeout. These are provided directly by the underlying connector and `play-redis` doesn't affect them. +For more details, see +the [`Redis` configuration](https://github.com/etaty/rediscala). + +### Recovery policy + +The intention of cache is usually to optimize the application behavior, not to provide any business logic. +In this case, it makes sense the cache could be removed without any visible change except for possible +performance loss. In consequence, **failed cache requests should not break the application flow**, +they should be logged and ignored. However, not always this is the desired behavior. To resolve this ambiguity, +the module provides `RecoveryPolicy` trait implementing the behavior to be executed when the cache request fails. +There are two major implementations. They both log the failure at first, and while one produces +the exception and let the application to deal with it, the other returns some neutral value, which +should result in behavior like there is no cache (default policy). However, besides these, it is possible, e.g., to also implement your own policy to, e.g., rerun a failed command. For more information see `RecoveryPolicy` trait. + +Besides the default implementations, you are free to extend the `RecoveryPolicy` trait and provide your own implementation. For example: + +```scala +class MyPolicy extends RecoveryPolicy { + def recoverFrom[ T ]( rerun: => Future[ T ], default: => Future[ T ], failure: RedisException ) = default +} +``` + +Next, name it and update the configuration file: `play.cache.redis.recovery: custom`, where `custom` is the name. Any name is possible. + +Then, if you use runtime DI (e.g. Guice), you have to bind the named `RecoveryPolicy` trait to your implementation. For example, create a module for it. Don't forget to register the module into enabled modules. And that's it. + +```scala +import play.api.cache.redis.RecoveryPolicy +import play.api.inject._ +import play.api.{Configuration, Environment} + +class ApplicationModule extends Module { + + def bindings( environment: Environment, configuration: Configuration ) = Seq( + // "custom" is the name in the configuration file + bind[ RecoveryPolicy ].qualifiedWith( "custom" ).to( classOf[ MyPolicy ] ) + ) +} +``` + +If you use compile-time DI, override `recoveryPolicyResolver` in `RedisCacheComponents` and return the instance when the policy name matches. + +## Running in different environments + +This module can run in various environments, from the localhost through the Heroku to your own premise. Each of these has a possibly different configuration. For this purpose, there is a `source` property accepting 4 values: `standalone` (default), `cluster`, `connection-string`, and `custom`. + +The `standalone` and `cluster` options are already explained. The latter two simplify the use in environments, where the connection cannot be written into the configuration file up front. + +First, the module supports instance definition through a connection string. This is especially useful when you are running the application, e.g., on Heroku or some other service. + +``` +play.cache.redis { + # enable connection-string mode, i.e., a standalone + # configured through a connection string + source: connection-string + + # HOCOON automatically injects the environmental variable + # and the module parses the string. + connection-string: '${REDIS_URL}' +} +``` + +Second, when none of the already existing providers is sufficient, you can implement your own and let the `RedisInstanceResolver` to take care of it. When the module finds a `custom` source, it calls the resolver with the cache name and expects the configuration in return. So all you need to do is to implement your own resolver and register it with DI. Details may differ based on the type of DI you use. + +### Example: Running on Heroku + +To enable redis cache on Heroku we have to do the following steps: + + 1. add library into application dependencies + 2. enable `play.cache.redis.RedisCacheModule` + 4. set `play.cache.redis.source` to `"${REDIS_URL}"` or `"${REDISCLOUD_URL}"`. + 5. done, you can run it and use any of provided interfaces + + +## Overview + +### Module wide (valid only under the root) + +| Key | Type | Default | Description | +|-------------------------------------|---------:|--------------------------------:|-------------------------------------| +| play.cache.redis.bind-default | Boolean | `true` | Whether to bind default unqualified APIs. Applies only with runtime DI | +| play.cache.redis.default-cache | String | `play` | Named of the default cache, applies with `bind-default` | + + +### Instance-specific (can be locally overridden) + +| Key | Type | Default | Description | +|-------------------------------------|---------:|--------------------------------:|-------------------------------------| +| play.cache.redis.source | String | `standalone` | Defines the source of the configuration. Accepted values are `standalone`, `cluster`, `connection-string`, and `custom` | +| play.cache.redis.timeout | Duration | `1s` | conversion timeout applied by `SyncAPI` to convert `Future[T]` to `T`| +| play.cache.redis.prefix | String | `null` | optional namespace, i.e., key prefix | +| play.cache.redis.dispatcher | String | `akka.actor.default-dispatcher` | Akka actor | +| play.cache.redis.recovery | String | `log-and-default` | Defines behavior when command execution fails. Accepted values are `log-and-fail` to log the error and rethrow the exception, `log-and-default` to log the failure and return default value neutral to the operation, `log-condensed-and-default` `log-condensed-and-fail` produce shorter but less informative error logs, and `custom` indicates the user binds his own implementation of `RecoveryPolicy`. | + diff --git a/doc/30-how-to-use.md b/doc/30-how-to-use.md new file mode 100644 index 00000000..35aeb743 --- /dev/null +++ b/doc/30-how-to-use.md @@ -0,0 +1,271 @@ +# How to Use this Module + +When you have the library added to your project, you can safely inject the `play.api.cache.redis.CacheApi` trait for the synchronous cache. If you want the asynchronous implementation, then inject `play.api.cache.redis.CacheAsyncApi`. + +Besides various common operations over the cache, the API supports working with the collections: List, Set, and Map. First, create a typed worker to use the collection under the give key. For example: `cache.list[ String ]( "my-list" )` Then you can fully operate the collection. Please **be aware of the complexity** of the operations and **optimize your code**. Although the API is simple and seems efficient, each of your calls is transmitted to Redis. + +## Checking operation result + +Regardless of current API, all operations throw an exception when fail. Consequently, +successful invocations do not throw an exception. The only difference is in checking for errors. +While synchronous APIs really throw an exception, asynchronous API returns a `Future` +wrapping both the success and the exception, i.e., use `onFailure` or `onComplete` to +check for errors. + +## Limitations + +The major limitation of this module is data serialization required for their transmission to and from the server. Play-redis provides native serialization support to basic data types such as String, Int, etc. +However, for other objects including collections, it uses `JavaSerializer` by default. + +Since Akka 2.4.1, default `JavaSerializer` is [officially considered inefficient for production use](https://github.com/akka/akka/pull/18552). +Nevertheless, to keep things simple, play-redis **still uses this inefficient serializer NOT to enforce** any serialization +library to end users. Although, it recommends [kryo serializer](https://github.com/romix/akka-kryo-serialization) claiming +great performance and small output stream. Any serialization library can be smoothly connected through Akka +configuration, see the [official Akka documentation](http://doc.akka.io/docs/akka/current/scala/serialization.html). + +## Eager and Lazy Invocation + +Some operations, e.g., `getOrElse` and `getOrFuture`, besides the intended computation of the value, invoke a side-effect to store and cache the value, e.g., with the `set` operation. However, though it is usually correct to wait for the side-effect and process the occasional error, there exist situations, where it is safe to ignore the result of the side-effect and return the value directly. This mechanism is handled by the `InvocationPolicy` trait, where default `LazyInvocation` waits for the result and considers the error (if any), while `EagerInvocation` does not wait for the result and ignores the error if happens. + +```scala + // this applies default LazyInvocation waiting for + // the result of the side-effect + cache.getOrElse( "my-key" )( "my-value" ) +``` + +```scala + import play.api.cache.redis._ + implicit val invocationPolicy = EagerInvocation + // this applies EagerInvocation not-waiting + // for the result of the side-effect + cache.getOrElse( "my-key" )( "my-value" ) +``` + + +## Use of `CacheApi` + +```scala +import scala.concurrent.Future +import scala.concurrent.duration._ + +import play.api.cache.redis.CacheApi + +class MyController @Inject() ( cache: CacheApi ) { + + cache.set( "key", "value" ) + // returns Option[ T ] where T stands for String in this example + cache.get[ String ]( "key" ) + cache.remove( "key" ) + + cache.set( "object", MyCaseClass() ) + // returns Option[ T ] where T stands for MyCaseClass + cache.get[ MyCaseClass ]( "object" ) + + // returns Unit + cache.set( "key", 1.23 ) + + // returns Option[ Double ] + cache.get[ Double ]( "key" ) + // returns Option[ MyCaseClass ] + cache.get[ MyCaseClass ]( "object" ) + + // set multiple values at once + cache.setAll( "key" -> 1.23, "key2" -> 5, "key3" -> 6 ) + // set only when all keys are unused + cache.setAllIfNotExist( "key" -> 1.23, "key2" -> 5, "key3" -> 6 ) + // get multiple keys at once, returns a list of options + cache.getAll[ Double ]( "key", "key2", "key3", "key6" ) + + // returns T where T is Double. If the value is not in the cache + // the computed result is saved + cache.getOrElse( "key" )( 1.24 ) + + // same as getOrElse but works for Futures. It returns Future[ T ] + cache.getOrFuture( "key" )( Future.successful( 1.24 ) ) + + // returns Unit and removes a key/keys from the storage + cache.remove( "key" ) + cache.remove( "key1", "key2" ) + cache.remove( "key1", "key2", "key3" ) + // remove all expects a sequence of keys, it performs same be behavior + // as remove methods, they are just syntax sugar + cache.removeAll( "key1", "key2", "key3" ) + + // removes all keys in the redis database! Beware using it + cache.invalidate() + + // refreshes expiration of the key if present + cache.expire( "key", 1.second ) + + // stores the value for infinite time if the key is not used + // returns true when store performed successfully + // returns false when some value was already defined + cache.setIfNotExists( "key", 1.23 ) + // stores the value for limited time if the key is not used + // this is not atomic operation, redis does not provide direct support + cache.setIfNotExists( "key", 1.23, 5.seconds ) + + // returns true if the key is in the storage, false otherwise + cache.exists( "key" ) + + // returns all keys matching given pattern. Beware, complexity is O(n), + // where n is the size of the database. It executes KEYS command. + cache.matching( "page/1/*" ) + + // removes all keys matching given pattern. Beware, complexity is O(n), + // where n is the size of the database. It internally uses method matching. + // It executes KEYS and DEL commands in a transaction. + cache.removeMatching( "page/1/*" ) + + // importing `play.api.cache.redis._` enables us + // using both `java.util.Date` and `org.joda.time.DateTime` as expiration + // dates instead of duration. These implicits are useful when + // we know the data regularly changes, e.g., at midnight, at 3 AM, etc. + // We do not have compute the duration ourselves, the library + // can do it for us + import play.api.cache.redis._ + cache.set( "key", "value", DateTime.parse( "2015-12-01T00:00" ).asExpiration ) + + // atomically increments stored value by one + // initializes with 0 if not exists + cache.increment( "integer" ) // returns 1 + cache.increment( "integer" ) // returns 2 + cache.increment( "integer", 5 ) // returns 7 + + // atomically decrements stored value by one + // initializes with 0 if not exists + cache.decrement( "integer" ) // returns -1 + cache.decrement( "integer" ) // returns -2 + cache.decrement( "integer", 5 ) // returns -7 +} +``` + +## Use of Lists + +```scala + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import play.api.cache.redis.CacheApi + +class MyController @Inject() ( cache: CacheApi ) { + + // enables List operations + // Scala wrapper over the list at this key + cache.list[ String ]( "my-list" ) + + // get the whole list + cache.list[ String ]( "my-list" ).toList + + // prepend values, beware, values are prepended in the reversed order! + // result List( "EFG", "ABC" ) + cache.list[ String ]( "my-list" ).prepend( "ABC" ).prepend( "EFG" ) + "EFG" +: "ABC" +: cache.list[ String ]( "my-list" ) + List( "ABC", "EFG" ) ++: cache.list[ String ]( "my-list" ) + + // append values to the list + // result List( "ABC", "EFG" ) + cache.list[ String ]( "my-list" ).append( "ABC" ).append( "EFG" ) + cache.list[ String ]( "my-list" ) :+ "ABC" :+ "EFG" + cache.list[ String ]( "my-list" ) :++ List( "ABC", "EFG" ) + + // getting a value + cache.list[ String ]( "my-list" ).apply( index = 1 ) // get or an exception + cache.list[ String ]( "my-list" ).get( index = 1 ) // Some or None + cache.list[ String ]( "my-list" ).head // get or an exception + cache.list[ String ]( "my-list" ).headOption // Some or None + cache.list[ String ]( "my-list" ).headPop // Some or None and REMOVE the head + cache.list[ String ]( "my-list" ).last // get or an exception + cache.list[ String ]( "my-list" ).lastOption // Some or None + + // size of the list + cache.list[ String ]( "my-list" ).size + + // overwrite the value at index + cache.list[ String ]( "my-list" ).set( position = 1, element = "HIJ" ) + + // remove the value + cache.list[ String ]( "my-list" ).remove( "ABC", count = 2 ) // remove by value + cache.list[ String ]( "my-list" ).removeAt( position = 1 ) // remove by index + + // returns an API to reading but not modifying the list + cache.list[ String ]( "my-list" ).view + + // returns an API to modify the underlying list + cache.list[ String ]( "my-list" ).modify +} +``` + +## Use of Sets + +```scala + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import play.api.cache.redis.CacheApi + +class MyController @Inject() ( cache: CacheApi ) { + + // enables Set operations + // Scala wrapper over the set at this key + cache.set[ String ]( "my-set" ) + + // get the whole set + cache.set[ String ]( "my-set" ).toSet + + // add values into the set + cache.set[ String ]( "my-set" ).add( "ABC", "EDF" ) + + // test existence in the set + cache.set[ String ]( "my-set" ).contains( "ABC" ) + + // size of the set + cache.set[ String ]( "my-set" ).size + cache.set[ String ]( "my-set" ).isEmpty + cache.set[ String ]( "my-set" ).nonEmpty + + // remove the value + cache.set[ String ]( "my-set" ).remove( "ABC" ) +} +``` + +## Use of Maps: + +```scala + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import play.api.cache.redis.CacheApi + +class MyController @Inject() ( cache: CacheApi ) { + + // enables Set operations + // Scala wrapper over the map at this key + cache.map[ Int ]( "my-map" ) + + // get the whole map + cache.map[ Int ]( "my-map" ).toMap + cache.map[ Int ]( "my-map" ).keySet + cache.map[ Int ]( "my-map" ).values + + // test existence in the map + cache.map[ Int ]( "my-map" ).contains( "ABC" ) + + // get single value + cache.map[ Int ]( "my-map" ).get( "ABC" ) + + // add values into the map + cache.map[ Int ]( "my-map" ).add( "ABC", 5 ) + + // size of the map + cache.map[ Int ]( "my-map" ).size + cache.map[ Int ]( "my-map" ).isEmpty + cache.map[ Int ]( "my-map" ).nonEmpty + + // remove the value + cache.map[ Int ]( "my-map" ).remove( "ABC" ) +} +``` diff --git a/doc/40-migration.md b/doc/40-migration.md new file mode 100644 index 00000000..66cc8289 --- /dev/null +++ b/doc/40-migration.md @@ -0,0 +1,48 @@ +# Migration Guide + +This document extends the [CHANGELOG](https://github.com/KarelCemus/play-redis/blob/master/CHANGELOG.md) summing up the most of the changes. There are discussed only the complex changes to provide tips on migration. + +Play-redis is a matter of regular development since we try to upkeep it in the best +possible way. To fulfill requirements of our users, we make our solution to be up-to-date +and increase its capabilities with every increment. This time, it required from us to broke the +backward compatibility in a few places. This Migration Guide allows you to adjust your usage +in the smoothest possible way. We believe that removing technical debt as soon as possible +is the only way of keeping this project in a good shape. + + +## Migration from 1.6.x to 2.0.x + +The major goal of the 2.0.x version was to support named caches and clean up the code to ease maintenance. In the consequence, there was major redesign of the configuration, several properties were removed or replaced and some other were introduced instead. However, for simple cases and probably most setups, the configuration can be left intact. + +### Configuration changes + +- default database was changed from 1 to 0 (redis default) to remove the inconsistency between play-redis and the redis itself +- the cache instance defined directly under the `play.cache.redis` is a default instance and is named with a default name +- introduced `instances` property to support named caches. See [documentation](https://github.com/KarelCemus/play-redis/wiki/Configuration#named-caches) for more details. +- `configuration` property was redesigned and renamed to `source`. Valid values are now `standalone`, `cluster`, `connection-string`, and `custom`. See [documentation](https://github.com/KarelCemus/play-redis/wiki/Configuration#standalone-vs-cluster) for more details. +- `connection-string-variable` was replaced by the `connection-string` property defining the connection string itself. The value is now passed directly into the property through, e.g., `${REDIS_URL}`, which HOCON correctly resolves. This applies when combined with `source: connection-string`. +- `wait` property renamed to `timeout` +- `source`, `timeout`, `dispatcher`, and `recovery` define defaults and may be locally overridden within each named cache configuration. +- introduced `bind-default` property (default is true) indicating whether to bind the default instance to unqualified APIs. +- introduced `default-cache` property (default is `play`) defining the name of the default instance + +### API changes + +Removed implementation of deprecated `play.cache.CacheApi` and `play.api.cache.CacheApi`. + +`RecoveryPolicy` was moved from `play.api.cache.redis.impl` to `play.api.cache.redis`. + +Joda time implicit helpers for expiration computation in the `ExpirationImplicits` was deprecated as Play 2.6 deprecated joda-time library. There were introduced implicits for `java.time.LocalDateTime` instead. + +#### Runtime DI (Guice) + +Major redesign of `RedisCacheModule`, however, no changes in use are expected. Internal components are no longer registered into DI container. + +#### Compile-time DI + +Major redesign of `RedisCacheComponents`. See the [trait](https://github.com/KarelCemus/play-redis/blob/master/src/main/scala/play/api/cache/redis/RedisCacheComponents.scala#L14) for details. To create +new API, call `cacheApi( instance )`, where the instance is either a String with the instance name or `RedisInstance` object with a custom configuration. This returns a `RedisCaches` object encapsulating all available APIs. In case of using multiple different APIs, it is suggested to **reuse this object** to prevent the duplicate creation of instances of the same cache connector. To provide a custom instance configuration, either override `redisInstanceResolver` mapping names to the objects or pass the `RedisInstance` object directly to the `cacheApi` call. For custom recovery policy override `recoveryPolicyResolver`. + +### Implementation changes + +There are many changes under the hood to simplify the code, improve readability and ease the maintenance. The configuration loaders are fully rewritten as consequence of the configuration redesign. Some files we moved to other packages to better fit their purpose. However, there are basically **no changes to the cache implementation itself**. All the changes are related to the supporting classes and object, there was major refactoring. For full details, please consult the Git history. diff --git a/doc/images/redis-settings.png b/doc/images/redis-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..7d93e6983800090ebdf8333e5574e71d12ca7c61 GIT binary patch literal 27442 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 \ No newline at end of file diff --git a/version.sbt b/version.sbt index 7e55eb27..121071ca 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "2.0.2-SNAPSHOT" +version in ThisBuild := "2.0.2"