├── samples ├── java │ ├── public │ │ ├── stylesheets │ │ │ └── main.css │ │ ├── javascripts │ │ │ └── hello.js │ │ └── images │ │ │ └── favicon.png │ ├── project │ │ ├── build.properties │ │ └── plugins.sbt │ ├── app │ │ ├── views │ │ │ ├── index.scala.html │ │ │ └── main.scala.html │ │ └── controllers │ │ │ └── Application.java │ ├── .gitignore │ ├── build.sbt │ ├── conf │ │ ├── routes │ │ ├── logback.xml │ │ └── application.conf │ ├── LICENSE │ └── test │ │ ├── IntegrationTest.java │ │ └── ApplicationTest.java └── scala │ ├── public │ ├── stylesheets │ │ └── main.css │ ├── javascripts │ │ └── hello.js │ └── images │ │ └── favicon.png │ ├── project │ ├── build.properties │ └── plugins.sbt │ ├── .gitignore │ ├── app │ ├── views │ │ ├── index.scala.html │ │ └── main.scala.html │ └── controllers │ │ └── Application.scala │ ├── build.sbt │ ├── LICENSE │ ├── test │ ├── IntegrationSpec.scala │ └── ApplicationSpec.scala │ └── conf │ ├── logback.xml │ ├── routes │ └── application.conf ├── project ├── build.properties └── plugins.sbt ├── sonatype.sbt ├── .gitignore ├── publish.sh ├── LICENSE ├── .travis.yml ├── plugin └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── com │ │ └── github │ │ └── mumoshu │ │ └── play2 │ │ └── memcached │ │ ├── Slf4JLogger.scala │ │ ├── api │ │ └── MemcachedComponents.scala │ │ ├── CustomSerializing.scala │ │ ├── MemcachedCacheApiProvider.scala │ │ ├── MemcachedComponents.java │ │ ├── MemcachedModule.scala │ │ ├── MemcachedClientProvider.scala │ │ └── MemcachedCacheApi.scala │ └── test │ └── MemcachedSpec.scala └── README.md /samples/java/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/scala/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /samples/java/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /samples/scala/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | import xerial.sbt.Sonatype._ 2 | 3 | sonatypeProfileName := "com.github.mumoshu" 4 | -------------------------------------------------------------------------------- /samples/java/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Play plugin 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0") 3 | 4 | -------------------------------------------------------------------------------- /samples/scala/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Play plugin 2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0") 3 | 4 | -------------------------------------------------------------------------------- /samples/java/public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } -------------------------------------------------------------------------------- /samples/scala/public/javascripts/hello.js: -------------------------------------------------------------------------------- 1 | if (window.console) { 2 | console.log("Welcome to your Play application's JavaScript!"); 3 | } -------------------------------------------------------------------------------- /samples/java/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play2-memcached/main/samples/java/public/images/favicon.png -------------------------------------------------------------------------------- /samples/scala/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/play2-memcached/main/samples/scala/public/images/favicon.png -------------------------------------------------------------------------------- /samples/java/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play") { 4 | 5 | Your new application is ready. 6 | 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | *~ 9 | .idea/ 10 | .settings/ 11 | .bsp/ 12 | -------------------------------------------------------------------------------- /samples/java/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | /.idea 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | .bsp/ 10 | -------------------------------------------------------------------------------- /samples/scala/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | /.idea 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | .bsp/ 10 | -------------------------------------------------------------------------------- /samples/scala/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play") { 4 | 5 | Your new application is ready. 6 | 7 | } 8 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PLAY_VERSION=2.8.0 sbt ++2.12.10 publishSigned sonatypeRelease 4 | PLAY_VERSION=2.8.0 sbt ++2.13.1 publishSigned sonatypeRelease 5 | -------------------------------------------------------------------------------- /samples/java/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies ++= Seq( 2 | javaJdbc, 3 | cacheApi, 4 | javaWs, 5 | "com.github.mumoshu" %% "play2-memcached-play29" % "0.12.0-SNAPSHOT" 6 | ) 7 | -------------------------------------------------------------------------------- /samples/scala/build.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies ++= Seq( 2 | jdbc, 3 | cacheApi, 4 | ws, 5 | "com.github.mumoshu" %% "play2-memcached-play29" % "0.12.0-SNAPSHOT", 6 | specs2 % Test 7 | ) 8 | 9 | resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases" 10 | -------------------------------------------------------------------------------- /samples/java/app/controllers/Application.java: -------------------------------------------------------------------------------- 1 | package controllers; 2 | 3 | import play.*; 4 | import play.mvc.*; 5 | 6 | import views.html.*; 7 | 8 | public class Application extends Controller { 9 | 10 | public Result index() { 11 | return ok(index.render("Your new application is ready.")); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | ivyLoggingLevel := UpdateLogging.Full 2 | 3 | val playVersion = scala.util.Properties.envOrElse("PLAY_VERSION", "2.9.0") 4 | 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % playVersion) 6 | 7 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.10.0") 8 | 9 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") 10 | -------------------------------------------------------------------------------- /samples/java/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index() 7 | 8 | # Map static resources from the /public folder to the /assets URL path 9 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) 10 | -------------------------------------------------------------------------------- /samples/java/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 |
7 |Usage:
14 | * 15 | *
16 | * public class MyComponents extends BuiltInComponentsFromContext implements MemcachedComponents {
17 | *
18 | * public MyComponents(ApplicationLoader.Context context) {
19 | * super(context);
20 | * }
21 | *
22 | * // A service class that depends on cache APIs
23 | * public CachedService someService() {
24 | * // defaultCacheApi is provided by MemcachedComponents
25 | * return new CachedService(defaultCacheApi());
26 | * }
27 | *
28 | * // Another service that depends on a specific named cache
29 | * public AnotherService someService() {
30 | * // cacheApi provided by MemcachedComponents and
31 | * // "anotherService" is the name of the cache.
32 | * return new CachedService(cacheApi("anotherService"));
33 | * }
34 | *
35 | * // other methods
36 | * }
37 | *
38 | */
39 | public interface MemcachedComponents extends ConfigurationComponents, AkkaComponents {
40 |
41 | Environment environment();
42 |
43 | ApplicationLifecycle applicationLifecycle();
44 |
45 | default MemcachedClientProvider memcachedClientProvider() {
46 | return new MemcachedClientProvider(
47 | config(),
48 | applicationLifecycle().asScala()
49 | );
50 | }
51 |
52 | default AsyncCacheApi cacheApi(String name) {
53 | play.api.cache.AsyncCacheApi scalaAsyncCacheApi = new MemcachedCacheApi(name, memcachedClientProvider().get(), config(), environment().asScala(), executionContext());
54 | return new DefaultAsyncCacheApi(scalaAsyncCacheApi);
55 | }
56 |
57 | default AsyncCacheApi defaultCacheApi() {
58 | return cacheApi("play");
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/plugin/src/test/MemcachedSpec.scala:
--------------------------------------------------------------------------------
1 | import com.github.mumoshu.play2.memcached._
2 | import net.spy.memcached.MemcachedClient
3 | import org.specs2.mutable._
4 | import play.api.cache.AsyncCacheApi
5 | import play.api.inject.guice.GuiceApplicationBuilder
6 | import play.cache.NamedCacheImpl
7 | import play.api.test.WithApplication
8 |
9 | import scala.jdk.CollectionConverters._
10 |
11 | object MemcachedSpec extends Specification {
12 |
13 | sequential
14 |
15 | val memcachedHost = "127.0.0.1:11211"
16 |
17 | val environment = play.api.Environment.simple()
18 |
19 | val configurationMap: Map[String, Object] = Map(
20 | "play.modules.enabled" -> List(
21 | "play.api.i18n.I18nModule",
22 | "play.api.mvc.CookiesModule",
23 | "com.github.mumoshu.play2.memcached.MemcachedModule",
24 | "play.api.inject.BuiltinModule"
25 | ).asJava,
26 | "play.allowGlobalApplication" -> "false",
27 | "play.cache.defaultCache" -> "default",
28 | "play.cache.bindCaches" -> List("secondary").asJava,
29 | "memcached.1.host" -> memcachedHost
30 | )
31 | val configuration = play.api.Configuration.from(
32 | configurationMap
33 | )
34 |
35 | val modules = play.api.inject.Modules.locate(environment, configuration)
36 |
37 | "play2-memcached" should {
38 | "provide MemcachedModule" in {
39 | (modules.find { module => module.isInstanceOf[MemcachedModule] }.get).asInstanceOf[MemcachedModule] must beAnInstanceOf[MemcachedModule]
40 | }
41 | }
42 |
43 | "Module" should {
44 | "provide bindings" in {
45 | val memcachedModule = (modules.find { module => module.isInstanceOf[MemcachedModule] }.get).asInstanceOf[MemcachedModule]
46 |
47 | val bindings = memcachedModule.bindings(environment, configuration)
48 |
49 | bindings.size mustNotEqual (0)
50 | }
51 | }
52 |
53 | "Injector" should {
54 |
55 | def app = GuiceApplicationBuilder().configure(configurationMap).build()
56 |
57 | "provide memcached clients" in new WithApplication(app) {
58 | override def running() = {
59 | val memcachedClient = this.app.injector.instanceOf(play.api.inject.BindingKey(classOf[MemcachedClient]))
60 |
61 | memcachedClient must beAnInstanceOf[MemcachedClient]
62 | }
63 | }
64 |
65 | "provide a CacheApi implementation backed by memcached" in new WithApplication(app) {
66 | override def running() = {
67 | val cacheApi = this.app.injector.instanceOf(play.api.inject.BindingKey(classOf[AsyncCacheApi]))
68 |
69 | cacheApi must beAnInstanceOf[MemcachedCacheApi]
70 | cacheApi.asInstanceOf[MemcachedCacheApi].namespace must equalTo ("default")
71 | }
72 | }
73 |
74 | "provide a named CacheApi implementation backed by memcached" in new WithApplication(app) {
75 | override def running() = {
76 | val cacheApi = this.app.injector.instanceOf(play.api.inject.BindingKey(classOf[AsyncCacheApi]).qualifiedWith(new NamedCacheImpl("secondary")))
77 |
78 | cacheApi must beAnInstanceOf[MemcachedCacheApi]
79 | cacheApi.asInstanceOf[MemcachedCacheApi].namespace must equalTo ("secondary")
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/samples/scala/test/ApplicationSpec.scala:
--------------------------------------------------------------------------------
1 | import org.specs2.mutable._
2 | import org.specs2.runner._
3 | import org.junit.runner._
4 | import play.api.cache.SyncCacheApi
5 |
6 | import play.api.Application
7 | import play.api.mvc.Result
8 | import play.api.test._
9 | import play.api.test.Helpers._
10 | import play.cache.NamedCacheImpl
11 | import play.api.inject.guice.GuiceApplicationBuilder
12 |
13 | import scala.concurrent.Future
14 |
15 | /**
16 | * Add your spec here.
17 | * You can mock out a whole application including requests, plugins etc.
18 | * For more information, consult the wiki.
19 | */
20 | @RunWith(classOf[JUnitRunner])
21 | class ApplicationSpec extends Specification {
22 |
23 | sequential
24 |
25 | def app = GuiceApplicationBuilder().configure(Map(
26 | "memcached.host" -> "127.0.0.1:11211"
27 | )).build()
28 |
29 | "Application" should {
30 |
31 | "send 404 on a bad request" in new WithApplication(app){
32 | override def running() = {
33 | route(this.app, FakeRequest(GET, "/boum")) must beSome[Future[Result]].which (status(_) == NOT_FOUND)
34 | }
35 | }
36 |
37 | "render the index page" in new WithApplication(app){
38 | override def running() = {
39 | val home = route(this.app, FakeRequest(GET, "/")).get
40 |
41 | status(home) must equalTo(OK)
42 | contentType(home) must beSome[String].which(_ == "text/html")
43 | contentAsString(home) must contain ("Your new application is ready.")
44 | }
45 | }
46 | }
47 |
48 | def connectingLocalMemcached[T](app: Application)(block: => T):T = {
49 |
50 | app.injector.instanceOf[SyncCacheApi].remove("key")
51 |
52 | val bindingKey = play.api.inject.BindingKey(classOf[SyncCacheApi]).qualifiedWith(new NamedCacheImpl("session-cache"))
53 | app.injector.instanceOf(bindingKey).remove("key")
54 |
55 | block
56 | }
57 |
58 | def c(app: Application, url: String): String = contentAsString(route(app, FakeRequest(GET, url)).get)
59 |
60 | "The scala sample application" should {
61 |
62 | "return a cached data" in new WithApplication(app){
63 | override def running() = {
64 | connectingLocalMemcached(this.app) {
65 | c(this.app, "/cache/get") must equalTo ("None")
66 | c(this.app, "/cache/set") must equalTo ("Cached.")
67 | c(this.app, "/cache/get") must equalTo ("Some(cached value)")
68 | c(this.app, "/session/get") must equalTo ("None")
69 | c(this.app, "/session/set") must equalTo ("Cached.")
70 | c(this.app, "/session/get") must equalTo ("Some(session value)")
71 | c(this.app, "/cache/get") must equalTo ("Some(cached value)")
72 | }
73 | }
74 | }
75 |
76 | "return expected results" in new WithApplication(app){
77 | override def running() = {
78 | connectingLocalMemcached(this.app) {
79 | c(this.app, "/cacheInt") must equalTo ("123class java.lang.Integer 123 int")
80 | c(this.app, "/cacheString") must equalTo ("Some(mikoto)")
81 | c(this.app, "/cacheBool") must equalTo ("true : class java.lang.Boolean, true : boolean")
82 | }
83 | }
84 | }
85 | }
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedModule.scala:
--------------------------------------------------------------------------------
1 | package com.github.mumoshu.play2.memcached
2 |
3 | import akka.stream.Materializer
4 |
5 | import play.api.cache._
6 | import play.api.inject._
7 |
8 | import play.cache.{ AsyncCacheApi => JavaAsyncCacheApi, SyncCacheApi => JavaSyncCacheApi, DefaultAsyncCacheApi => JavaDefaultAsyncCacheApi, DefaultSyncCacheApi => JavaDefaultSyncCacheApi, NamedCacheImpl }
9 |
10 | import javax.inject.{Inject, Singleton, Provider}
11 |
12 | import net.spy.memcached.MemcachedClient
13 |
14 | import scala.reflect.ClassTag
15 |
16 | class MemcachedModule extends SimpleModule((environment, configuration) => {
17 |
18 | import scala.jdk.CollectionConverters._
19 |
20 | val defaultCacheName = configuration.underlying.getString("play.cache.defaultCache")
21 | val bindCaches = configuration.underlying.getStringList("play.cache.bindCaches").asScala
22 |
23 | // Creates a named cache qualifier
24 | def named(name: String): NamedCache = {
25 | new NamedCacheImpl(name)
26 | }
27 |
28 | // bind wrapper classes
29 | def wrapperBindings(cacheApiKey: BindingKey[AsyncCacheApi], namedCache: NamedCache): Seq[Binding[_]] = Seq(
30 | bind[JavaAsyncCacheApi].qualifiedWith(namedCache).to(new NamedJavaAsyncCacheApiProvider(cacheApiKey)),
31 | bind[Cached].qualifiedWith(namedCache).to(new NamedCachedProvider(cacheApiKey)),
32 | bind[SyncCacheApi].qualifiedWith(namedCache).to(new NamedSyncCacheApiProvider(cacheApiKey)),
33 | bind[JavaSyncCacheApi].qualifiedWith(namedCache).to(new NamedJavaSyncCacheApiProvider(cacheApiKey))
34 | )
35 |
36 | // bind a cache with the given name
37 | def bindCache(name: String) = {
38 | val namedCache = named(name)
39 | val cacheApiKey = bind[AsyncCacheApi].qualifiedWith(namedCache)
40 | Seq(
41 | cacheApiKey.to(new MemcachedCacheApiProvider(name, bind[MemcachedClient], configuration.underlying, environment))
42 | ) ++ wrapperBindings(cacheApiKey, namedCache)
43 | }
44 |
45 | def bindDefault[T: ClassTag]: Binding[T] = {
46 | bind[T].to(bind[T].qualifiedWith(named(defaultCacheName)))
47 | }
48 |
49 | Seq(
50 | bind[MemcachedClient].toProvider[MemcachedClientProvider],
51 | // alias the default cache to the unqualified implementation
52 | bindDefault[AsyncCacheApi],
53 | bindDefault[JavaAsyncCacheApi],
54 | bindDefault[SyncCacheApi],
55 | bindDefault[JavaSyncCacheApi]
56 | ) ++ bindCache(defaultCacheName) ++ bindCaches.flatMap(bindCache)
57 | })
58 |
59 | private class NamedSyncCacheApiProvider(key: BindingKey[AsyncCacheApi])
60 | extends Provider[SyncCacheApi] {
61 | @Inject private var injector: Injector = _
62 | lazy val get: SyncCacheApi =
63 | new DefaultSyncCacheApi(injector.instanceOf(key))
64 | }
65 |
66 | private class NamedJavaAsyncCacheApiProvider(key: BindingKey[AsyncCacheApi]) extends Provider[JavaAsyncCacheApi] {
67 | @Inject private var injector: Injector = _
68 | lazy val get: JavaAsyncCacheApi =
69 | new JavaDefaultAsyncCacheApi(injector.instanceOf(key))
70 | }
71 |
72 | private class NamedJavaSyncCacheApiProvider(key: BindingKey[AsyncCacheApi])
73 | extends Provider[JavaSyncCacheApi] {
74 | @Inject private var injector: Injector = _
75 | lazy val get: JavaSyncCacheApi =
76 | new JavaDefaultSyncCacheApi(new JavaDefaultAsyncCacheApi(injector.instanceOf(key)))
77 | }
78 |
79 | private class NamedCachedProvider(key: BindingKey[AsyncCacheApi]) extends Provider[Cached] {
80 | @Inject private var injector: Injector = _
81 | lazy val get: Cached =
82 | new Cached(injector.instanceOf(key))(injector.instanceOf[Materializer])
83 | }
84 |
--------------------------------------------------------------------------------
/plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedClientProvider.scala:
--------------------------------------------------------------------------------
1 | package com.github.mumoshu.play2.memcached
2 |
3 | import com.typesafe.config.Config
4 | import net.spy.memcached.auth.{PlainCallbackHandler, AuthDescriptor}
5 | import play.api.Logger
6 | import play.api.inject.ApplicationLifecycle
7 |
8 | import scala.concurrent.Future
9 |
10 | import javax.inject.{Inject, Singleton, Provider}
11 |
12 | import net.spy.memcached.{KetamaConnectionFactory, ConnectionFactoryBuilder, AddrUtil, MemcachedClient, DefaultConnectionFactory}
13 |
14 | @Singleton
15 | class MemcachedClientProvider @Inject() (config: Config, lifecycle: ApplicationLifecycle) extends Provider[MemcachedClient] {
16 | lazy val logger = Logger("memcached.plugin")
17 | lazy val get: MemcachedClient = {
18 | val client = {
19 | System.setProperty("net.spy.log.LoggerImpl", "com.github.mumoshu.play2.memcached.Slf4JLogger")
20 |
21 | if(config.hasPath(MemcachedClientProvider.CONFIG_ELASTICACHE_CONFIG_ENDPOINT)) {
22 | new MemcachedClient(AddrUtil.getAddresses(config.getString(MemcachedClientProvider.CONFIG_ELASTICACHE_CONFIG_ENDPOINT)))
23 | } else {
24 | lazy val singleHost = if(config.hasPath(MemcachedClientProvider.CONFIG_HOST)) {
25 | Option(AddrUtil.getAddresses(config.getString(MemcachedClientProvider.CONFIG_HOST)))
26 | } else {
27 | None
28 | }
29 | lazy val multipleHosts = if(config.hasPath("memcached.1.host")) {
30 | def accumulate(nb: Int): String = {
31 | if(config.hasPath("memcached." + nb + ".host")) {
32 | val h = config.getString("memcached." + nb + ".host")
33 | h + " " + accumulate(nb + 1)
34 | } else {
35 | ""
36 | }
37 | }
38 | Option(AddrUtil.getAddresses(accumulate(1).trim()))
39 | } else {
40 | None
41 | }
42 |
43 | val addrs = singleHost.orElse(multipleHosts)
44 | .getOrElse(throw new RuntimeException("Bad configuration for memcached: missing host(s)"))
45 |
46 | if(config.hasPath(MemcachedClientProvider.CONFIG_USER)) {
47 | val memcacheUser = config.getString(MemcachedClientProvider.CONFIG_USER)
48 | val memcachePassword = if(config.hasPath(MemcachedClientProvider.CONFIG_PASSWORD)) {
49 | config.getString(MemcachedClientProvider.CONFIG_PASSWORD)
50 | } else {
51 | throw new RuntimeException("Bad configuration for memcached: missing password")
52 | }
53 |
54 | // Use plain SASL to connect to memcached
55 | val ad = new AuthDescriptor(Array("PLAIN"),
56 | new PlainCallbackHandler(memcacheUser, memcachePassword))
57 | lazy val consistentHashing = config.getBoolean("memcached.consistentHashing")
58 | val cf = (if (consistentHashing) new ConnectionFactoryBuilder(new KetamaConnectionFactory()) else new ConnectionFactoryBuilder())
59 | .setProtocol(ConnectionFactoryBuilder.Protocol.BINARY)
60 | .setTimeoutExceptionThreshold(
61 | if(config.hasPath(MemcachedClientProvider.CONFIG_MAX_TIMEOUT_EXCEPTION_THRESHOLD)) {
62 | config.getInt(MemcachedClientProvider.CONFIG_MAX_TIMEOUT_EXCEPTION_THRESHOLD)
63 | } else {
64 | DefaultConnectionFactory.DEFAULT_MAX_TIMEOUTEXCEPTION_THRESHOLD
65 | })
66 | .setAuthDescriptor(ad)
67 | .build()
68 |
69 | new MemcachedClient(cf, addrs)
70 | } else {
71 | new MemcachedClient(addrs)
72 | }
73 | }
74 | }
75 |
76 | logger.info("Starting MemcachedPlugin.")
77 |
78 | lifecycle.addStopHook(() => Future.successful {
79 | logger.info("Stopping MemcachedPlugin.")
80 | client.shutdown()
81 | })
82 |
83 | client
84 | }
85 | }
86 |
87 | object MemcachedClientProvider {
88 | final val CONFIG_ELASTICACHE_CONFIG_ENDPOINT = "elasticache.config.endpoint"
89 | final val CONFIG_MAX_TIMEOUT_EXCEPTION_THRESHOLD = "memcached.max-timeout-exception-threshold"
90 | final val CONFIG_HOST = "memcached.host"
91 | final val CONFIG_USER = "memcached.user"
92 | final val CONFIG_PASSWORD = "memcached.password"
93 | }
94 |
--------------------------------------------------------------------------------
/plugin/src/main/com/github/mumoshu/play2/memcached/MemcachedCacheApi.scala:
--------------------------------------------------------------------------------
1 | package com.github.mumoshu.play2.memcached
2 |
3 | import akka.Done
4 |
5 | import com.typesafe.config.Config
6 | import net.spy.memcached.transcoders.Transcoder
7 | import net.spy.memcached.internal.{ GetCompletionListener, GetFuture, OperationCompletionListener, OperationFuture }
8 | import net.spy.memcached.ops.StatusCode
9 | import play.api.cache.AsyncCacheApi
10 | import play.api.{Environment, Logger}
11 |
12 | import javax.inject.{Inject, Singleton}
13 |
14 | import scala.concurrent.duration.Duration
15 |
16 | import net.spy.memcached.MemcachedClient
17 |
18 | import scala.concurrent.{ ExecutionContext, Future, Promise }
19 | import scala.reflect.ClassTag
20 |
21 | @Singleton
22 | class MemcachedCacheApi @Inject() (val namespace: String, val client: MemcachedClient, config: Config, environment: Environment)(implicit context: ExecutionContext) extends AsyncCacheApi {
23 | lazy val logger = Logger("memcached.plugin")
24 | lazy val tc = new CustomSerializing(environment.classLoader).asInstanceOf[Transcoder[Any]]
25 | lazy val hashkeys: String = config.getString("memcached.hashkeys")
26 | lazy val throwExceptionFromGetOnError: Boolean = config.getBoolean("memcached.throwExceptionFromGetOnError")
27 |
28 | def get[T: ClassTag](key: String): Future[Option[T]] = {
29 | if (key.isEmpty) {
30 | Future.successful(None)
31 | } else {
32 | val ct = implicitly[ClassTag[T]]
33 |
34 | logger.debug("Getting the cache for key " + namespace + key)
35 | val p = Promise[Option[T]]() // create incomplete promise/future
36 | client.asyncGet(namespace + hash(key), tc).addListener(new GetCompletionListener() {
37 | def onComplete(result: GetFuture[_]): Unit = {
38 | try {
39 | result.getStatus().getStatusCode() match {
40 | case StatusCode.SUCCESS => {
41 | val any = result.get
42 | logger.debug("any is " + any.getClass)
43 | p.success(Option(
44 | any match {
45 | case x if ct.runtimeClass.isInstance(x) => x.asInstanceOf[T]
46 | case x if ct == ClassTag.Nothing => x.asInstanceOf[T]
47 | case x => x.asInstanceOf[T]
48 | }
49 | ))
50 | }
51 | case StatusCode.ERR_NOT_FOUND => {
52 | logger.debug("Cache miss for " + namespace + key)
53 | p.success(None)
54 | }
55 | case _ => {
56 | fail(result, None)
57 | }
58 | }
59 | } catch {
60 | case e: Throwable => fail(result, Some(e))
61 | }
62 | }
63 |
64 | def fail(result: GetFuture[_], exception: Option[Throwable]): Unit = {
65 | val msg = "An error has occured while getting the value from memcached. ct=" + ct + ". key=" + key + ". " +
66 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage()
67 | if (throwExceptionFromGetOnError) {
68 | p.failure(exception.fold(new RuntimeException(msg))(new RuntimeException(msg, _)))
69 | } else {
70 | logger.error(msg, exception.orNull)
71 | p.success(None)
72 | }
73 | }
74 | })
75 | p.future
76 | }
77 | }
78 |
79 | def getOrElseUpdate[A: ClassTag](key: String, expiration: Duration)(orElse: => Future[A]): Future[A] = {
80 | get[A](key).flatMap {
81 | case Some(value) => Future.successful(value)
82 | case None => orElse.flatMap(value => set(key, value, expiration).map(_ => value))
83 | }
84 | }
85 |
86 | def set(key: String, value: Any, expiration: Duration = Duration.Inf): Future[Done] = {
87 | if (!key.isEmpty) {
88 | val p = Promise[Done]() // create incomplete promise/future
89 | val exp = if (expiration.isFinite) expiration.toSeconds.toInt else 0
90 | client.set(namespace + hash(key), exp, value, tc).addListener(new OperationCompletionListener() {
91 | def onComplete(result: OperationFuture[_]): Unit = {
92 | result.getStatus().getStatusCode() match {
93 | case StatusCode.SUCCESS => {
94 | p.success(Done)
95 | }
96 | case _ => {
97 | val msg = "An error has occured while setting the value in memcached. key=" + key + ". value=" + value + ". " +
98 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage()
99 | if (throwExceptionFromGetOnError) {
100 | p.failure(new RuntimeException(msg))
101 | } else {
102 | logger.error(msg)
103 | p.success(Done)
104 | }
105 | }
106 | }
107 | }
108 | })
109 | p.future
110 | } else {
111 | Future.successful(Done)
112 | }
113 | }
114 |
115 | def remove(key: String): Future[Done] = {
116 | if (!key.isEmpty) {
117 | val p = Promise[Done]() // create incomplete promise/future
118 | client.delete(namespace + hash(key)).addListener(new OperationCompletionListener() {
119 | def onComplete(result: OperationFuture[_]): Unit = {
120 | result.getStatus().getStatusCode() match {
121 | case StatusCode.SUCCESS => {
122 | p.success(Done)
123 | }
124 | case StatusCode.ERR_NOT_FOUND => {
125 | logger.debug("Cache miss when removing " + namespace + key)
126 | p.success(Done)
127 | }
128 | case _ => {
129 | val msg = "An error has occured while removing the value in memcached. key=" + key + ". " +
130 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage()
131 | if (throwExceptionFromGetOnError) {
132 | p.failure(new RuntimeException(msg))
133 | } else {
134 | logger.error(msg)
135 | p.success(Done)
136 | }
137 | }
138 | }
139 | }
140 | })
141 | p.future
142 | } else {
143 | Future.successful(Done)
144 | }
145 | }
146 |
147 | def removeAll(): Future[Done] = {
148 | val p = Promise[Done]() // create incomplete promise/future
149 | client.flush().addListener(new OperationCompletionListener() {
150 | def onComplete(result: OperationFuture[_]): Unit = {
151 | result.getStatus().getStatusCode() match {
152 | case StatusCode.SUCCESS => {
153 | p.success(Done)
154 | }
155 | case _ => {
156 | val msg = "An error has occured while removing all values from memcached. " +
157 | "spymemcached code: " + result.getStatus().getStatusCode() + " memcached code:" + result.getStatus().getMessage()
158 | if (throwExceptionFromGetOnError) {
159 | p.failure(new RuntimeException(msg))
160 | } else {
161 | logger.error(msg)
162 | p.success(Done)
163 | }
164 | }
165 | }
166 | }
167 | })
168 | p.future
169 | }
170 |
171 | // you may override hash implementation to use more sophisticated hashes, like xxHash for higher performance
172 | protected def hash(key: String): String = if(hashkeys == "off") key
173 | else java.security.MessageDigest.getInstance(hashkeys).digest(key.getBytes).map("%02x".format(_)).mkString
174 | }
175 |
176 | object MemcachedCacheApi {
177 | object ValFromJavaObject {
178 | def unapply(x: AnyRef): Option[AnyVal] = x match {
179 | case x: java.lang.Byte => Some(x.byteValue())
180 | case x: java.lang.Short => Some(x.shortValue())
181 | case x: java.lang.Integer => Some(x.intValue())
182 | case x: java.lang.Long => Some(x.longValue())
183 | case x: java.lang.Float => Some(x.floatValue())
184 | case x: java.lang.Double => Some(x.doubleValue())
185 | case x: java.lang.Character => Some(x.charValue())
186 | case x: java.lang.Boolean => Some(x.booleanValue())
187 | case _ => None
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Memcached Plugin for Play framework 2.x
2 | ---------------------------------------
3 |
4 | A Memcached implementation of Cache API for Play 2.x.
5 | Using spymemcached internally, which is the same as Play 1.x's default Cache implementation.
6 |
7 | ## Usage
8 |
9 | Add the following dependency to your Play project:
10 |
11 | ### Library dependencies
12 |
13 | For Play 2.6.x and newer:
14 | !!! Changed `play.modules.cache.*` config keys to `play.cache.*` !!!
15 |
16 | ```scala
17 | val appDependencies = Seq(
18 | play.PlayImport.cacheApi,
19 | "com.github.mumoshu" %% "play2-memcached-play26" % "0.9.2"
20 | )
21 | val main = Project(appName).enablePlugins(play.PlayScala).settings(
22 | version := appVersion,
23 | libraryDependencies ++= appDependencies,
24 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
25 | )
26 | ```
27 |
28 | For Play 2.5.x:
29 |
30 | ```scala
31 | val appDependencies = Seq(
32 | play.PlayImport.cache,
33 | "com.github.mumoshu" %% "play2-memcached-play25" % "0.8.0"
34 | )
35 | val main = Project(appName).enablePlugins(play.PlayScala).settings(
36 | version := appVersion,
37 | libraryDependencies ++= appDependencies,
38 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
39 | )
40 | ```
41 |
42 | For Play 2.4.x:
43 |
44 | ```scala
45 | val appDependencies = Seq(
46 | play.PlayImport.cache,
47 | "com.github.mumoshu" %% "play2-memcached-play24" % "0.7.0"
48 | )
49 | val main = Project(appName).enablePlugins(play.PlayScala).settings(
50 | version := appVersion,
51 | libraryDependencies ++= appDependencies,
52 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
53 | )
54 | ```
55 |
56 | For Play 2.3.x:
57 |
58 | ```scala
59 | val appDependencies = Seq(
60 | play.PlayImport.cache,
61 | "com.github.mumoshu" %% "play2-memcached-play23" % "0.7.0"
62 | )
63 | val main = Project(appName).enablePlugins(play.PlayScala).settings(
64 | version := appVersion,
65 | libraryDependencies ++= appDependencies,
66 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
67 | )
68 | ```
69 |
70 | For Play 2.2.0:
71 |
72 | ```scala
73 | val appDependencies = Seq(
74 | cache, // or play.Project.cache if not imported play.Project._
75 | "com.github.mumoshu" %% "play2-memcached" % "0.4.0" // or 0.5.0-RC1 to try the latest improvements
76 | )
77 | val main = play.Project(appName, appVersion, appDependencies).settings(
78 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
79 | )
80 | ```
81 |
82 |
83 | For Play 2.1.0:
84 |
85 | ```scala
86 | val appDependencies = Seq(
87 | "com.github.mumoshu" %% "play2-memcached" % "0.3.0.3"
88 | )
89 | val main = play.Project(appName, appVersion, appDependencies).settings(
90 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
91 | )
92 | ```
93 |
94 | For Play 2.0:
95 |
96 | ```scala
97 | val appDependencies = Seq(
98 | "com.github.mumoshu" %% "play2-memcached" % "0.2.4.3"
99 | )
100 | val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
101 | resolvers += "Spy Repository" at "http://files.couchbase.com/maven2" // required to resolve `spymemcached`, the plugin's dependency.
102 | )
103 | ```
104 |
105 | ### Configurations
106 |
107 | #### Starting with Play 2.6.x
108 |
109 | ```
110 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule"
111 |
112 | # Well-known configuration provided by Play
113 | play.cache.defaultCache=default
114 | play.cache.bindCaches=["db-cache", "user-cache", "session-cache"]
115 |
116 | # Tell play2-memcached where your memcached host is located at
117 | memcached.host="127.0.0.1:11211"
118 | ```
119 |
120 | #### For Play 2.4.x and above
121 |
122 | ```
123 | play.modules.enabled+="com.github.mumoshu.play2.memcached.MemcachedModule"
124 |
125 | # To avoid conflict with play2-memcached's Memcached-based cache module
126 | play.modules.disabled+="play.api.cache.EhCacheModule"
127 |
128 | # Well-known configuration provided by Play
129 | play.modules.cache.defaultCache=default
130 | play.modules.cache.bindCaches=["db-cache", "user-cache", "session-cache"]
131 |
132 | # Tell play2-memcached where your memcached host is located at
133 | memcached.host="127.0.0.1:11211"
134 | ```
135 |
136 | #### Play 2.3.x or below
137 |
138 | Add a reference to the plugin in `play.plugins` file.
139 | `play.plugins` file must be put somewhere in the classpath.
140 | My recommendation is to put it in `conf/` directory.
141 |
142 | ```
143 | 5000:com.github.mumoshu.play2.memcached.MemcachedPlugin
144 | ```
145 |
146 | First of all, in `application.conf`, disable the EhCachePlugin - Play's default implementation of CacheAPI:
147 |
148 | ```
149 | ehcacheplugin=disabled
150 | ```
151 |
152 | Specify the host name or IP address of the memcached server, and the port number:
153 |
154 | ```
155 | memcached.host="127.0.0.1:11211"
156 | ```
157 |
158 | If you have multiple memcached instances over different host names or IP addresses, provide them like:
159 |
160 | ```
161 | memcached.1.host="mumocached1:11211"
162 | memcached.2.host="mumocached2:11211"
163 | ```
164 |
165 | ### Code examples
166 |
167 | #### For Play 2.4.x and above
168 |
169 | See the Play Framework documentation for the [Scala](https://www.playframework.com/documentation/2.6.x/ScalaCache) and [Java](https://www.playframework.com/documentation/2.6.x/JavaCache) API.
170 |
171 | #### For Play 2.3.x or below
172 |
173 | Then, you can use the `play.api.cache.Cache` object to store a value in memcached:
174 |
175 | ```scala
176 | Cache.set("key", "theValue")
177 | ```
178 |
179 | This way, memcached tries to retain the stored value eternally.
180 | Of course Memcached does not guarantee eternity of the value, nor can it retain the value on restart.
181 |
182 | If you want the value expired after some time:
183 |
184 | ```scala
185 | Cache.set("key", "theValueWithExpirationTime", 3600)
186 | // The value expires after 3600 seconds.
187 | ```
188 |
189 | To get the value for a key:
190 |
191 | ```scala
192 | val theValue = Cache.getAs[String]("key")
193 | ```
194 |
195 | You can remove the value (It's not yet a part of Play 2.0's Cache API, though):
196 |
197 | ```scala
198 | play.api.Play.current.plugin[MemcachedPlugin].get.api.remove("keyToRemove")
199 | ```
200 |
201 | ### Advanced configurations
202 |
203 | #### Disabling the plugin (For Play 2.3.x or below)
204 |
205 | You can disable the plugin in a similar manner to Play's build-in Ehcache Plugin.
206 | To disable the plugin in `application.conf`:
207 |
208 | ```
209 | memcachedplugin=disabled
210 | ```
211 |
212 | #### Authentication with SASL
213 |
214 | If you memcached requires the client an authentication with SASL, provide username/password like:
215 |
216 | ```
217 | memcached.user=misaka
218 | memcached.password=mikoto
219 | ```
220 |
221 | #### Configure logging
222 |
223 | By default, the plugin (or the spymemcached under the hood) does not output any logs at all.
224 | If you need to peek into what's going on, set the log level like:
225 |
226 | ##### For Play 2.4.x and above
227 |
228 | In your `logback.xml`:
229 |
230 | ```
231 |