├── .gitignore ├── project ├── .gnupg │ ├── pubring.gpg │ └── secring.gpg ├── plugins.sbt ├── build.properties └── publish ├── release-notes ├── 1.10.md ├── 1.9.md ├── 1.6.0.md ├── 1.8.md └── 1.7.md ├── run-benchmarks ├── src ├── test │ ├── scala │ │ └── shade │ │ │ ├── tests │ │ │ ├── Value.scala │ │ │ ├── CodecsSuite.scala │ │ │ ├── MemcachedTestHelpers.scala │ │ │ ├── InMemoryCacheVer2Suite.scala │ │ │ ├── FakeMemcachedSuite.scala │ │ │ └── MemcachedSuite.scala │ │ │ ├── testModels │ │ │ ├── Advertiser.scala │ │ │ ├── UserInfo.scala │ │ │ ├── Impression.scala │ │ │ ├── GeoIPLocation.scala │ │ │ ├── Session.scala │ │ │ ├── Offer.scala │ │ │ ├── ContentPiece.scala │ │ │ └── package.scala │ │ │ └── memcached │ │ │ └── internals │ │ │ └── MutablePartialResultSuite.scala │ └── resources │ │ └── logback.xml └── main │ ├── scala │ └── shade │ │ ├── memcached │ │ ├── internals │ │ │ ├── Result.scala │ │ │ ├── Status.scala │ │ │ ├── PartialResult.scala │ │ │ └── SpyMemcachedIntegration.scala │ │ ├── GenericCodecObjectInputStream.scala │ │ ├── FakeMemcached.scala │ │ ├── Configuration.scala │ │ ├── Codec.scala │ │ ├── Memcached.scala │ │ └── MemcachedImpl.scala │ │ ├── CacheException.scala │ │ └── inmemory │ │ └── InMemoryCache.scala │ └── java │ └── shade │ └── memcached │ └── internals │ └── Slf4jLogger.java ├── benchmarking └── src │ └── main │ └── scala │ └── shade │ └── benchmarks │ ├── IncDecrOps.scala │ ├── NonExistingKeyOps.scala │ ├── ExistingKeyOps.scala │ └── MemcachedBase.scala ├── LICENSE.txt ├── .travis.yml ├── CONTRIBUTING.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea* 2 | *.log 3 | .DS_Store 4 | target/ 5 | /.lib/ 6 | project/target/ 7 | -------------------------------------------------------------------------------- /project/.gnupg/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monix/shade/HEAD/project/.gnupg/pubring.gpg -------------------------------------------------------------------------------- /project/.gnupg/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monix/shade/HEAD/project/.gnupg/secring.gpg -------------------------------------------------------------------------------- /release-notes/1.10.md: -------------------------------------------------------------------------------- 1 | # Version 1.10.0 - Aug 16, 2017 2 | 3 | - Update SBT to 0.13.15 4 | - Update Scala versions 5 | - Update SpyMemcached 6 | - Add ability to configure the `timeoutThreshold` 7 | - Configure project for automatic releases from Travis 8 | -------------------------------------------------------------------------------- /run-benchmarks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # We need to explicitly set scala version because we get java.lang.NoSuchMethodError 4 | # Perhaps caused by SBT-JMH being on Scala 2.10.x 5 | sbt ++2.11.8 clean benchmarking/'jmh:run -i 10 -wi 10 -f3 -t 1' 6 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Classpaths.sbtPluginReleases 2 | 3 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.1") 4 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 5 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.18") 6 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "1.1") 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.9.3") 8 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | # See the project homepage at: https://github.com/monix/shade 4 | # 5 | # Licensed under the MIT License (the "License"); you may not use this 6 | # file except in compliance with the License. You may obtain a copy 7 | # of the License at: 8 | # 9 | # https://github.com/monix/shade/blob/master/LICENSE.txt 10 | # 11 | 12 | sbt.version=0.13.15 13 | -------------------------------------------------------------------------------- /src/test/scala/shade/tests/Value.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.tests 13 | 14 | case class Value(str: String) 15 | 16 | -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/Advertiser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | case class Advertiser( 15 | id: Option[Int], 16 | name: Option[String], 17 | serviceID: String) 18 | -------------------------------------------------------------------------------- /release-notes/1.9.md: -------------------------------------------------------------------------------- 1 | # Version 1.9.5 - May 3, 2017 2 | 3 | - Updated Monix to version 2.3.0 4 | - Updated Scala to 2.11.11 and 2.12.2 5 | 6 | # Version 1.9.4 - Mar 27, 2017 7 | 8 | - Updated Monix to version 2.2.4 9 | 10 | # Version 1.9.3 - Mar 10, 2017 11 | 12 | - Updated Monix to version 2.2.3 13 | 14 | # Version 1.9.2 - Feb 22, 2017 15 | 16 | - Updated Monix to version 2.2.2 17 | - Updated SpyMemcached to 2.12.2 18 | - Updated Slf4J to 1.7.23 19 | 20 | # Version 1.9.1 - Jan 27, 2016 21 | 22 | - Updated Monix to version 2.2.1 23 | 24 | # Version 1.9.0 - Jan 25, 2016 25 | 26 | - Updated Monix to version 2.2.0 27 | -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/UserInfo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | case class UserInfo( 15 | ip: String, 16 | forwardedFor: String, 17 | via: String, 18 | agent: String, 19 | geoip: Option[GeoIPLocation]) -------------------------------------------------------------------------------- /release-notes/1.6.0.md: -------------------------------------------------------------------------------- 1 | # Version 1.6.0 - April 26, 2014 2 | 3 | - clean up dependencies list 4 | - remove dependency on `scala-atomic` and replace it with [Monifu](https://github.com/alexandru/monifu), 5 | the new project to which my work on Scala Atomic references has moved 6 | - remove Akka's Scheduler dependency and replace it with 7 | Monifu's [Scheduler](https://github.com/alexandru/monifu/blob/master/docs/schedulers.md) 8 | - fix critical bug in the `InMemoryCache` implementation - bad case match 9 | - add cross-compilation settings - project Shade is now cross-compiled for 10 | Scala 2.10.4 and Scala 2.11.0 11 | - published to Maven Central -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/internals/Result.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached.internals 13 | 14 | sealed trait Result[+T] 15 | case class SuccessfulResult[+T](key: String, result: T) extends Result[T] 16 | case class FailedResult(key: String, state: Status) extends Result[Nothing] 17 | -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/Impression.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | case class Impression( 15 | uuid: String, 16 | session: Session, 17 | servedOffers: Seq[Offer] = Seq.empty, 18 | requestCount: Int = 0, 19 | alreadyServed: Boolean = false, 20 | clientVersion: Option[String] = None) -------------------------------------------------------------------------------- /benchmarking/src/main/scala/shade/benchmarks/IncDecrOps.scala: -------------------------------------------------------------------------------- 1 | package shade.benchmarks 2 | 3 | 4 | import scala.concurrent.duration._ 5 | import org.openjdk.jmh.annotations._ 6 | import org.openjdk.jmh.infra.Blackhole 7 | 8 | class IncDecrOps extends MemcachedBase { 9 | 10 | val key: String = "incr-decr" 11 | val duration: FiniteDuration = 1.day 12 | 13 | @Setup 14 | def prepare(): Unit = { 15 | memcached.awaitSet(key, 1E10.toLong.toString, duration) 16 | } 17 | 18 | @Benchmark 19 | def increment(bh: Blackhole): Unit = bh.consume{ 20 | memcached.awaitIncrement(key, 1L, None, duration) 21 | } 22 | 23 | @Benchmark 24 | def decrement(bh: Blackhole): Unit = bh.consume { 25 | memcached.awaitDecrement(key, 1L, None, duration) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/GeoIPLocation.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | case class GeoIPLocation( 15 | countryCode: String, 16 | city: Option[String], 17 | countryName: Option[String], 18 | latitude: Option[Float], 19 | longitude: Option[Float], 20 | areaCode: Option[Int], 21 | postalCode: Option[String], 22 | region: Option[String], 23 | dmaCode: Option[Int]) -------------------------------------------------------------------------------- /benchmarking/src/main/scala/shade/benchmarks/NonExistingKeyOps.scala: -------------------------------------------------------------------------------- 1 | package shade.benchmarks 2 | 3 | import scala.concurrent.duration._ 4 | import org.openjdk.jmh.annotations._ 5 | import org.openjdk.jmh.infra.Blackhole 6 | 7 | class NonExistingKeyOps extends MemcachedBase { 8 | 9 | val key: String = "non-existing" 10 | val duration: FiniteDuration = 1.day 11 | 12 | @Setup 13 | def prepare(): Unit = memcached.delete(key) 14 | 15 | @Benchmark 16 | def get(bh: Blackhole): Unit = bh.consume { 17 | memcached.awaitGet[String](key) 18 | } 19 | 20 | @Benchmark 21 | def set(bh: Blackhole): Unit = bh.consume { 22 | memcached.awaitSet(key, 1L, duration) 23 | } 24 | 25 | @Benchmark 26 | def delete(bh: Blackhole): Unit = bh.consume { 27 | memcached.awaitDelete(key) 28 | } 29 | } -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/Session.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | case class Session( 15 | uuid: String, 16 | deviceID: String, 17 | device: String, 18 | userInfo: UserInfo, 19 | appID: Option[String] = None, 20 | servedBy: Option[String] = None, 21 | userIP: Option[String] = None, 22 | locationLat: Option[Float] = None, 23 | locationLon: Option[Float] = None, 24 | countryCode: Option[String] = None) 25 | 26 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 15 | %-5level %logger{36} - %msg%n 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /benchmarking/src/main/scala/shade/benchmarks/ExistingKeyOps.scala: -------------------------------------------------------------------------------- 1 | package shade.benchmarks 2 | 3 | import org.openjdk.jmh.annotations.{Benchmark, Setup} 4 | import org.openjdk.jmh.infra.Blackhole 5 | 6 | import scala.concurrent.duration._ 7 | 8 | class ExistingKeyOps extends MemcachedBase { 9 | 10 | val key: String = "existing" 11 | val duration: FiniteDuration = 1.day 12 | 13 | @Setup 14 | def prepare(): Unit = { 15 | memcached.set(key, 10L, duration) 16 | } 17 | 18 | @Benchmark 19 | def get(bh: Blackhole): Unit = bh.consume { 20 | memcached.awaitGet[String](key) 21 | } 22 | 23 | @Benchmark 24 | def set(bh: Blackhole): Unit = bh.consume{ 25 | memcached.awaitSet(key, 100L, duration) 26 | } 27 | 28 | @Benchmark 29 | def delete(bh: Blackhole): Unit = bh.consume { 30 | memcached.awaitDelete(key) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /benchmarking/src/main/scala/shade/benchmarks/MemcachedBase.scala: -------------------------------------------------------------------------------- 1 | package shade.benchmarks 2 | 3 | 4 | import java.util.concurrent.TimeUnit 5 | 6 | import org.openjdk.jmh.annotations._ 7 | import shade.memcached.{Configuration, FailureMode, Memcached, Protocol} 8 | 9 | import scala.concurrent.ExecutionContext.global 10 | import scala.concurrent.duration._ 11 | 12 | /** 13 | * Base class for benchmarks that need an instance of [[Memcached]] 14 | */ 15 | @State(Scope.Thread) 16 | @BenchmarkMode(Array(Mode.AverageTime)) 17 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 18 | abstract class MemcachedBase { 19 | 20 | val memcached: Memcached = { 21 | val defaultConfig = Configuration( 22 | addresses = "127.0.0.1:11211", 23 | authentication = None, 24 | keysPrefix = Some("my-benchmarks"), 25 | protocol = Protocol.Binary, 26 | failureMode = FailureMode.Retry, 27 | operationTimeout = 15.seconds 28 | ) 29 | Memcached(defaultConfig)(global) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /release-notes/1.8.md: -------------------------------------------------------------------------------- 1 | # Version 1.8.1 - December 19, 2016 2 | 3 | - Updated Monix to version 2.1.2 4 | 5 | # Version 1.8.0 - December 17, 2016 6 | 7 | - [Issue #42](https://github.com/monix/shade/pull/42) 8 | Added possibility to select Locator during Configuration creation 9 | - [Issue #40](https://github.com/monix/shade/pull/40) 10 | Added benchmarks 11 | - [Issue #43](https://github.com/monix/shade/pull/43) 12 | Migrating to Monix as a dependency 13 | - [Issue #44](https://github.com/monix/shade/pull/44) 14 | Add Scala 2.12 support 15 | - [Issue #45](https://github.com/monix/shade/pull/45) 16 | Introduce `CancelableFuture` as the return type for async operations 17 | - [Issue #46](https://github.com/monix/shade/issues/46) 18 | Change the organization of the project to @monix on GitHub 19 | and to `io.monix` for published artifacts on Maven Central 20 | - Upgrade dependencies 21 | - Add copyright headers on all files 22 | - Add CONTRIBUTING guidelines, along with the Typelevel Code of Conduct 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2009 Dustin Sallings 2 | Copyright (c) 2009-2011 Couchbase, Inc. 3 | Copyright (c) 2012-2017 Alexandru Nedelcu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /release-notes/1.7.md: -------------------------------------------------------------------------------- 1 | # Version 1.7.0 - December 18, 2015 2 | 3 | - clean up dependencies list 4 | - update to latest versions 5 | - remove junk 6 | 7 | ## Version 1.7.1 - December 18, 2015 8 | 9 | Integrated several contributions: 10 | 11 | - changed the `Memcached.apply` to take ExecutionContext implicitly (contribution) 12 | - added condition to not clobber existing logger 13 | - added CustomInputStream (contribution) 14 | - add custom config options for customizing SpyMemcached's internal queues 15 | 16 | ## Version 1.7.2 - January 30, 2016 17 | 18 | - add Array[Byte] codec 19 | - allow enabling low-level client optimisation (collapse multiple sequential get ops) 20 | - send expiration time as 0L when expiration duration is infinity (comply with Memcached expectations) 21 | 22 | ## Version 1.7.3 - May 11, 2016 23 | 24 | - bump SBT version to 0.13.11 25 | - bump Scala version to 2.11.8 26 | - bump Monifu version to 1.2 27 | - add support for Double and Float by default 28 | - add implicitNotFound message on Codec 29 | - enable configuration of hash algorithm 30 | 31 | ## Version 1.7.4 - Jun 11, 2016 32 | 33 | - Issue #38: Add server-side increment / decrement support 34 | - Issue #39: Complete failed status translations with tryComplete 35 | - Increment Spymemcached dependency version to 2.12.1 -------------------------------------------------------------------------------- /project/publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | def exec(cmd) 4 | abort("Error encountered, aborting") unless system(cmd) 5 | end 6 | 7 | puts "CI=#{ENV['CI']}" 8 | puts "TRAVIS_BRANCH=#{ENV['TRAVIS_BRANCH']}" 9 | puts "TRAVIS_PULL_REQUEST=#{ENV['TRAVIS_PULL_REQUEST']}" 10 | puts "PUBLISH=#{ENV['PUBLISH']}" 11 | puts "SONATYPE_USER=xxxx" if ENV['SONATYPE_USER'] 12 | puts "SONATYPE_PASS=xxxx" if ENV['SONATYPE_PASS'] 13 | puts "PGP_PASS=xxxx" if ENV['PGP_PASS'] 14 | puts 15 | 16 | unless ENV['CI'] == 'true' 17 | abort("ERROR: Not running on top of Travis, aborting!") 18 | end 19 | 20 | unless ENV['PUBLISH'] == 'true' 21 | puts "Publish is disabled" 22 | exit 23 | end 24 | 25 | branch = ENV['TRAVIS_BRANCH'] 26 | version = nil 27 | 28 | unless branch =~ /^v(\d+\.\d+\.\d+)$/ || 29 | (branch == "snapshot" && ENV['TRAVIS_PULL_REQUEST'] == 'false') 30 | 31 | puts "Only deploying docs on the `publish` branch, or for version tags " + 32 | "and not for pull requests or other branches, exiting!" 33 | exit 0 34 | else 35 | version = $1 36 | puts "Version branch detected: #{version}" if version 37 | end 38 | 39 | # Forcing a change to the root directory, if not there already 40 | Dir.chdir(File.absolute_path(File.join(File.dirname(__FILE__), ".."))) 41 | 42 | # Go, go, go 43 | exec("sbt release") 44 | -------------------------------------------------------------------------------- /src/main/scala/shade/CacheException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade 13 | 14 | /** 15 | * Super-class for errors thrown when specific cache-store related 16 | * errors occur. 17 | */ 18 | class CacheException(val msg: String) extends RuntimeException(msg) 19 | 20 | /** 21 | * Thrown in case a cache store related operation times out. 22 | */ 23 | class TimeoutException(val key: String) extends CacheException(key) 24 | 25 | /** 26 | * Thrown in case a cache store related operation is cancelled 27 | * (like due to closed / broken connections) 28 | */ 29 | class CancelledException(val key: String) extends CacheException(key) 30 | 31 | /** 32 | * Gets thrown in case the implementation is wrong and 33 | * mishandled a status. Should never get thrown and 34 | * if it does, then it's a bug. 35 | */ 36 | class UnhandledStatusException(msg: String) extends CacheException(msg) 37 | 38 | /** 39 | * Gets thrown in case a key is not found in the cache store on #apply(). 40 | */ 41 | class KeyNotInCacheException(val key: String) extends CacheException(key) 42 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/internals/Status.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached.internals 13 | 14 | import net.spy.memcached.ops.OperationStatus 15 | import scala.language.existentials 16 | 17 | sealed trait Status extends Product with Serializable 18 | case object TimedOutStatus extends Status 19 | case object CancelledStatus extends Status 20 | case object CASExistsStatus extends Status 21 | case object CASNotFoundStatus extends Status 22 | case object CASSuccessStatus extends Status 23 | case object CASObserveErrorInArgs extends Status 24 | case object CASObserveModified extends Status 25 | case object CASObserveTimeout extends Status 26 | case object IllegalCompleteStatus extends Status 27 | 28 | object UnhandledStatus { 29 | 30 | /** 31 | * Builds a serialisable UnhandledStatus from a given [[OperationStatus]] from SpyMemcached 32 | */ 33 | def fromSpyMemcachedStatus(spyStatus: OperationStatus): UnhandledStatus = UnhandledStatus(spyStatus.getClass, spyStatus.getMessage) 34 | } 35 | 36 | final case class UnhandledStatus(statusClass: Class[_], message: String) extends Status -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: required 3 | dist: trusty 4 | group: edge 5 | 6 | matrix: 7 | include: 8 | - jdk: oraclejdk8 9 | scala: 2.10.6 10 | env: COMMAND=ci PUBLISH= 11 | - jdk: oraclejdk8 12 | scala: 2.11.11 13 | env: COMMAND=ci PUBLISH= 14 | - jdk: oraclejdk8 15 | scala: 2.12.3 16 | env: COMMAND=ci PUBLISH=true 17 | 18 | env: 19 | global: 20 | - secure: GRdfKNrJn/zqjaDWE+16HCfuCSf/wsDpLHocxrOSDiW6QCy73a+MYCujfB989YndQkrmGVkzdmAyKhcfTyYW/Sqjh/sJc2OOc6p+4CeMOGRcLV73wTwi9PjsrzzN0260HnICq3X+3ZUiLdkWoJPLfD6Mflj9iRjJBQIOtV0LzeU= 21 | - secure: SPSIblLKFVns7pVY1x3SEs4/16htY5HUzRC51uWXeESE7Nwi3SvBY8LE2BqHygQl/9wKKOdOKoCIBoftukWupIi/r1rT2nVFHremO23Y36hcffN+PFXtW6NIohwIoX34O6G7VGuS2b71IZQHqwr88bY4aHeU4jI3MtU3nXhbEMI= 22 | - secure: YVx2BSSsqF7LdYTwinf6o8nqJiYL9FeFAm1HDLxt+ltuMAEbFprOEDA763FANZoUino0uYtOBQ9jWqgMsoo+DvWFrBk4eExC9jGRk7Y/aWw6lx+TCbISGYztkhREQf73JKjbejoxLXf9h9gfo3MpPdrQhzMd2zVKOgSNf8FddZA= 23 | 24 | script: 25 | - sbt -J-Xmx6144m ++$TRAVIS_SCALA_VERSION $COMMAND 26 | after_success: 27 | - ./project/publish 28 | 29 | services: 30 | - docker 31 | 32 | before_install: 33 | - sudo service memcached stop 34 | - docker pull memcached 35 | - docker run -d -p 127.0.0.1:11211:11211 memcached memcached 36 | 37 | cache: 38 | directories: 39 | - $HOME/.sbt/0.13 40 | - $HOME/.sbt/boot/scala* 41 | - $HOME/.sbt/cache 42 | - $HOME/.sbt/launchers 43 | - $HOME/.ivy2 44 | before_cache: 45 | - du -h -d 1 $HOME/.ivy2/ 46 | - du -h -d 2 $HOME/.sbt/ 47 | - find $HOME/.sbt -name "*.lock" -type f -delete 48 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -type f -delete 49 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/GenericCodecObjectInputStream.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached 13 | 14 | import java.io.{ InputStream, ObjectInputStream, ObjectStreamClass } 15 | 16 | import scala.reflect.ClassTag 17 | import scala.util.control.NonFatal 18 | 19 | /** 20 | * Object input stream which tries the thread local class loader. 21 | * 22 | * Thread Local class loader is used by SBT to avoid polluting system class loader when 23 | * running different tasks. 24 | * 25 | * This allows deserialization of classes from sub-projects during something like 26 | * Play's test/run modes. 27 | */ 28 | class GenericCodecObjectInputStream(classTag: ClassTag[_], in: InputStream) 29 | extends ObjectInputStream(in) { 30 | 31 | private def classTagClassLoader = 32 | classTag.runtimeClass.getClassLoader 33 | private def threadLocalClassLoader = 34 | Thread.currentThread().getContextClassLoader 35 | 36 | override protected def resolveClass(desc: ObjectStreamClass): Class[_] = { 37 | try classTagClassLoader.loadClass(desc.getName) catch { 38 | case NonFatal(_) => 39 | try super.resolveClass(desc) catch { 40 | case NonFatal(_) => 41 | threadLocalClassLoader.loadClass(desc.getName) 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/internals/PartialResult.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached.internals 13 | 14 | import monix.execution.atomic.AtomicAny 15 | 16 | import scala.concurrent.{ Future, Promise } 17 | import scala.util.{ Success, Try } 18 | 19 | sealed trait PartialResult[+T] 20 | case class FinishedResult[T](result: Try[Result[T]]) extends PartialResult[T] 21 | case class FutureResult[T](result: Future[Result[T]]) extends PartialResult[T] 22 | case object NoResultAvailable extends PartialResult[Nothing] 23 | 24 | final class MutablePartialResult[T] { 25 | def tryComplete(result: Try[Result[T]]): Boolean = 26 | _result.compareAndSet(NoResultAvailable, FinishedResult(result)) 27 | 28 | def tryCompleteWith(result: Future[Result[T]]): Boolean = 29 | _result.compareAndSet(NoResultAvailable, FutureResult(result)) 30 | 31 | def completePromise(key: String, promise: Promise[Result[T]]): Unit = { 32 | _result.get match { 33 | case FinishedResult(result) => 34 | promise.tryComplete(result) 35 | case FutureResult(result) => 36 | promise.tryCompleteWith(result) 37 | case NoResultAvailable => 38 | promise.tryComplete(Success(FailedResult(key, IllegalCompleteStatus))) 39 | } 40 | } 41 | 42 | private[this] val _result = 43 | AtomicAny(NoResultAvailable: PartialResult[T]) 44 | } 45 | -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/Offer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | import java.util.UUID 15 | 16 | case class Offer( 17 | id: Option[Int], 18 | name: String, 19 | 20 | advertiser: Advertiser, 21 | offerType: String, 22 | 23 | liveDeal: LiveDealInfo, 24 | creative: OfferCreative, 25 | 26 | deliveryMechanisms: Seq[String], 27 | 28 | servedURL: String, 29 | realURL: Option[String], 30 | 31 | // is_active and is_valid 32 | isRunning: Boolean, 33 | isDynamic: Boolean, 34 | isGlobal: Boolean, 35 | 36 | countries: Seq[String]) { 37 | 38 | def uniqueToken = { 39 | val token = id.toString + "-" + advertiser.serviceID + 40 | "-" + liveDeal.uid.getOrElse("static") 41 | UUID.nameUUIDFromBytes(token.getBytes).toString 42 | } 43 | 44 | def isExpired = { 45 | if (liveDeal.expires.isEmpty) 46 | false 47 | else if (liveDeal.expires.get > System.currentTimeMillis() / 1000) 48 | false 49 | else 50 | true 51 | } 52 | } 53 | 54 | case class LiveDealInfo( 55 | uid: Option[String], 56 | expires: Option[Int], 57 | refreshToken: Option[Int], 58 | searchKeyword: Option[String]) 59 | 60 | case class OfferCreative( 61 | title: String, 62 | description: String, 63 | merchantName: Option[String], 64 | merchantPhone: Option[String], 65 | htmlDescription: Option[String]) 66 | -------------------------------------------------------------------------------- /src/test/scala/shade/tests/CodecsSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.tests 13 | 14 | import org.scalacheck.Arbitrary 15 | import org.scalatest.FunSuite 16 | import org.scalatest.prop.GeneratorDrivenPropertyChecks 17 | import shade.memcached.{ Codec, MemcachedCodecs } 18 | 19 | class CodecsSuite extends FunSuite with MemcachedCodecs with GeneratorDrivenPropertyChecks { 20 | 21 | /** 22 | * Properties-based checking for a codec of type A 23 | */ 24 | private def serdesCheck[A: Arbitrary](codec: Codec[A]): Unit = { 25 | forAll { n: A => 26 | val serialised = codec.serialize(n) 27 | val deserialised = codec.deserialize(serialised) 28 | assert(deserialised == n) 29 | } 30 | } 31 | 32 | test("IntBinaryCodec") { 33 | serdesCheck(IntBinaryCodec) 34 | } 35 | 36 | test("DoubleBinaryCodec") { 37 | serdesCheck(DoubleBinaryCodec) 38 | } 39 | 40 | test("FloatBinaryCodec") { 41 | serdesCheck(FloatBinaryCodec) 42 | } 43 | 44 | test("LongBinaryCodec") { 45 | serdesCheck(LongBinaryCodec) 46 | } 47 | 48 | test("BooleanBinaryCodec") { 49 | serdesCheck(BooleanBinaryCodec) 50 | } 51 | 52 | test("CharBinaryCodec") { 53 | serdesCheck(CharBinaryCodec) 54 | } 55 | 56 | test("ShortBinaryCodec") { 57 | serdesCheck(ShortBinaryCodec) 58 | } 59 | 60 | test("StringBinaryCodec") { 61 | serdesCheck(StringBinaryCodec) 62 | } 63 | 64 | test("ArrayByteBinaryCodec") { 65 | serdesCheck(ArrayByteBinaryCodec) 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/test/scala/shade/tests/MemcachedTestHelpers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.tests 13 | 14 | import shade.memcached._ 15 | 16 | import scala.concurrent.ExecutionContext.Implicits._ 17 | import scala.concurrent.duration._ 18 | 19 | trait MemcachedTestHelpers extends MemcachedCodecs { 20 | val defaultConfig = Configuration( 21 | addresses = "127.0.0.1:11211", 22 | authentication = None, 23 | keysPrefix = Some("my-tests"), 24 | protocol = Protocol.Binary, 25 | failureMode = FailureMode.Retry, 26 | operationTimeout = 15.seconds 27 | ) 28 | 29 | def createCacheObject(prefix: String, opTimeout: Option[FiniteDuration] = None, failureMode: Option[FailureMode.Value] = None, isFake: Boolean = false): Memcached = { 30 | val config = defaultConfig.copy( 31 | keysPrefix = defaultConfig.keysPrefix.map(s => s + "-" + prefix), 32 | failureMode = failureMode.getOrElse(defaultConfig.failureMode), 33 | operationTimeout = opTimeout.getOrElse(defaultConfig.operationTimeout) 34 | ) 35 | 36 | Memcached(config)(global) 37 | } 38 | 39 | def withFakeMemcached[T](cb: Memcached => T): T = { 40 | val cache = new FakeMemcached(global) 41 | try { 42 | cb(cache) 43 | } finally { 44 | cache.close() 45 | } 46 | } 47 | 48 | def withCache[T](prefix: String, failureMode: Option[FailureMode.Value] = None, opTimeout: Option[FiniteDuration] = None)(cb: Memcached => T): T = { 49 | val cache = createCacheObject(prefix = prefix, failureMode = failureMode, opTimeout = opTimeout) 50 | 51 | try { 52 | cb(cache) 53 | } finally { 54 | cache.close() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/shade/memcached/internals/Slf4jLogger.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached.internals; 13 | 14 | import net.spy.memcached.compat.log.AbstractLogger; 15 | import net.spy.memcached.compat.log.Level; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | public class Slf4jLogger extends AbstractLogger { 20 | private Logger logger; 21 | 22 | public Slf4jLogger(String name) { 23 | super(name); 24 | logger = LoggerFactory.getLogger(name); 25 | } 26 | 27 | 28 | @Override 29 | public boolean isDebugEnabled() { 30 | return logger.isDebugEnabled(); 31 | } 32 | 33 | @Override 34 | public boolean isInfoEnabled() { 35 | return logger.isInfoEnabled(); 36 | } 37 | 38 | @Override 39 | public boolean isTraceEnabled() { 40 | return logger.isTraceEnabled(); 41 | } 42 | 43 | @Override public void log(Level level, Object message, Throwable e) { 44 | switch (level) { 45 | case DEBUG: 46 | logger.debug("{}", message, e); 47 | break; 48 | case INFO: 49 | logger.info("{}", message, e); 50 | break; 51 | case WARN: 52 | logger.warn("{}", message, e); 53 | break; 54 | case ERROR: 55 | logger.error("{}", message, e); 56 | break; 57 | case FATAL: 58 | logger.error("{}", message, e); 59 | break; 60 | default: 61 | logger.error("Unhandled log level: {}", level); 62 | logger.error("{}", message, e); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Shade 2 | 3 | The Shade project welcomes contributions from anybody wishing to participate. 4 | All code or documentation that is provided must be licensed with the same 5 | license that Shade is licensed with (Apache 2.0, see LICENSE.txt). 6 | 7 | ## Code of Conduct 8 | 9 | People are expected to follow the [Typelevel Code of Conduct](http://typelevel.org/conduct.html) 10 | when discussing Shade on the GitHub page, Gitter channel, or other venues. 11 | 12 | We hope that our community will be respectful, helpful, and kind. If you find 13 | yourself embroiled in a situation that becomes heated, or that fails to live up 14 | to our expectations, you should disengage and contact one of the project maintainers 15 | in private. We hope to avoid letting minor aggressions and misunderstandings 16 | escalate into larger problems. 17 | 18 | ## General Workflow 19 | 20 | 1. Make sure you can license your work under the MIT license 21 | 22 | 2. Before starting to work, make sure there is a ticket in the issue 23 | or create one first. It can help accelerate the acceptance process 24 | if the change is agreed upon 25 | 26 | 3. If you don't have write access to the repository, you should do 27 | your work in a local branch of your own fork and then submit a pull 28 | request. If you do have write access to the repository, never work 29 | directly on master. 30 | 31 | 4. When the work is completed, submit a Pull Request. 32 | 33 | 5. Anyone can comment on a pull request and you are expected to 34 | answer questions or to incorporate feedback. 35 | 36 | 6. It is not allowed to force push to the branch on which the pull 37 | request is based. 38 | 39 | ## General Guidelines 40 | 41 | 1. It is recommended that the work is accompanied by unit tests. 42 | 43 | 2. The commit messages should be clear and short one lines, if more 44 | details are needed, specify a body. 45 | 46 | 3. New source files should be accompanied by the copyright header. 47 | 48 | 4. Follow the structure of the code in this repository and the 49 | indentation rules used. 50 | 51 | 5. Your first commit request should be accompanied with a change to 52 | the AUTHORS file, adding yourself to the authors list. 53 | 54 | ## License 55 | 56 | All code must be licensed under the MIT license and all files 57 | must include the following copyright header: 58 | 59 | ``` 60 | Copyright (c) 2012-$today.year by its authors. Some rights reserved. 61 | See the project homepage at: https://github.com/monix/shade 62 | 63 | Licensed under the MIT License (the "License"); you may not use this 64 | file except in compliance with the License. You may obtain a copy 65 | of the License at: 66 | 67 | https://github.com/monix/shade/blob/master/LICENSE.txt 68 | ``` -------------------------------------------------------------------------------- /src/test/scala/shade/memcached/internals/MutablePartialResultSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached.internals 13 | 14 | import org.scalatest.FunSuite 15 | import org.scalatest.concurrent.{ IntegrationPatience, ScalaFutures } 16 | 17 | import scala.concurrent.{ Future, Promise } 18 | import scala.util.Success 19 | 20 | class MutablePartialResultSuite 21 | extends FunSuite 22 | with ScalaFutures 23 | with IntegrationPatience { 24 | 25 | def assertCompletePromise(toCheck: MutablePartialResult[Boolean], expected: Boolean): Unit = { 26 | val promise = Promise[Result[Boolean]]() 27 | toCheck.completePromise("key1", promise) 28 | whenReady(promise.future) { 29 | case SuccessfulResult(_, r) => assert(r == expected) 30 | case _ => fail("not successful") 31 | } 32 | } 33 | 34 | test("initial state") { 35 | val pResult = new MutablePartialResult[Boolean] 36 | val promise = Promise[Result[Boolean]]() 37 | pResult.completePromise("key1", promise) 38 | whenReady(promise.future) { r => 39 | assert(r.isInstanceOf[FailedResult]) 40 | } 41 | } 42 | 43 | test("#tryComplete on a fresh MutablePartialResult") { 44 | val pResult = new MutablePartialResult[Boolean] 45 | pResult.tryComplete(Success(SuccessfulResult("key1", false))) 46 | assertCompletePromise(toCheck = pResult, expected = false) 47 | } 48 | 49 | test("#tryComplete on a MutablePartialResult that has already been completed") { 50 | val pResult = new MutablePartialResult[Boolean] 51 | assert(pResult.tryComplete(Success(SuccessfulResult("key1", false)))) 52 | assert(!pResult.tryComplete(Success(SuccessfulResult("key1", true)))) 53 | assertCompletePromise(toCheck = pResult, expected = false) 54 | } 55 | 56 | test("#tryCompleteWith on a fresh MutablePartialResult") { 57 | val pResult = new MutablePartialResult[Boolean] 58 | pResult.tryCompleteWith(Future.successful(SuccessfulResult("key1", false))) 59 | assertCompletePromise(toCheck = pResult, expected = false) 60 | } 61 | 62 | test("#tryCompleteWith on a MutablePartialResult that has already been completed") { 63 | val pResult = new MutablePartialResult[Boolean] 64 | assert(pResult.tryCompleteWith(Future.successful(SuccessfulResult("key1", false)))) 65 | assert(!pResult.tryCompleteWith(Future.successful(SuccessfulResult("key1", true)))) 66 | assertCompletePromise(toCheck = pResult, expected = false) 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/ContentPiece.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.testModels 13 | 14 | sealed trait ContentPiece extends Serializable { 15 | import ContentPiece._ 16 | 17 | def id: Option[Int] 18 | def url: String 19 | def creator: String 20 | def source: ContentSource 21 | def tags: Vector[String] 22 | 23 | val contentType = this match { 24 | case o: Image => "image" 25 | case o: Title => "title" 26 | case o: Article => "article" 27 | } 28 | 29 | def getTitle = this match { 30 | case o: Image => o.title 31 | case o: Title => Some(o.title) 32 | case o: Article => Some(o.title) 33 | } 34 | 35 | def getPhoto = this match { 36 | case o: Image => Some(o.photo) 37 | case _ => None 38 | } 39 | 40 | def getShortExcerpt = this match { 41 | case o: Article => Some(o.shortExcerpt) 42 | case _ => None 43 | } 44 | 45 | def getExcerptHtml = this match { 46 | case o: Article => Some(o.excerptHtml) 47 | case _ => None 48 | } 49 | 50 | def getContentHtml = this match { 51 | case o: Article => Some(o.contentHtml) 52 | case _ => None 53 | } 54 | 55 | def withId(id: Int) = this match { 56 | case o: Article => o.copy(id = Some(id)) 57 | case o: Image => o.copy(id = Some(id)) 58 | case o: Title => o.copy(id = Some(id)) 59 | } 60 | 61 | def withTags(tags: Vector[String]) = this match { 62 | case o: Article => o.copy(tags = tags) 63 | case o: Title => o.copy(tags = tags) 64 | case o: Image => o.copy(tags = tags) 65 | } 66 | } 67 | 68 | object ContentPiece { 69 | @SerialVersionUID(23904298512054925L) 70 | case class Image( 71 | id: Option[Int], 72 | url: String, 73 | creator: String, 74 | photo: String, 75 | title: Option[String], 76 | source: ContentSource, 77 | tags: Vector[String]) extends ContentPiece 78 | 79 | @SerialVersionUID(9785234918758324L) 80 | case class Title( 81 | id: Option[Int], 82 | url: String, 83 | creator: String, 84 | title: String, 85 | source: ContentSource, 86 | tags: Vector[String]) extends ContentPiece 87 | 88 | @SerialVersionUID(9348538729520853L) 89 | case class Article( 90 | id: Option[Int], 91 | url: String, 92 | creator: String, 93 | title: String, 94 | shortExcerpt: String, 95 | excerptHtml: String, 96 | contentHtml: Option[String], 97 | source: ContentSource, 98 | tags: Vector[String]) extends ContentPiece 99 | } 100 | 101 | sealed trait ContentSource extends Serializable { 102 | def value: String 103 | } 104 | 105 | object ContentSource { 106 | def apply(value: String): ContentSource = value match { 107 | case "tumblr" => Tumblr 108 | case "wordpress" => WordPress 109 | } 110 | 111 | case object Tumblr extends ContentSource { 112 | val value = "tumblr" 113 | } 114 | 115 | case object WordPress extends ContentSource { 116 | val value = "wordpress" 117 | } 118 | } -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/FakeMemcached.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached 13 | 14 | import monix.execution.CancelableFuture 15 | import shade.UnhandledStatusException 16 | import shade.inmemory.InMemoryCache 17 | 18 | import scala.concurrent.duration.Duration 19 | import scala.concurrent.{ ExecutionContext, Future } 20 | 21 | class FakeMemcached(context: ExecutionContext) extends Memcached { 22 | private[this] implicit val ec = context 23 | 24 | def add[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): CancelableFuture[Boolean] = 25 | value match { 26 | case null => 27 | CancelableFuture.successful(false) 28 | case _ => 29 | CancelableFuture.successful(cache.add(key, codec.serialize(value).toSeq, exp)) 30 | } 31 | 32 | def set[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): CancelableFuture[Unit] = 33 | value match { 34 | case null => 35 | CancelableFuture.successful(()) 36 | case _ => 37 | CancelableFuture.successful(cache.set(key, codec.serialize(value).toSeq, exp)) 38 | } 39 | 40 | def delete(key: String): CancelableFuture[Boolean] = 41 | CancelableFuture.successful(cache.delete(key)) 42 | 43 | def get[T](key: String)(implicit codec: Codec[T]): Future[Option[T]] = 44 | Future.successful(cache.get[Seq[Byte]](key)).map(_.map(x => codec.deserialize(x.toArray))) 45 | 46 | def compareAndSet[T](key: String, expecting: Option[T], newValue: T, exp: Duration)(implicit codec: Codec[T]): Future[Boolean] = 47 | Future.successful(cache.compareAndSet(key, expecting.map(x => codec.serialize(x).toSeq), codec.serialize(newValue).toSeq, exp)) 48 | 49 | def transformAndGet[T](key: String, exp: Duration)(cb: (Option[T]) => T)(implicit codec: Codec[T]): Future[T] = 50 | Future.successful(cache.transformAndGet[Seq[Byte]](key: String, exp) { current => 51 | val cValue = current.map(x => codec.deserialize(x.toArray)) 52 | val update = cb(cValue) 53 | codec.serialize(update).toSeq 54 | }) map { update => 55 | codec.deserialize(update.toArray) 56 | } 57 | 58 | def getAndTransform[T](key: String, exp: Duration)(cb: (Option[T]) => T)(implicit codec: Codec[T]): Future[Option[T]] = 59 | Future.successful(cache.getAndTransform[Seq[Byte]](key: String, exp) { current => 60 | val cValue = current.map(x => codec.deserialize(x.toArray)) 61 | val update = cb(cValue) 62 | codec.serialize(update).toSeq 63 | }) map { update => 64 | update.map(x => codec.deserialize(x.toArray)) 65 | } 66 | 67 | def increment(key: String, by: Long, default: Option[Long], exp: Duration): Future[Long] = { 68 | def toBigInt(bytes: Seq[Byte]): BigInt = BigInt(new String(bytes.toArray)) 69 | Future.successful(cache.transformAndGet[Seq[Byte]](key, exp) { 70 | case Some(current) => (toBigInt(current) + by).toString.getBytes 71 | case None if default.isDefined => default.get.toString.getBytes 72 | case None => throw new UnhandledStatusException(s"For key $key - CASNotFoundStatus") 73 | }).map(toBigInt).map(_.toLong) 74 | } 75 | 76 | def decrement(key: String, by: Long, default: Option[Long], exp: Duration): Future[Long] = { 77 | def toBigInt(bytes: Seq[Byte]): BigInt = BigInt(new String(bytes.toArray)) 78 | Future.successful(cache.transformAndGet[Seq[Byte]](key, exp) { 79 | case Some(current) => (toBigInt(current) - by).max(0).toString.getBytes 80 | case None if default.isDefined => default.get.toString.getBytes 81 | case None => throw new UnhandledStatusException(s"For key $key - CASNotFoundStatus") 82 | }).map(toBigInt).map(_.toLong) 83 | } 84 | 85 | def close(): Unit = { 86 | cache.close() 87 | } 88 | 89 | private[this] val cache = InMemoryCache(context) 90 | } 91 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/Configuration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached 13 | 14 | import net.spy.memcached.ConnectionFactoryBuilder.Locator 15 | import net.spy.memcached.ops.OperationQueueFactory 16 | import net.spy.memcached.{ DefaultConnectionFactory, HashAlgorithm } 17 | 18 | import scala.concurrent.duration._ 19 | 20 | /** 21 | * Represents the Memcached connection configuration. 22 | * 23 | * @param addresses the list of server addresses, separated by space, 24 | * e.g. `"192.168.1.3:11211 192.168.1.4:11211"` 25 | * @param authentication the authentication credentials (if None, then no authentication is performed) 26 | * 27 | * @param keysPrefix is the prefix to be added to used keys when storing/retrieving values, 28 | * useful for having the same Memcached instances used by several 29 | * applications to prevent them from stepping over each other. 30 | * 31 | * @param protocol can be either `Text` or `Binary` 32 | * 33 | * @param failureMode specifies failure mode for SpyMemcached when connections drop: 34 | * - in Retry mode a connection is retried until it recovers. 35 | * - in Cancel mode all operations are cancelled 36 | * - in Redistribute mode, the client tries to redistribute operations to other nodes 37 | * 38 | * @param operationTimeout is the default operation timeout; When the limit is reached, the 39 | * Future responses finish with Failure(TimeoutException) 40 | * 41 | * @param timeoutThreshold is the maximum number of timeouts for a connection that will be tolerated before 42 | * the connection is considered dead and will not be retried. Once this threshold is breached, 43 | * the client will consider the connection to be lost and attempt to establish a new one. 44 | * If None, the default Spymemcached implementation is used (998) 45 | * 46 | * @param shouldOptimize If true, optimization will collapse multiple sequential get ops. 47 | * 48 | * @param opQueueFactory can be used to customize the operations queue, 49 | * i.e. the queue of operations waiting to be processed by SpyMemcached. 50 | * If `None`, the default SpyMemcached implementation (a bounded ArrayBlockingQueue) is used. 51 | * 52 | * @param readQueueFactory can be used to customize the read queue, 53 | * i.e. the queue of Memcached responses waiting to be processed by SpyMemcached. 54 | * If `None`, the default SpyMemcached implementation (an unbounded LinkedBlockingQueue) is used. 55 | * 56 | * @param writeQueueFactory can be used to customize the write queue, 57 | * i.e. the queue of operations waiting to be sent to Memcached by SpyMemcached. 58 | * If `None`, the default SpyMemcached implementation (an unbounded LinkedBlockingQueue) is used. 59 | * 60 | * @param hashAlgorithm the method for hashing a cache key for server selection 61 | * 62 | * @param locator locator selection, by default ARRAY_MOD 63 | */ 64 | case class Configuration( 65 | addresses: String, 66 | authentication: Option[AuthConfiguration] = None, 67 | keysPrefix: Option[String] = None, 68 | protocol: Protocol.Value = Protocol.Binary, 69 | failureMode: FailureMode.Value = FailureMode.Retry, 70 | operationTimeout: FiniteDuration = 1.second, 71 | timeoutThreshold: Option[Int] = None, 72 | shouldOptimize: Boolean = false, 73 | opQueueFactory: Option[OperationQueueFactory] = None, 74 | writeQueueFactory: Option[OperationQueueFactory] = None, 75 | readQueueFactory: Option[OperationQueueFactory] = None, 76 | hashAlgorithm: HashAlgorithm = DefaultConnectionFactory.DEFAULT_HASH, 77 | locator: Locator = Locator.ARRAY_MOD) 78 | 79 | object Protocol extends Enumeration { 80 | type Type = Value 81 | val Binary, Text = Value 82 | } 83 | 84 | object FailureMode extends Enumeration { 85 | val Retry, Cancel, Redistribute = Value 86 | } 87 | 88 | case class AuthConfiguration( 89 | username: String, 90 | password: String) 91 | -------------------------------------------------------------------------------- /src/test/scala/shade/testModels/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade 13 | 14 | import scala.collection.mutable.ArrayBuffer 15 | 16 | package object testModels { 17 | val bigInstance = Impression( 18 | "96298b14-1e13-a162-662b-969bd3b41ca4", 19 | Session( 20 | "c5c94985-1d91-3a8b-b36b-6791efefc38c", 21 | "dummy-user-sa9d08ahusid", 22 | "android.web", 23 | UserInfo( 24 | "71.89.145.102", 25 | "71.89.145.102", 26 | "71.89.145.102", 27 | "Mozilla/5.0 (Linux; U; Android 0.5; en-us) AppleWebKit/522 (KHTML, like Gecko) Safari/419.3", 28 | Some( 29 | GeoIPLocation( 30 | "us", 31 | Some("Ashburn"), 32 | Some("United States"), 33 | Some(39.0437.toFloat), 34 | Some(-77.4875.toFloat), 35 | Some(703), 36 | None, 37 | Some("VA"), 38 | Some(511) 39 | ) 40 | ) 41 | ), 42 | 43 | Some("aac636be-e42b-01d6-449b-6a0c2e5e7b09"), 44 | Some("something-65"), 45 | Some("71.89.145.102"), 46 | None, 47 | None, 48 | Some("us") 49 | ), 50 | List( 51 | Offer( 52 | Some(3352251), 53 | "Some Dummy Offer Title", 54 | Advertiser( 55 | Some(137), 56 | Some("something"), 57 | "something" 58 | ), 59 | "cpa", 60 | LiveDealInfo( 61 | Some(""), 62 | None, 63 | None, 64 | None 65 | ), 66 | 67 | OfferCreative( 68 | "So Many Dresses!", 69 | "Daily Deals For Moms, Babies and Kids. Up to 90% OFF! Shop Now!", 70 | Some("Something.com"), 71 | Some(""), 72 | None 73 | ), 74 | 75 | ArrayBuffer("viewnow"), 76 | 77 | "http://something.com/track?clickID=242323&pubID=982345&something=219&subID=something", 78 | None, 79 | true, 80 | false, 81 | false, 82 | List("us") 83 | ) 84 | ), 85 | 112, 86 | true, 87 | Some("light-fullscreen") 88 | ) 89 | 90 | val bigInstance2 = Impression( 91 | "96298b14-1e13-a162-662b-969bd3b41ca4", 92 | Session( 93 | "c5c94985-1d91-3a8b-b36b-6791efefc38c", 94 | "dummy-user-sa9d08ahusid", 95 | "android.web", 96 | UserInfo( 97 | "71.89.145.102", 98 | "71.89.145.102", 99 | "71.89.145.102", 100 | "Mozilla/5.0 (Linux; U; Android 0.5; en-us) AppleWebKit/522 (KHTML, like Gecko) Safari/419.3", 101 | Some( 102 | GeoIPLocation( 103 | "us", 104 | Some("Ashburn"), 105 | Some("United States"), 106 | Some(39.0437.toFloat), 107 | Some(-77.4875.toFloat), 108 | Some(703), 109 | None, 110 | Some("VA"), 111 | Some(511) 112 | ) 113 | ) 114 | ), 115 | 116 | Some("aac636be-e42b-01d6-449b-6a0c2e5e7b09"), 117 | Some("something-65"), 118 | Some("71.89.145.102"), 119 | None, 120 | None, 121 | Some("us") 122 | ), 123 | List.empty, 124 | 112, 125 | true, 126 | Some("light-fullscreen") 127 | ) 128 | 129 | val contentSeq = Vector( 130 | ContentPiece.Article( 131 | id = Some(1), 132 | url = "http://google.com/", 133 | creator = "alex", 134 | title = "Hello world!", 135 | shortExcerpt = "Hello world", 136 | excerptHtml = "Hello world", 137 | contentHtml = Some("

Sample

Hello world"), 138 | source = ContentSource.WordPress, 139 | tags = Vector("auto", "hello") 140 | ), 141 | ContentPiece.Image( 142 | id = Some(2), 143 | url = "http://google.com/", 144 | creator = "alex", 145 | photo = "http://google.com/image.png", 146 | title = Some("Image"), 147 | source = ContentSource.Tumblr, 148 | tags = Vector("google", "image") 149 | ), 150 | ContentPiece.Title( 151 | id = Some(3), 152 | url = "http://google.com/3", 153 | title = "Hello Title", 154 | creator = "alex", 155 | source = ContentSource.Tumblr, 156 | tags = Vector("title", "hello") 157 | ) 158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/Codec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached 13 | 14 | import java.io._ 15 | 16 | import scala.annotation.implicitNotFound 17 | import scala.language.implicitConversions 18 | import scala.reflect.ClassTag 19 | import scala.util.control.NonFatal 20 | 21 | /** 22 | * Represents a type class that needs to be implemented 23 | * for serialization/deserialization to work. 24 | */ 25 | @implicitNotFound("Could not find any Codec implementation for type ${T}. Please provide one or import shade.memcached.MemcachedCodecs._") 26 | trait Codec[T] { 27 | def serialize(value: T): Array[Byte] 28 | def deserialize(data: Array[Byte]): T 29 | } 30 | 31 | object Codec extends BaseCodecs 32 | 33 | trait BaseCodecs { 34 | implicit object IntBinaryCodec extends Codec[Int] { 35 | def serialize(value: Int): Array[Byte] = 36 | Array( 37 | (value >>> 24).asInstanceOf[Byte], 38 | (value >>> 16).asInstanceOf[Byte], 39 | (value >>> 8).asInstanceOf[Byte], 40 | value.asInstanceOf[Byte] 41 | ) 42 | 43 | def deserialize(data: Array[Byte]): Int = 44 | (data(0).asInstanceOf[Int] & 255) << 24 | 45 | (data(1).asInstanceOf[Int] & 255) << 16 | 46 | (data(2).asInstanceOf[Int] & 255) << 8 | 47 | data(3).asInstanceOf[Int] & 255 48 | } 49 | 50 | implicit object DoubleBinaryCodec extends Codec[Double] { 51 | import java.lang.{ Double => JvmDouble } 52 | def serialize(value: Double): Array[Byte] = { 53 | val l = JvmDouble.doubleToLongBits(value) 54 | LongBinaryCodec.serialize(l) 55 | } 56 | 57 | def deserialize(data: Array[Byte]): Double = { 58 | val l = LongBinaryCodec.deserialize(data) 59 | JvmDouble.longBitsToDouble(l) 60 | } 61 | } 62 | 63 | implicit object FloatBinaryCodec extends Codec[Float] { 64 | import java.lang.{ Float => JvmFloat } 65 | def serialize(value: Float): Array[Byte] = { 66 | val i = JvmFloat.floatToIntBits(value) 67 | IntBinaryCodec.serialize(i) 68 | } 69 | 70 | def deserialize(data: Array[Byte]): Float = { 71 | val i = IntBinaryCodec.deserialize(data) 72 | JvmFloat.intBitsToFloat(i) 73 | } 74 | } 75 | 76 | implicit object LongBinaryCodec extends Codec[Long] { 77 | def serialize(value: Long): Array[Byte] = 78 | Array( 79 | (value >>> 56).asInstanceOf[Byte], 80 | (value >>> 48).asInstanceOf[Byte], 81 | (value >>> 40).asInstanceOf[Byte], 82 | (value >>> 32).asInstanceOf[Byte], 83 | (value >>> 24).asInstanceOf[Byte], 84 | (value >>> 16).asInstanceOf[Byte], 85 | (value >>> 8).asInstanceOf[Byte], 86 | value.asInstanceOf[Byte] 87 | ) 88 | 89 | def deserialize(data: Array[Byte]): Long = 90 | (data(0).asInstanceOf[Long] & 255) << 56 | 91 | (data(1).asInstanceOf[Long] & 255) << 48 | 92 | (data(2).asInstanceOf[Long] & 255) << 40 | 93 | (data(3).asInstanceOf[Long] & 255) << 32 | 94 | (data(4).asInstanceOf[Long] & 255) << 24 | 95 | (data(5).asInstanceOf[Long] & 255) << 16 | 96 | (data(6).asInstanceOf[Long] & 255) << 8 | 97 | data(7).asInstanceOf[Long] & 255 98 | } 99 | 100 | implicit object BooleanBinaryCodec extends Codec[Boolean] { 101 | def serialize(value: Boolean): Array[Byte] = 102 | Array((if (value) 1 else 0).asInstanceOf[Byte]) 103 | 104 | def deserialize(data: Array[Byte]): Boolean = 105 | data.isDefinedAt(0) && data(0) == 1 106 | } 107 | 108 | implicit object CharBinaryCodec extends Codec[Char] { 109 | def serialize(value: Char): Array[Byte] = Array( 110 | (value >>> 8).asInstanceOf[Byte], 111 | value.asInstanceOf[Byte] 112 | ) 113 | 114 | def deserialize(data: Array[Byte]): Char = 115 | ((data(0).asInstanceOf[Int] & 255) << 8 | 116 | data(1).asInstanceOf[Int] & 255) 117 | .asInstanceOf[Char] 118 | } 119 | 120 | implicit object ShortBinaryCodec extends Codec[Short] { 121 | def serialize(value: Short): Array[Byte] = Array( 122 | (value >>> 8).asInstanceOf[Byte], 123 | value.asInstanceOf[Byte] 124 | ) 125 | 126 | def deserialize(data: Array[Byte]): Short = 127 | ((data(0).asInstanceOf[Short] & 255) << 8 | 128 | data(1).asInstanceOf[Short] & 255) 129 | .asInstanceOf[Short] 130 | } 131 | 132 | implicit object StringBinaryCodec extends Codec[String] { 133 | def serialize(value: String): Array[Byte] = value.getBytes("UTF-8") 134 | def deserialize(data: Array[Byte]): String = new String(data, "UTF-8") 135 | } 136 | 137 | implicit object ArrayByteBinaryCodec extends Codec[Array[Byte]] { 138 | def serialize(value: Array[Byte]): Array[Byte] = value 139 | def deserialize(data: Array[Byte]): Array[Byte] = data 140 | } 141 | } 142 | 143 | trait GenericCodec { 144 | 145 | private[this] class GenericCodec[S <: Serializable](classTag: ClassTag[S]) extends Codec[S] { 146 | 147 | def using[T <: Closeable, R](obj: T)(f: T => R): R = 148 | try 149 | f(obj) 150 | finally 151 | try obj.close() catch { 152 | case NonFatal(_) => // does nothing 153 | } 154 | 155 | def serialize(value: S): Array[Byte] = 156 | using (new ByteArrayOutputStream()) { buf => 157 | using (new ObjectOutputStream(buf)) { out => 158 | out.writeObject(value) 159 | out.close() 160 | buf.toByteArray 161 | } 162 | } 163 | 164 | def deserialize(data: Array[Byte]): S = 165 | using (new ByteArrayInputStream(data)) { buf => 166 | val in = new GenericCodecObjectInputStream(classTag, buf) 167 | using (in) { inp => 168 | inp.readObject().asInstanceOf[S] 169 | } 170 | } 171 | } 172 | 173 | implicit def AnyRefBinaryCodec[S <: Serializable](implicit ev: ClassTag[S]): Codec[S] = 174 | new GenericCodec[S](ev) 175 | 176 | } 177 | 178 | trait MemcachedCodecs extends BaseCodecs with GenericCodec 179 | 180 | object MemcachedCodecs extends MemcachedCodecs 181 | 182 | -------------------------------------------------------------------------------- /src/test/scala/shade/tests/InMemoryCacheVer2Suite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.tests 13 | 14 | import org.scalatest.FunSuite 15 | import shade.inmemory.InMemoryCache 16 | 17 | import scala.concurrent.ExecutionContext.Implicits.global 18 | import scala.concurrent.duration._ 19 | import scala.concurrent.{ Await, Future } 20 | 21 | class InMemoryCacheVer2Suite extends FunSuite { 22 | test("get(), set()") { 23 | withInstance { cache => 24 | assert(cache.get[String]("hello") === None) 25 | 26 | cache.set("hello", "world") 27 | assert(cache.get[String]("hello") === Some("world")) 28 | } 29 | } 30 | 31 | test("add()") { 32 | withInstance { cache => 33 | assert(cache.get[String]("hello") === None) 34 | 35 | assert(cache.add("hello", "world"), "value should be added successfully") 36 | assert(cache.get[String]("hello") === Some("world")) 37 | 38 | assert(!cache.add("hello", "world version 2"), "value already exists") 39 | assert(cache.get[String]("hello") === Some("world")) 40 | 41 | cache.set("hello", "world version 2") 42 | assert(cache.get[String]("hello") === Some("world version 2")) 43 | } 44 | } 45 | 46 | test("getOrElse()") { 47 | withInstance { cache => 48 | assert(cache.getOrElse("hello", "default") === "default") 49 | cache.set("hello", "world") 50 | assert(cache.getOrElse("hello", "world") === "world") 51 | } 52 | } 53 | 54 | test("delete()") { 55 | withInstance { cache => 56 | assert(cache.get[String]("hello") === None) 57 | cache.set("hello", "world") 58 | assert(cache.get[String]("hello") === Some("world")) 59 | 60 | assert(cache.delete("hello"), "item should be deleted") 61 | assert(cache.get[String]("hello") === None) 62 | assert(!cache.delete("hello"), "item should not be there anymore") 63 | } 64 | } 65 | 66 | test("cachedFuture()") { 67 | withInstance { cache => 68 | assert(cache.get[String]("hello") === None) 69 | 70 | def future() = cache.cachedFuture("hello", 1.minute) { 71 | Future { 72 | Thread.sleep(1000) 73 | "world" 74 | } 75 | } 76 | 77 | for (idx <- 0 until 10000) 78 | assert(Await.result(future(), 4.seconds) === "world") 79 | } 80 | } 81 | 82 | test("compareAndSet()") { 83 | withInstance { cache => 84 | assert(cache.compareAndSet("hello", None, "world"), "first CAS should succeed") 85 | assert(cache.compareAndSet("hello", Some("world"), "world updated"), "second CAS should succeed") 86 | assert(cache.get[String]("hello") === Some("world updated")) 87 | assert(!cache.compareAndSet("hello", Some("bollocks"), "world"), "third CAS should fail") 88 | } 89 | } 90 | 91 | test("transformAndGet() (with expiry)") { 92 | withInstance { cache => 93 | def incr() = cache.transformAndGet[Int]("number", 1.second) { 94 | case Some(nr) => nr + 1 95 | case None => 0 96 | } 97 | 98 | for (idx <- 0 until 100) 99 | assert(incr() === idx) 100 | 101 | Thread.sleep(1000) 102 | assert(incr() === 0) 103 | } 104 | } 105 | 106 | test("getAndTransform() (with expiry)") { 107 | withInstance { cache => 108 | def incr() = cache.getAndTransform[Int]("number", 1.second) { 109 | case Some(nr) => nr + 1 110 | case None => 1 111 | } 112 | 113 | for (idx <- 0 until 100) 114 | if (idx == 0) 115 | assert(incr() === None) 116 | else 117 | assert(incr() === Some(idx)) 118 | 119 | Thread.sleep(1000) 120 | assert(incr() === None) 121 | } 122 | } 123 | 124 | test("add() expiration") { 125 | withInstance { cache => 126 | assert(cache.add("hello", "world", 1.second), "add() should work") 127 | assert(cache.get[String]("hello") === Some("world")) 128 | 129 | Thread.sleep(1000) 130 | assert(cache.get[String]("hello") === None) 131 | } 132 | } 133 | 134 | test("set() expiration") { 135 | withInstance { cache => 136 | cache.set("hello", "world", 1.second) 137 | assert(cache.get[String]("hello") === Some("world")) 138 | 139 | Thread.sleep(1000) 140 | assert(cache.get[String]("hello") === None) 141 | } 142 | } 143 | 144 | test("delete() expiration") { 145 | withInstance { cache => 146 | cache.set("hello", "world", 1.second) 147 | assert(cache.get[String]("hello") === Some("world")) 148 | 149 | Thread.sleep(1000) 150 | assert(!cache.delete("hello"), "delete() should return false") 151 | } 152 | } 153 | 154 | test("cachedFuture() expiration") { 155 | withInstance { cache => 156 | val result = Await.result(cache.cachedFuture("hello", 1.second) { Future("world") }, 1.second) 157 | assert(result === "world") 158 | 159 | val size = cache.realSize 160 | assert(size === 1) 161 | 162 | Thread.sleep(1000) 163 | assert(cache.get[String]("hello") === None) 164 | } 165 | } 166 | 167 | test("compareAndSet() expiration") { 168 | withInstance { cache => 169 | assert(cache.compareAndSet("hello", None, "world", 1.second), "CAS should succeed") 170 | assert(cache.get[String]("hello") === Some("world")) 171 | 172 | Thread.sleep(1000) 173 | assert(cache.get[String]("hello") === None) 174 | } 175 | } 176 | 177 | test("maintenance / scheduler") { 178 | withInstance { cache => 179 | val startTS = System.currentTimeMillis() 180 | 181 | cache.set("hello", "world", 1.second) 182 | cache.set("hello2", "world2") 183 | 184 | assert(cache.realSize === 2) 185 | 186 | val diff = Await.result(cache.maintenance, 20.seconds) 187 | val m1ts = System.currentTimeMillis() 188 | 189 | assert(diff === 1) 190 | assert(cache.realSize === 1) 191 | 192 | val timeWindow1 = math.round((m1ts - startTS) / 1000.0) 193 | assert(timeWindow1 >= 3 && timeWindow1 <= 7, "scheduler should run at no less than 3 secs and no more than 7 secs") 194 | 195 | val diff2 = Await.result(cache.maintenance, 20.seconds) 196 | val m2ts = System.currentTimeMillis() 197 | 198 | assert(diff2 === 0) 199 | assert(cache.realSize === 1) 200 | 201 | val timeWindow2 = math.round((m2ts - m1ts) / 1000.0) 202 | assert(timeWindow2 >= 3 && timeWindow2 <= 7, "scheduler should run at no less than 3 secs and no more than 7 secs") 203 | } 204 | } 205 | 206 | def withInstance[T](cb: InMemoryCache => T) = { 207 | val instance = InMemoryCache(global) 208 | try cb(instance) finally { 209 | instance.close() 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/Memcached.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached 13 | 14 | import monix.execution.CancelableFuture 15 | 16 | import scala.concurrent.duration.Duration 17 | import scala.concurrent.{ Await, ExecutionContext, Future } 18 | 19 | trait Memcached extends java.io.Closeable { 20 | /** 21 | * Adds a value for a given key, if the key doesn't already exist in the cache store. 22 | * 23 | * If the key already exists in the cache, the future returned result will be false and the 24 | * current value will not be overridden. If the key isn't there already, the value 25 | * will be set and the future returned result will be true. 26 | * 27 | * The expiry time can be Duration.Inf (infinite duration). 28 | * 29 | * @return either true, in case the value was set, or false otherwise 30 | */ 31 | def add[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): CancelableFuture[Boolean] 32 | 33 | def awaitAdd[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): Boolean = 34 | Await.result(add(key, value, exp), Duration.Inf) 35 | 36 | /** 37 | * Sets a (key, value) in the cache store. 38 | * 39 | * The expiry time can be Duration.Inf (infinite duration). 40 | */ 41 | def set[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): CancelableFuture[Unit] 42 | 43 | def awaitSet[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]) { 44 | Await.result(set(key, value, exp), Duration.Inf) 45 | } 46 | 47 | /** 48 | * Deletes a key from the cache store. 49 | * 50 | * @return true if a key was deleted or false if there was nothing there to delete 51 | */ 52 | def delete(key: String): CancelableFuture[Boolean] 53 | 54 | def awaitDelete(key: String): Boolean = 55 | Await.result(delete(key), Duration.Inf) 56 | 57 | /** 58 | * Fetches a value from the cache store. 59 | * 60 | * @return Some(value) in case the key is available, or None otherwise (doesn't throw exception on key missing) 61 | */ 62 | def get[T](key: String)(implicit codec: Codec[T]): Future[Option[T]] 63 | 64 | def awaitGet[T](key: String)(implicit codec: Codec[T]): Option[T] = 65 | Await.result(get[T](key), Duration.Inf) 66 | 67 | /** 68 | * Compare and set. 69 | * 70 | * @param expecting should be None in case the key is not expected, or Some(value) otherwise 71 | * @param exp can be Duration.Inf (infinite) for not setting an expiration 72 | * @return either true (in case the compare-and-set succeeded) or false otherwise 73 | */ 74 | def compareAndSet[T](key: String, expecting: Option[T], newValue: T, exp: Duration)(implicit codec: Codec[T]): Future[Boolean] 75 | 76 | /** 77 | * Transforms the given key and returns the new value. 78 | * 79 | * The cb callback receives the current value 80 | * (None in case the key is missing or Some(value) otherwise) 81 | * and should return the new value to store. 82 | * 83 | * The method retries until the compare-and-set operation succeeds, so 84 | * the callback should have no side-effects. 85 | * 86 | * This function can be used for atomic increments and stuff like that. 87 | * 88 | * @return the new value 89 | */ 90 | def transformAndGet[T](key: String, exp: Duration)(cb: Option[T] => T)(implicit codec: Codec[T]): Future[T] 91 | 92 | /** 93 | * Transforms the given key and returns the old value as an Option[T] 94 | * (None in case the key wasn't in the cache or Some(value) otherwise). 95 | * 96 | * The cb callback receives the current value 97 | * (None in case the key is missing or Some(value) otherwise) 98 | * and should return the new value to store. 99 | * 100 | * The method retries until the compare-and-set operation succeeds, so 101 | * the callback should have no side-effects. 102 | * 103 | * This function can be used for atomic increments and stuff like that. 104 | * 105 | * @return the old value 106 | */ 107 | def getAndTransform[T](key: String, exp: Duration)(cb: Option[T] => T)(implicit codec: Codec[T]): Future[Option[T]] 108 | 109 | /** 110 | * Atomically increments the given key by a non-negative integer amount 111 | * and returns the new value. 112 | * 113 | * The value is stored as the ASCII decimal representation of a 64-bit 114 | * unsigned integer. 115 | * 116 | * If the key does not exist and a default is provided, sets the value of the 117 | * key to the provided default and expiry time. 118 | * 119 | * If the key does not exist and no default is provided, or if the key exists 120 | * with a value that does not conform to the expected representation, the 121 | * operation will fail. 122 | * 123 | * If the operation succeeds, it returns the new value of the key. 124 | * 125 | * Note that the default value is always treated as None when using the text 126 | * protocol. 127 | * 128 | * The expiry time can be Duration.Inf (infinite duration). 129 | */ 130 | def increment(key: String, by: Long, default: Option[Long], exp: Duration): Future[Long] 131 | 132 | def awaitIncrement(key: String, by: Long, default: Option[Long], exp: Duration): Long = 133 | Await.result(increment(key, by, default, exp), Duration.Inf) 134 | 135 | /** 136 | * Atomically decrements the given key by a non-negative integer amount 137 | * and returns the new value. 138 | * 139 | * The value is stored as the ASCII decimal representation of a 64-bit 140 | * unsigned integer. 141 | * 142 | * If the key does not exist and a default is provided, sets the value of the 143 | * key to the provided default and expiry time. 144 | * 145 | * If the key does not exist and no default is provided, or if the key exists 146 | * with a value that does not conform to the expected representation, the 147 | * operation will fail. 148 | * 149 | * If the operation succeeds, it returns the new value of the key. 150 | * 151 | * Note that the default value is always treated as None when using the text 152 | * protocol. 153 | * 154 | * The expiry time can be Duration.Inf (infinite duration). 155 | */ 156 | def decrement(key: String, by: Long, default: Option[Long], exp: Duration): Future[Long] 157 | 158 | def awaitDecrement(key: String, by: Long, default: Option[Long], exp: Duration): Long = 159 | Await.result(decrement(key, by, default, exp), Duration.Inf) 160 | 161 | /** 162 | * Shuts down the cache instance, performs any additional cleanups necessary. 163 | */ 164 | def close(): Unit 165 | } 166 | 167 | object Memcached { 168 | /** 169 | * Builds a [[Memcached]] instance. Needs a [[Configuration]]. 170 | */ 171 | def apply(config: Configuration)(implicit ec: ExecutionContext): Memcached = 172 | new MemcachedImpl(config, ec) 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shade - Memcached Client for Scala 2 | 3 | [![Build Status](https://travis-ci.org/monix/shade.svg?branch=master)](https://travis-ci.org/monix/shade) 4 | [![Join the chat at https://gitter.im/monix/shade](https://badges.gitter.im/monix/shade.svg)](https://gitter.im/monix/shade?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | ## Overview 7 | 8 | Shade is a Memcached client based on the de-facto Java library 9 | [SpyMemcached](https://code.google.com/p/spymemcached/). 10 | 11 | The interface exposed is very Scala-ish, as you have a choice between 12 | making asynchronous calls, with results wrapped as Scala 13 | [Futures](http://docs.scala-lang.org/overviews/core/futures.html), 14 | or blocking calls. The performance is stellar as it benefits from the 15 | [optimizations that went into SpyMemcached](https://web.archive.org/web/20140914202520/https://code.google.com/p/spymemcached/wiki/Optimizations) 16 | over the years. Shade also fixes some problems with SpyMemcached's 17 | architecture, choices that made sense in the context of Java, but 18 | don't make so much sense in the context of Scala (TODO: add details). 19 | 20 | The client is production quality. 21 | Supported for Scala versions: 2.10, 2.11 and 2.12. 22 | 23 | ## Release Notes 24 | 25 | - [Version 1.10.x](release-notes/1.10.md) 26 | - [Version 1.9.x](release-notes/1.9.md) 27 | - [Version 1.8.x](release-notes/1.8.md) 28 | - [Version 1.7.x](release-notes/1.7.md) 29 | - [Version 1.6.0](release-notes/1.6.0.md) 30 | 31 | ## Maintainers 32 | 33 | These are the people maintaining this project that you can annoy: 34 | 35 | - Alex: @alexandru 36 | - Lloyd: @lloydmeta 37 | 38 | ## Usage From SBT 39 | 40 | ```scala 41 | dependencies += "io.monix" %% "shade" % "1.10.0" 42 | ``` 43 | 44 | ### Initializing the Memcached Client 45 | 46 | To initialize a Memcached client, you need a configuration object. 47 | Checkout the 48 | [Configuration](src/main/scala/shade/memcached/Configuration.scala) 49 | case class. 50 | 51 | ```scala 52 | import shade.memcached._ 53 | import scala.concurrent.ExecutionContext.Implicits.global 54 | 55 | val memcached = 56 | Memcached(Configuration("127.0.0.1:11211")) 57 | ``` 58 | 59 | As you can see, you also need an 60 | [ExecutionContext](http://www.scala-lang.org/api/current/#scala.concurrent.ExecutionContext) 61 | passed explicitly. As an implementation detail, the execution context represents the 62 | thread-pool in which requests get processed. 63 | 64 | ### Simple non-blocking requests 65 | 66 | Useful imports: 67 | 68 | ```scala 69 | import concurrent.duration._ // for specifying timeouts 70 | import concurrent.Future 71 | ``` 72 | 73 | Setting a key: 74 | 75 | ```scala 76 | val op: Future[Unit] = memcached.set("username", "Alex", 1.minute) 77 | ``` 78 | 79 | Adding a key that will only set it if the key is missing (returns true 80 | if the key was added, or false if the key was already there): 81 | 82 | ```scala 83 | val op: Future[Boolean] = memcached.add("username", "Alex", 1.minute) 84 | ``` 85 | 86 | Deleting a key (returns true if a key was deleted, or false if the key 87 | was missing): 88 | 89 | ```scala 90 | val op: Future[Boolean] = memcached.delete("username") 91 | ``` 92 | 93 | Fetching a key: 94 | 95 | ```scala 96 | val result: Future[Option[String]] = memcached.get[String]("username") 97 | ``` 98 | 99 | As you can see, for fetching a key the `get()` method needs an 100 | explicit type parameter, otherwise it doesn't know how to deserialize 101 | it. More on this below. 102 | 103 | ### Blocking requests 104 | 105 | Sometimes working with Futures is painful for quick hacks, therefore 106 | `add()`, `set()`, `delete()` and `get()` have blocking versions in the 107 | form of `awaitXXX()`: 108 | 109 | ```scala 110 | memcached.awaitGet("username") match { 111 | case Some(username) => println(s"Hello, $username") 112 | case None => 113 | memcached.awaitSet("username", "Alex", 1.minute) 114 | } 115 | ``` 116 | 117 | ### Compare-and-set 118 | 119 | Sometimes you want to have some sort of synchronization for modifying 120 | values safely, like incrementing a counter. Memcached supports 121 | [Compare-And-Swap](http://en.wikipedia.org/wiki/Compare-and-swap) 122 | atomic operations and so does this client. 123 | 124 | ```scala 125 | val op: Future[Boolean] = 126 | memcached.compareAndSet("username", Some("Alex"), "Amalia", 1.minute) 127 | ``` 128 | 129 | This will return either true or false if the operation was a success 130 | or not. But working with `compareAndSet` is too low level, so the 131 | client also provides these helpers: 132 | 133 | ```scala 134 | def incrementCounter: Future[Int] = 135 | memcached.transformAndGet[Int]("counter", 1.minute) { 136 | case Some(existing) => existing + 1 137 | case None => 1 138 | } 139 | ``` 140 | 141 | The above returns the new, incremented value. In case you want the old 142 | value to be returned, do this: 143 | 144 | ```scala 145 | def incrementCounter: Future[Option[Int]] = 146 | memcached.getAndTransform[Int]("counter", 1.minute) { 147 | case Some(existing) => existing + 1 148 | case None => 1 149 | } 150 | ``` 151 | 152 | ### Serializing/Deserializing 153 | 154 | Storing values in Memcached and retrieving values involves serializing 155 | and deserializing those values into bytes. Methods such as `get()`, 156 | `set()`, `add()` take an implicit parameter of type `Codec[T]` which 157 | is a type-class that specifies how to serialize and deserialize values 158 | of type `T`. 159 | 160 | By default, Shade provides default implementations of `Codec[T]` for 161 | primitives, such as Strings and numbers. Checkout 162 | [Codec.scala](src/main/scala/shade/memcached/Codec.scala) to see those 163 | defaults. 164 | 165 | For more complex types, a default implementation based on Java's 166 | [ObjectOutputStream](http://docs.oracle.com/javase/7/docs/api/java/io/ObjectOutputStream.html) 167 | and 168 | [ObjectInputStream](http://docs.oracle.com/javase/7/docs/api/java/io/ObjectInputStream.html) 169 | exist (also in Codec.scala). 170 | 171 | However, because serializing/deserializing values like this is 172 | problematic (you can end up with lots of errors related to the 173 | ClassLoader used), this codec is available as part of the 174 | `MemcachedCodecs` trait (also in 175 | [Codec.scala](src/main/scala/shade/memcached/Codec.scala)) and it 176 | either needs to be imported or mixed-in. 177 | 178 | The import works like so: 179 | 180 | ```scala 181 | import shade.memcached.MemcachedCodecs._ 182 | ``` 183 | 184 | But this can land you in trouble because of the ClassLoader. For 185 | example in a Play 2.x application, in development mode the code is 186 | recompiled when changes happen and the whole environment gets 187 | restarted. If you do a plain import, you'll get `ClassCastException` 188 | or other weird errors. You can solve this by mixing-in 189 | `MemcachedCodecs` in whatever trait, class or object you want to do 190 | requests, as in: 191 | 192 | ```scala 193 | case class User(id: Int, name: String, age: Int) 194 | 195 | trait HelloController extends Controller with MemcachedCodecs { 196 | def memcached: Memcached // to be injected 197 | 198 | // a Play 2.2 standard controller action 199 | def userInfo(id: Int) = Action.async { 200 | for (user <- memcached.get[User]("user-" + id)) yield 201 | Ok(views.showUserDetails(user)) 202 | } 203 | 204 | // ... 205 | } 206 | ``` 207 | 208 | Or, in case you want to optimize serialization/deserialization, you 209 | can always implement your own `Codec[T]`, like: 210 | 211 | ```scala 212 | // hackish example 213 | implicit object UserCodec extends Codec[User] { 214 | def serialize(user: User): Array[Byte] = 215 | s"${user.id}|${user.name}|${user.age}".getBytes("utf-8") 216 | 217 | def deserialize(data: Array[Byte]): User = { 218 | val str = new String(data, "utf-8") 219 | val Array(id, name, age) = str.split("|") 220 | User(id.toInt, name, age.toInt) 221 | } 222 | } 223 | ``` 224 | -------------------------------------------------------------------------------- /src/main/scala/shade/inmemory/InMemoryCache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.inmemory 13 | 14 | import monix.execution.Scheduler 15 | import monix.execution.atomic.AtomicAny 16 | 17 | import scala.annotation.tailrec 18 | import scala.concurrent.duration._ 19 | import scala.concurrent.{ ExecutionContext, Future, Promise } 20 | import scala.util.Try 21 | 22 | trait InMemoryCache extends java.io.Closeable { 23 | def get[T](key: String): Option[T] 24 | def getOrElse[T](key: String, default: => T): T 25 | def add[T](key: String, value: T, expiry: Duration = Duration.Inf): Boolean 26 | def set[T](key: String, value: T, expiry: Duration = Duration.Inf): Unit 27 | def delete(key: String): Boolean 28 | def cachedFuture[T](key: String, expiry: Duration = Duration.Inf)(cb: => Future[T]): Future[T] 29 | 30 | def compareAndSet[T](key: String, expected: Option[T], update: T, expiry: Duration = Duration.Inf): Boolean 31 | def transformAndGet[T](key: String, expiry: Duration = Duration.Inf)(cb: Option[T] => T): T 32 | def getAndTransform[T](key: String, expiry: Duration = Duration.Inf)(cb: Option[T] => T): Option[T] 33 | 34 | def size: Int 35 | 36 | def realSize: Int 37 | 38 | /** 39 | * Future that completes when a maintenance window has run, 40 | * giving the number of items that were removed. 41 | * @return 42 | */ 43 | def maintenance: Future[Int] 44 | 45 | def close(): Unit 46 | } 47 | 48 | object InMemoryCache { 49 | def apply(ec: ExecutionContext): InMemoryCache = 50 | new InMemoryCacheImpl()(ec) 51 | } 52 | 53 | private[inmemory] final class InMemoryCacheImpl(implicit ec: ExecutionContext) extends InMemoryCache { 54 | private[this] val scheduler = Scheduler(ec) 55 | 56 | def get[T](key: String): Option[T] = { 57 | val currentState = stateRef.get 58 | 59 | currentState.values.get(key) match { 60 | case Some(value) if value.expiresAt > System.currentTimeMillis() => 61 | Some(value.value.asInstanceOf[T]) 62 | case _ => 63 | None 64 | } 65 | } 66 | 67 | def getOrElse[T](key: String, default: => T): T = 68 | get[T](key) match { 69 | case Some(value) => value 70 | case None => default 71 | } 72 | 73 | @tailrec 74 | def add[T](key: String, value: T, expiry: Duration = Duration.Inf): Boolean = { 75 | val ts = getExpiryTS(expiry) 76 | val currentTS = System.currentTimeMillis() 77 | val currentState = stateRef.get 78 | 79 | val itemExists = currentState.values.get(key) match { 80 | case Some(item) if item.expiresAt > currentTS => 81 | true 82 | case _ => 83 | false 84 | } 85 | 86 | if (itemExists || ts <= currentTS) 87 | false 88 | else { 89 | val firstExpiry = if (currentState.firstExpiry == 0) ts else math.min(currentState.firstExpiry, ts) 90 | val values = currentState.values.updated(key, CacheValue(value, ts)) 91 | val newState = currentState.copy(values = values, firstExpiry = firstExpiry) 92 | 93 | if (stateRef.compareAndSet(currentState, newState)) 94 | true 95 | else 96 | add(key, value, expiry) 97 | } 98 | } 99 | 100 | def set[T](key: String, value: T, expiry: Duration = Duration.Inf): Unit = { 101 | val ts = getExpiryTS(expiry) 102 | 103 | stateRef.transform { current => 104 | val firstExpiry = if (current.firstExpiry == 0) ts else math.min(current.firstExpiry, ts) 105 | val values = current.values.updated(key, CacheValue(value, ts)) 106 | current.copy(values = values, firstExpiry = firstExpiry) 107 | } 108 | } 109 | 110 | @tailrec 111 | def delete(key: String): Boolean = { 112 | val currentState = stateRef.get 113 | 114 | currentState.values.get(key) match { 115 | case Some(value) => 116 | val values = currentState.values - key 117 | val newState = currentState.copy(values = values) 118 | 119 | if (stateRef.compareAndSet(currentState, newState)) 120 | value.expiresAt > System.currentTimeMillis() 121 | else 122 | delete(key) 123 | case None => 124 | false 125 | } 126 | } 127 | 128 | @tailrec 129 | def cachedFuture[T](key: String, expiry: Duration = Duration.Inf)(cb: => Future[T]): Future[T] = { 130 | val currentState = stateRef.get 131 | 132 | val currentValue = currentState.values.get(key) match { 133 | case Some(value) if value.expiresAt > System.currentTimeMillis() => 134 | Some(value.value.asInstanceOf[Future[T]]) 135 | case _ => 136 | None 137 | } 138 | 139 | currentValue match { 140 | case Some(value) => 141 | value 142 | case None => 143 | val ts = getExpiryTS(expiry) 144 | val promise = Promise[T]() 145 | val future = promise.future 146 | 147 | val values = currentState.values.updated(key, CacheValue(future, ts)) 148 | val firstExpiry = if (currentState.firstExpiry == 0) ts else math.min(currentState.firstExpiry, ts) 149 | val newState = currentState.copy(values, firstExpiry) 150 | 151 | if (stateRef.compareAndSet(currentState, newState)) { 152 | promise.completeWith(cb) 153 | future 154 | } else 155 | cachedFuture(key, expiry)(cb) 156 | } 157 | } 158 | 159 | def compareAndSet[T](key: String, expected: Option[T], update: T, expiry: Duration): Boolean = { 160 | val current = stateRef.get 161 | val ts = getExpiryTS(expiry) 162 | 163 | val currentValue = current.values.get(key) match { 164 | case Some(value) if value.expiresAt > System.currentTimeMillis() => 165 | Some(value.value.asInstanceOf[T]) 166 | case _ => 167 | None 168 | } 169 | 170 | if (currentValue != expected) 171 | false 172 | else { 173 | val values = current.values.updated(key, CacheValue(update, ts)) 174 | val firstExpiry = if (current.firstExpiry == 0) ts else math.min(current.firstExpiry, ts) 175 | val newState = current.copy(values, firstExpiry) 176 | stateRef.compareAndSet(current, newState) 177 | } 178 | } 179 | 180 | def transformAndGet[T](key: String, expiry: Duration)(cb: (Option[T]) => T): T = 181 | stateRef.transformAndExtract { current => 182 | val ts = getExpiryTS(expiry) 183 | 184 | val currentValue = current.values.get(key) match { 185 | case Some(value) if value.expiresAt > System.currentTimeMillis() => 186 | Some(value.value.asInstanceOf[T]) 187 | case _ => 188 | None 189 | } 190 | 191 | val newValue = cb(currentValue) 192 | val values = current.values.updated(key, CacheValue(newValue, ts)) 193 | val firstExpiry = if (current.firstExpiry == 0) ts else math.min(current.firstExpiry, ts) 194 | (newValue, current.copy(values, firstExpiry)) 195 | } 196 | 197 | def getAndTransform[T](key: String, expiry: Duration)(cb: (Option[T]) => T): Option[T] = 198 | stateRef.transformAndExtract { current => 199 | val ts = getExpiryTS(expiry) 200 | 201 | val currentValue = current.values.get(key) match { 202 | case Some(value) if value.expiresAt > System.currentTimeMillis() => 203 | Some(value.value.asInstanceOf[T]) 204 | case _ => 205 | None 206 | } 207 | 208 | val newValue = cb(currentValue) 209 | val values = current.values.updated(key, CacheValue(newValue, ts)) 210 | val firstExpiry = if (current.firstExpiry == 0) ts else math.min(current.firstExpiry, ts) 211 | (currentValue, current.copy(values, firstExpiry)) 212 | } 213 | 214 | def clean(): Boolean = { 215 | val (promise, difference) = stateRef.transformAndExtract { currentState => 216 | val currentTS = System.currentTimeMillis() 217 | 218 | if (currentState.firstExpiry <= currentTS) { 219 | val values = currentState.values.filterNot(value => value._2.expiresAt <= currentTS) 220 | val difference = currentState.values.size - values.size 221 | 222 | val firstExpiry = values.foldLeft(0L) { (acc, elem) => 223 | if (acc == 0 || acc < elem._2.expiresAt) 224 | elem._2.expiresAt 225 | else 226 | acc 227 | } 228 | 229 | val newState = CacheState(values, firstExpiry) 230 | ((currentState.maintenancePromise, difference), newState) 231 | } else { 232 | val newState = currentState.copy(maintenancePromise = Promise()) 233 | ((currentState.maintenancePromise, 0), newState) 234 | } 235 | } 236 | 237 | promise.trySuccess(difference) 238 | } 239 | 240 | def size: Int = { 241 | val ts = System.currentTimeMillis() 242 | stateRef.get.values.count(_._2.expiresAt <= ts) 243 | } 244 | 245 | def realSize: Int = stateRef.get.values.size 246 | 247 | /** 248 | * Future that completes when a maintenance window has run, 249 | * giving the number of items that were removed. 250 | * @return 251 | */ 252 | def maintenance: Future[Int] = 253 | stateRef.get.maintenancePromise.future 254 | 255 | def close(): Unit = { 256 | Try(task.cancel()) 257 | val state = stateRef.getAndSet(CacheState()) 258 | state.maintenancePromise.trySuccess(0) 259 | } 260 | 261 | protected def getExpiryTS(expiry: Duration): Long = 262 | if (expiry.isFinite()) 263 | System.currentTimeMillis() + expiry.toMillis 264 | else 265 | System.currentTimeMillis() + 365.days.toMillis 266 | 267 | private[this] val task = 268 | scheduler.scheduleWithFixedDelay(3.seconds, 3.seconds) { 269 | clean() 270 | } 271 | 272 | private[this] case class CacheValue( 273 | value: Any, 274 | expiresAt: Long) 275 | 276 | private[this] case class CacheState( 277 | values: Map[String, CacheValue] = Map.empty, 278 | firstExpiry: Long = 0, 279 | maintenancePromise: Promise[Int] = Promise[Int]()) 280 | 281 | private[this] val stateRef = AtomicAny(CacheState()) 282 | } 283 | -------------------------------------------------------------------------------- /src/test/scala/shade/tests/FakeMemcachedSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.tests 13 | 14 | import java.io.{ ByteArrayOutputStream, ObjectOutputStream } 15 | 16 | import org.scalatest.FunSuite 17 | import shade.testModels.Impression 18 | 19 | import scala.concurrent.Await 20 | import scala.concurrent.ExecutionContext.Implicits.global 21 | import scala.concurrent.duration._ 22 | 23 | class FakeMemcachedSuite extends FunSuite with MemcachedTestHelpers { 24 | implicit val timeout = 5.second 25 | 26 | test("add") { 27 | withFakeMemcached { cache => 28 | val op1 = cache.awaitAdd("hello", Value("world"), 5.seconds) 29 | assert(op1 === true) 30 | 31 | val stored = cache.awaitGet[Value]("hello") 32 | assert(stored === Some(Value("world"))) 33 | 34 | val op2 = cache.awaitAdd("hello", Value("changed"), 5.seconds) 35 | assert(op2 === false) 36 | 37 | val changed = cache.awaitGet[Value]("hello") 38 | assert(changed === Some(Value("world"))) 39 | } 40 | } 41 | 42 | test("add-null") { 43 | withFakeMemcached { cache => 44 | val op1 = cache.awaitAdd("hello", null, 5.seconds) 45 | assert(op1 === false) 46 | 47 | val stored = cache.awaitGet[Value]("hello") 48 | assert(stored === None) 49 | } 50 | } 51 | 52 | test("get") { 53 | withFakeMemcached { cache => 54 | val value = cache.awaitGet[Value]("missing") 55 | assert(value === None) 56 | } 57 | } 58 | 59 | test("set") { 60 | withFakeMemcached { cache => 61 | assert(cache.awaitGet[Value]("hello") === None) 62 | 63 | cache.awaitSet("hello", Value("world"), 3.seconds) 64 | assert(cache.awaitGet[Value]("hello") === Some(Value("world"))) 65 | 66 | cache.awaitSet("hello", Value("changed"), 3.seconds) 67 | assert(cache.awaitGet[Value]("hello") === Some(Value("changed"))) 68 | 69 | Thread.sleep(3000) 70 | 71 | assert(cache.awaitGet[Value]("hello") === None) 72 | } 73 | } 74 | 75 | test("set-null") { 76 | withFakeMemcached { cache => 77 | val op1 = cache.awaitAdd("hello", null, 5.seconds) 78 | assert(op1 === false) 79 | 80 | val stored = cache.awaitGet[Value]("hello") 81 | assert(stored === None) 82 | } 83 | } 84 | 85 | test("delete") { 86 | withFakeMemcached { cache => 87 | cache.awaitDelete("hello") 88 | assert(cache.awaitGet[Value]("hello") === None) 89 | 90 | cache.awaitSet("hello", Value("world"), 1.minute) 91 | assert(cache.awaitGet[Value]("hello") === Some(Value("world"))) 92 | 93 | assert(cache.awaitDelete("hello") === true) 94 | assert(cache.awaitGet[Value]("hello") === None) 95 | 96 | assert(cache.awaitDelete("hello") === false) 97 | } 98 | } 99 | 100 | test("compareAndSet") { 101 | withFakeMemcached { cache => 102 | cache.awaitDelete("some-key") 103 | assert(cache.awaitGet[Value]("some-key") === None) 104 | 105 | // no can do 106 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("invalid")), Value("value1"), 15.seconds), Duration.Inf) === false) 107 | assert(cache.awaitGet[Value]("some-key") === None) 108 | 109 | // set to value1 110 | assert(Await.result(cache.compareAndSet("some-key", None, Value("value1"), 5.seconds), Duration.Inf) === true) 111 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value1"))) 112 | 113 | // no can do 114 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("invalid")), Value("value1"), 15.seconds), Duration.Inf) === false) 115 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value1"))) 116 | 117 | // set to value2, from value1 118 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("value1")), Value("value2"), 15.seconds), Duration.Inf) === true) 119 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value2"))) 120 | 121 | // no can do 122 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("invalid")), Value("value1"), 15.seconds), Duration.Inf) === false) 123 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value2"))) 124 | 125 | // set to value3, from value2 126 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("value2")), Value("value3"), 15.seconds), Duration.Inf) === true) 127 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value3"))) 128 | } 129 | } 130 | 131 | test("transformAndGet") { 132 | withFakeMemcached { cache => 133 | cache.awaitDelete("some-key") 134 | assert(cache.awaitGet[Value]("some-key") === None) 135 | 136 | def incrementValue = 137 | cache.transformAndGet[Int]("some-key", 5.seconds) { 138 | case None => 1 139 | case Some(nr) => nr + 1 140 | } 141 | 142 | assert(Await.result(incrementValue, Duration.Inf) === 1) 143 | assert(Await.result(incrementValue, Duration.Inf) === 2) 144 | assert(Await.result(incrementValue, Duration.Inf) === 3) 145 | assert(Await.result(incrementValue, Duration.Inf) === 4) 146 | assert(Await.result(incrementValue, Duration.Inf) === 5) 147 | assert(Await.result(incrementValue, Duration.Inf) === 6) 148 | } 149 | } 150 | 151 | test("getAndTransform") { 152 | withFakeMemcached { cache => 153 | cache.awaitDelete("some-key") 154 | assert(cache.awaitGet[Value]("some-key") === None) 155 | 156 | def incrementValue = Await.result( 157 | cache.getAndTransform[Int]("some-key", 5.seconds) { 158 | case None => 1 159 | case Some(nr) => nr + 1 160 | }, 161 | Duration.Inf 162 | ) 163 | 164 | assert(incrementValue === None) 165 | assert(incrementValue === Some(1)) 166 | assert(incrementValue === Some(2)) 167 | assert(incrementValue === Some(3)) 168 | assert(incrementValue === Some(4)) 169 | assert(incrementValue === Some(5)) 170 | assert(incrementValue === Some(6)) 171 | } 172 | } 173 | 174 | test("transformAndGet-concurrent") { 175 | withFakeMemcached { cache => 176 | cache.awaitDelete("some-key") 177 | assert(cache.awaitGet[Value]("some-key") === None) 178 | 179 | def incrementValue = 180 | cache.transformAndGet[Int]("some-key", 60.seconds) { 181 | case None => 1 182 | case Some(nr) => nr + 1 183 | } 184 | 185 | val seq = concurrent.Future.sequence((0 until 500).map(nr => incrementValue)) 186 | Await.result(seq, 20.seconds) 187 | 188 | assert(cache.awaitGet[Int]("some-key") === Some(500)) 189 | } 190 | } 191 | 192 | test("getAndTransform-concurrent") { 193 | withFakeMemcached { cache => 194 | cache.awaitDelete("some-key") 195 | assert(cache.awaitGet[Value]("some-key") === None) 196 | 197 | def incrementValue = 198 | cache.getAndTransform[Int]("some-key", 60.seconds) { 199 | case None => 1 200 | case Some(nr) => nr + 1 201 | } 202 | 203 | val seq = concurrent.Future.sequence((0 until 500).map(nr => incrementValue)) 204 | Await.result(seq, 20.seconds) 205 | 206 | assert(cache.awaitGet[Int]("some-key") === Some(500)) 207 | } 208 | } 209 | 210 | test("increment-decrement") { 211 | withFakeMemcached { cache => 212 | assert(cache.awaitGet[Int]("hello") === None) 213 | 214 | cache.awaitSet("hello", "123", 1.second)(StringBinaryCodec) 215 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 216 | 217 | cache.awaitIncrement("hello", 1, None, 1.second) 218 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("124")) 219 | 220 | cache.awaitDecrement("hello", 1, None, 1.second) 221 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 222 | 223 | Thread.sleep(3000) 224 | 225 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 226 | } 227 | } 228 | 229 | test("increment-decrement-delta") { 230 | withFakeMemcached { cache => 231 | assert(cache.awaitGet[Int]("hello") === None) 232 | 233 | cache.awaitSet("hello", "123", 1.second)(StringBinaryCodec) 234 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 235 | 236 | cache.awaitIncrement("hello", 5, None, 1.second) 237 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("128")) 238 | 239 | cache.awaitDecrement("hello", 5, None, 1.second) 240 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 241 | 242 | Thread.sleep(3000) 243 | 244 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 245 | } 246 | } 247 | 248 | test("increment-default") { 249 | withFakeMemcached { cache => 250 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 251 | 252 | cache.awaitIncrement("hello", 1, Some(0), 1.second) 253 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("0")) 254 | 255 | cache.awaitIncrement("hello", 1, Some(0), 1.second) 256 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("1")) 257 | 258 | Thread.sleep(3000) 259 | 260 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 261 | } 262 | } 263 | 264 | test("increment-overflow") { 265 | withFakeMemcached { cache => 266 | assert(cache.awaitIncrement("hello", 1, Some(Long.MaxValue), 1.minute) === Long.MaxValue) 267 | 268 | assert(cache.awaitIncrement("hello", 1, None, 1.minute) === Long.MinValue) 269 | 270 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("9223372036854775808")) 271 | } 272 | } 273 | 274 | test("decrement-underflow") { 275 | withFakeMemcached { cache => 276 | assert(cache.awaitDecrement("hello", 1, Some(1), 1.minute) === 1) 277 | 278 | assert(cache.awaitDecrement("hello", 1, None, 1.minute) === 0) 279 | 280 | assert(cache.awaitDecrement("hello", 1, None, 1.minute) === 0) 281 | 282 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("0")) 283 | } 284 | } 285 | 286 | test("big-instance-1") { 287 | withFakeMemcached { cache => 288 | val impression = shade.testModels.bigInstance 289 | cache.awaitSet(impression.uuid, impression, 60.seconds) 290 | assert(cache.awaitGet[Impression](impression.uuid) === Some(impression)) 291 | } 292 | } 293 | 294 | test("big-instance-1-manual") { 295 | withFakeMemcached { cache => 296 | val byteOut = new ByteArrayOutputStream() 297 | val objectOut = new ObjectOutputStream(byteOut) 298 | 299 | val impression = shade.testModels.bigInstance 300 | objectOut.writeObject(impression) 301 | val byteArray = byteOut.toByteArray 302 | 303 | cache.awaitSet(impression.uuid, byteArray, 60.seconds) 304 | 305 | val inBytes = cache.awaitGet[Array[Byte]](impression.uuid) 306 | assert(inBytes.isDefined) 307 | assert(inBytes.get.length == byteArray.length) 308 | } 309 | } 310 | 311 | test("big-instance-2") { 312 | withFakeMemcached { cache => 313 | val impression = shade.testModels.bigInstance2 314 | cache.awaitSet(impression.uuid, impression, 60.seconds) 315 | assert(cache.awaitGet[Impression](impression.uuid) === Some(impression)) 316 | } 317 | } 318 | 319 | test("big-instance-3") { 320 | withFakeMemcached { cache => 321 | val impression = shade.testModels.bigInstance 322 | val result = cache.set(impression.uuid, impression, 60.seconds) flatMap { _ => 323 | cache.get[Impression](impression.uuid) 324 | } 325 | 326 | assert(Await.result(result, Duration.Inf) === Some(impression)) 327 | } 328 | } 329 | 330 | test("cancel-strategy simple test") { 331 | withFakeMemcached { cache => 332 | Thread.sleep(100) 333 | val impression = shade.testModels.bigInstance2 334 | cache.awaitSet(impression.uuid, impression, 60.seconds) 335 | assert(cache.awaitGet[Impression](impression.uuid) === Some(impression)) 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/test/scala/shade/tests/MemcachedSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.tests 13 | 14 | import java.io.{ ByteArrayOutputStream, ObjectOutputStream } 15 | 16 | import org.scalatest.FunSuite 17 | import shade.TimeoutException 18 | import shade.memcached.FailureMode 19 | import shade.testModels.{ ContentPiece, Impression } 20 | 21 | import scala.concurrent.Await 22 | import scala.concurrent.ExecutionContext.Implicits.global 23 | import scala.concurrent.duration._ 24 | 25 | class MemcachedSuite extends FunSuite with MemcachedTestHelpers { 26 | implicit val timeout = 5.second 27 | 28 | test("add") { 29 | withCache("add") { cache => 30 | val op1 = cache.awaitAdd("hello", Value("world"), 5.seconds) 31 | assert(op1 === true) 32 | 33 | val stored = cache.awaitGet[Value]("hello") 34 | assert(stored === Some(Value("world"))) 35 | 36 | val op2 = cache.awaitAdd("hello", Value("changed"), 5.seconds) 37 | assert(op2 === false) 38 | 39 | val changed = cache.awaitGet[Value]("hello") 40 | assert(changed === Some(Value("world"))) 41 | } 42 | } 43 | 44 | test("add-null") { 45 | withCache("add-null") { cache => 46 | val op1 = cache.awaitAdd("hello", null, 5.seconds) 47 | assert(op1 === false) 48 | 49 | val stored = cache.awaitGet[Value]("hello") 50 | assert(stored === None) 51 | } 52 | } 53 | 54 | test("get") { 55 | withCache("get") { cache => 56 | val value = cache.awaitGet[Value]("missing") 57 | assert(value === None) 58 | } 59 | } 60 | 61 | test("set") { 62 | withCache("set") { cache => 63 | assert(cache.awaitGet[Value]("hello") === None) 64 | 65 | cache.awaitSet("hello", Value("world"), 1.seconds) 66 | assert(cache.awaitGet[Value]("hello") === Some(Value("world"))) 67 | 68 | cache.awaitSet("hello", Value("changed"), 1.second) 69 | assert(cache.awaitGet[Value]("hello") === Some(Value("changed"))) 70 | 71 | Thread.sleep(3000) 72 | 73 | assert(cache.awaitGet[Value]("hello") === None) 74 | } 75 | } 76 | 77 | test("set-null") { 78 | withCache("set-null") { cache => 79 | val op1 = cache.awaitAdd("hello", null, 5.seconds) 80 | assert(op1 === false) 81 | 82 | val stored = cache.awaitGet[Value]("hello") 83 | assert(stored === None) 84 | } 85 | } 86 | 87 | test("delete") { 88 | withCache("delete") { cache => 89 | cache.awaitDelete("hello") 90 | assert(cache.awaitGet[Value]("hello") === None) 91 | 92 | cache.awaitSet("hello", Value("world"), 1.minute) 93 | assert(cache.awaitGet[Value]("hello") === Some(Value("world"))) 94 | 95 | assert(cache.awaitDelete("hello") === true) 96 | assert(cache.awaitGet[Value]("hello") === None) 97 | 98 | assert(cache.awaitDelete("hello") === false) 99 | } 100 | } 101 | 102 | test("compareAndSet") { 103 | withCache("compareAndSet") { cache => 104 | cache.awaitDelete("some-key") 105 | assert(cache.awaitGet[Value]("some-key") === None) 106 | 107 | // no can do 108 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("invalid")), Value("value1"), 15.seconds), Duration.Inf) === false) 109 | assert(cache.awaitGet[Value]("some-key") === None) 110 | 111 | // set to value1 112 | assert(Await.result(cache.compareAndSet("some-key", None, Value("value1"), 5.seconds), Duration.Inf) === true) 113 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value1"))) 114 | 115 | // no can do 116 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("invalid")), Value("value1"), 15.seconds), Duration.Inf) === false) 117 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value1"))) 118 | 119 | // set to value2, from value1 120 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("value1")), Value("value2"), 15.seconds), Duration.Inf) === true) 121 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value2"))) 122 | 123 | // no can do 124 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("invalid")), Value("value1"), 15.seconds), Duration.Inf) === false) 125 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value2"))) 126 | 127 | // set to value3, from value2 128 | assert(Await.result(cache.compareAndSet("some-key", Some(Value("value2")), Value("value3"), 15.seconds), Duration.Inf) === true) 129 | assert(cache.awaitGet[Value]("some-key") === Some(Value("value3"))) 130 | } 131 | } 132 | 133 | test("transformAndGet") { 134 | withCache("transformAndGet") { cache => 135 | cache.awaitDelete("some-key") 136 | assert(cache.awaitGet[Value]("some-key") === None) 137 | 138 | def incrementValue = 139 | cache.transformAndGet[Int]("some-key", 5.seconds) { 140 | case None => 1 141 | case Some(nr) => nr + 1 142 | } 143 | 144 | assert(Await.result(incrementValue, Duration.Inf) === 1) 145 | assert(Await.result(incrementValue, Duration.Inf) === 2) 146 | assert(Await.result(incrementValue, Duration.Inf) === 3) 147 | assert(Await.result(incrementValue, Duration.Inf) === 4) 148 | assert(Await.result(incrementValue, Duration.Inf) === 5) 149 | assert(Await.result(incrementValue, Duration.Inf) === 6) 150 | } 151 | } 152 | 153 | test("getAndTransform") { 154 | withCache("getAndTransform") { cache => 155 | cache.awaitDelete("some-key") 156 | assert(cache.awaitGet[Value]("some-key") === None) 157 | 158 | def incrementValue = Await.result( 159 | cache.getAndTransform[Int]("some-key", 5.seconds) { 160 | case None => 1 161 | case Some(nr) => nr + 1 162 | }, 163 | Duration.Inf 164 | ) 165 | 166 | assert(incrementValue === None) 167 | assert(incrementValue === Some(1)) 168 | assert(incrementValue === Some(2)) 169 | assert(incrementValue === Some(3)) 170 | assert(incrementValue === Some(4)) 171 | assert(incrementValue === Some(5)) 172 | assert(incrementValue === Some(6)) 173 | } 174 | } 175 | 176 | test("transformAndGet-concurrent") { 177 | withCache("transformAndGet", opTimeout = Some(10.seconds)) { cache => 178 | cache.awaitDelete("some-key") 179 | assert(cache.awaitGet[Value]("some-key") === None) 180 | 181 | def incrementValue = 182 | cache.transformAndGet[Int]("some-key", 60.seconds) { 183 | case None => 1 184 | case Some(nr) => nr + 1 185 | } 186 | 187 | val seq = concurrent.Future.sequence((0 until 100).map(nr => incrementValue)) 188 | Await.result(seq, 20.seconds) 189 | 190 | assert(cache.awaitGet[Int]("some-key") === Some(100)) 191 | } 192 | } 193 | 194 | test("getAndTransform-concurrent") { 195 | withCache("getAndTransform", opTimeout = Some(10.seconds)) { cache => 196 | cache.awaitDelete("some-key") 197 | assert(cache.awaitGet[Value]("some-key") === None) 198 | 199 | def incrementValue = 200 | cache.getAndTransform[Int]("some-key", 60.seconds) { 201 | case None => 1 202 | case Some(nr) => nr + 1 203 | } 204 | 205 | val seq = concurrent.Future.sequence((0 until 100).map(nr => incrementValue)) 206 | Await.result(seq, 20.seconds) 207 | 208 | assert(cache.awaitGet[Int]("some-key") === Some(100)) 209 | } 210 | } 211 | 212 | test("transformAndGet-concurrent-timeout") { 213 | withCache("transformAndGet", opTimeout = Some(300.millis)) { cache => 214 | cache.awaitDelete("some-key") 215 | assert(cache.awaitGet[Value]("some-key") === None) 216 | 217 | def incrementValue = 218 | cache.transformAndGet[Int]("some-key", 60.seconds) { 219 | case None => 1 220 | case Some(nr) => nr + 1 221 | } 222 | 223 | val initial = Await.result(incrementValue.flatMap { case _ => incrementValue }, 3.seconds) 224 | assert(initial === 2) 225 | 226 | val seq = concurrent.Future.sequence((0 until 500).map(nr => incrementValue)) 227 | try { 228 | Await.result(seq, 20.seconds) 229 | fail("should throw exception") 230 | } catch { 231 | case ex: TimeoutException => 232 | assert(ex.getMessage === "some-key") 233 | } 234 | } 235 | } 236 | 237 | test("getAndTransform-concurrent-timeout") { 238 | withCache("getAndTransform", opTimeout = Some(300.millis)) { cache => 239 | cache.awaitDelete("some-key") 240 | assert(cache.awaitGet[Value]("some-key") === None) 241 | 242 | def incrementValue = 243 | cache.getAndTransform[Int]("some-key", 60.seconds) { 244 | case None => 1 245 | case Some(nr) => nr + 1 246 | } 247 | 248 | val initial = Await.result(incrementValue.flatMap { case _ => incrementValue }, 3.seconds) 249 | assert(initial === Some(1)) 250 | 251 | val seq = concurrent.Future.sequence((0 until 500).map(nr => incrementValue)) 252 | 253 | try { 254 | Await.result(seq, 20.seconds) 255 | fail("should throw exception") 256 | } catch { 257 | case ex: TimeoutException => 258 | assert(ex.key === "some-key") 259 | } 260 | } 261 | } 262 | 263 | test("increment-decrement") { 264 | withCache("increment-decrement") { cache => 265 | assert(cache.awaitGet[Int]("hello") === None) 266 | 267 | cache.awaitSet("hello", "123", 1.second)(StringBinaryCodec) 268 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 269 | 270 | cache.awaitIncrement("hello", 1, None, 1.second) 271 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("124")) 272 | 273 | cache.awaitDecrement("hello", 1, None, 1.second) 274 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 275 | 276 | Thread.sleep(3000) 277 | 278 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 279 | } 280 | } 281 | 282 | test("increment-decrement-delta") { 283 | withCache("increment-decrement-delta") { cache => 284 | assert(cache.awaitGet[Int]("hello") === None) 285 | 286 | cache.awaitSet("hello", "123", 1.second)(StringBinaryCodec) 287 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 288 | 289 | cache.awaitIncrement("hello", 5, None, 1.second) 290 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("128")) 291 | 292 | cache.awaitDecrement("hello", 5, None, 1.second) 293 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("123")) 294 | 295 | Thread.sleep(3000) 296 | 297 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 298 | } 299 | } 300 | 301 | test("increment-default") { 302 | withCache("increment-default") { cache => 303 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 304 | 305 | cache.awaitIncrement("hello", 1, Some(0), 1.second) 306 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("0")) 307 | 308 | cache.awaitIncrement("hello", 1, Some(0), 1.second) 309 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("1")) 310 | 311 | Thread.sleep(3000) 312 | 313 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === None) 314 | } 315 | } 316 | 317 | test("increment-overflow") { 318 | withCache("increment-overflow") { cache => 319 | assert(cache.awaitIncrement("hello", 1, Some(Long.MaxValue), 1.minute) === Long.MaxValue) 320 | 321 | assert(cache.awaitIncrement("hello", 1, None, 1.minute) === Long.MinValue) 322 | 323 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("9223372036854775808")) 324 | } 325 | } 326 | 327 | test("decrement-underflow") { 328 | withCache("increment-underflow") { cache => 329 | assert(cache.awaitDecrement("hello", 1, Some(1), 1.minute) === 1) 330 | 331 | assert(cache.awaitDecrement("hello", 1, None, 1.minute) === 0) 332 | 333 | assert(cache.awaitDecrement("hello", 1, None, 1.minute) === 0) 334 | 335 | assert(cache.awaitGet[String]("hello")(StringBinaryCodec) === Some("0")) 336 | } 337 | } 338 | 339 | test("vector-inherited-case-classes") { 340 | withCache("vector-inherited-case-classes") { cache => 341 | val content = shade.testModels.contentSeq 342 | cache.awaitSet("blog-posts", content, 60.seconds) 343 | assert(cache.awaitGet[Vector[ContentPiece]]("blog-posts") === Some(content)) 344 | } 345 | } 346 | 347 | test("big-instance-1") { 348 | withCache("big-instance-1") { cache => 349 | val impression = shade.testModels.bigInstance 350 | cache.awaitSet(impression.uuid, impression, 60.seconds) 351 | assert(cache.awaitGet[Impression](impression.uuid) === Some(impression)) 352 | } 353 | } 354 | 355 | test("big-instance-1-manual") { 356 | withCache("big-instance-1-manual") { cache => 357 | val byteOut = new ByteArrayOutputStream() 358 | val objectOut = new ObjectOutputStream(byteOut) 359 | 360 | val impression = shade.testModels.bigInstance 361 | objectOut.writeObject(impression) 362 | val byteArray = byteOut.toByteArray 363 | 364 | cache.awaitSet(impression.uuid, byteArray, 60.seconds) 365 | 366 | val inBytes = cache.awaitGet[Array[Byte]](impression.uuid) 367 | assert(inBytes.isDefined) 368 | assert(inBytes.get.length == byteArray.length) 369 | } 370 | } 371 | 372 | test("big-instance-2") { 373 | withCache("big-instance-2") { cache => 374 | val impression = shade.testModels.bigInstance2 375 | cache.awaitSet(impression.uuid, impression, 60.seconds) 376 | assert(cache.awaitGet[Impression](impression.uuid) === Some(impression)) 377 | } 378 | } 379 | 380 | test("big-instance-3") { 381 | withCache("big-instance-3") { cache => 382 | val impression = shade.testModels.bigInstance 383 | val result = cache.set(impression.uuid, impression, 60.seconds) flatMap { _ => 384 | cache.get[Impression](impression.uuid) 385 | } 386 | 387 | assert(Await.result(result, Duration.Inf) === Some(impression)) 388 | } 389 | } 390 | 391 | test("cancel-strategy simple test") { 392 | withCache("cancel-strategy", failureMode = Some(FailureMode.Cancel)) { cache => 393 | Thread.sleep(100) 394 | val impression = shade.testModels.bigInstance2 395 | cache.awaitSet(impression.uuid, impression, 60.seconds) 396 | assert(cache.awaitGet[Impression](impression.uuid) === Some(impression)) 397 | } 398 | } 399 | 400 | test("infinite-duration") { 401 | withCache("infinite-duration") { cache => 402 | assert(cache.awaitGet[Value]("hello") === None) 403 | try { 404 | cache.awaitSet("hello", Value("world"), Duration.Inf) 405 | assert(cache.awaitGet[Value]("hello") === Some(Value("world"))) 406 | 407 | Thread.sleep(5000) 408 | assert(cache.awaitGet[Value]("hello") === Some(Value("world"))) 409 | } finally { 410 | cache.awaitDelete("hello") 411 | } 412 | } 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/MemcachedImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached 13 | 14 | import java.util.concurrent.TimeUnit 15 | 16 | import monix.execution.{ CancelableFuture, Scheduler } 17 | import net.spy.memcached.ConnectionFactoryBuilder.{ Protocol => SpyProtocol } 18 | import net.spy.memcached.auth.{ AuthDescriptor, PlainCallbackHandler } 19 | import net.spy.memcached.ops.Mutator 20 | import net.spy.memcached.{ FailureMode => SpyFailureMode, _ } 21 | import shade.memcached.internals.{ FailedResult, SuccessfulResult, _ } 22 | import shade.{ CancelledException, TimeoutException, UnhandledStatusException } 23 | 24 | import scala.concurrent.duration._ 25 | import scala.concurrent.{ ExecutionContext, Future } 26 | 27 | /** 28 | * Memcached client implementation based on SpyMemcached. 29 | * 30 | * See the parent trait (Cache) for API docs. 31 | */ 32 | class MemcachedImpl(config: Configuration, ec: ExecutionContext) extends Memcached { 33 | private[this] implicit val context = ec 34 | 35 | /** 36 | * Adds a value for a given key, if the key doesn't already exist in the cache store. 37 | * 38 | * If the key already exists in the cache, the future returned result will be false and the 39 | * current value will not be overridden. If the key isn't there already, the value 40 | * will be set and the future returned result will be true. 41 | * 42 | * The expiry time can be Duration.Inf (infinite duration). 43 | * 44 | * @return either true, in case the value was set, or false otherwise 45 | */ 46 | def add[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): CancelableFuture[Boolean] = 47 | value match { 48 | case null => 49 | CancelableFuture.successful(false) 50 | case _ => 51 | instance.realAsyncAdd(withPrefix(key), codec.serialize(value), 0, exp, config.operationTimeout) map { 52 | case SuccessfulResult(givenKey, Some(_)) => 53 | true 54 | case SuccessfulResult(givenKey, None) => 55 | false 56 | case failure: FailedResult => 57 | throwExceptionOn(failure) 58 | } 59 | } 60 | 61 | /** 62 | * Sets a (key, value) in the cache store. 63 | * 64 | * The expiry time can be Duration.Inf (infinite duration). 65 | */ 66 | def set[T](key: String, value: T, exp: Duration)(implicit codec: Codec[T]): CancelableFuture[Unit] = 67 | value match { 68 | case null => 69 | CancelableFuture.successful(()) 70 | case _ => 71 | instance.realAsyncSet(withPrefix(key), codec.serialize(value), 0, exp, config.operationTimeout) map { 72 | case SuccessfulResult(givenKey, _) => 73 | () 74 | case failure: FailedResult => 75 | throwExceptionOn(failure) 76 | } 77 | } 78 | 79 | /** 80 | * Deletes a key from the cache store. 81 | * 82 | * @return true if a key was deleted or false if there was nothing there to delete 83 | */ 84 | def delete(key: String): CancelableFuture[Boolean] = 85 | instance.realAsyncDelete(withPrefix(key), config.operationTimeout) map { 86 | case SuccessfulResult(givenKey, result) => 87 | result 88 | case failure: FailedResult => 89 | throwExceptionOn(failure) 90 | } 91 | 92 | /** 93 | * Fetches a value from the cache store. 94 | * 95 | * @return Some(value) in case the key is available, or None otherwise (doesn't throw exception on key missing) 96 | */ 97 | def get[T](key: String)(implicit codec: Codec[T]): Future[Option[T]] = 98 | instance.realAsyncGet(withPrefix(key), config.operationTimeout) map { 99 | case SuccessfulResult(givenKey, option) => 100 | option.map(codec.deserialize) 101 | case failure: FailedResult => 102 | throwExceptionOn(failure) 103 | } 104 | 105 | def getOrElse[T](key: String, default: => T)(implicit codec: Codec[T]): Future[T] = 106 | get[T](key) map { 107 | case Some(value) => value 108 | case None => default 109 | } 110 | 111 | /** 112 | * Compare and set. 113 | * 114 | * @param expecting should be None in case the key is not expected, or Some(value) otherwise 115 | * @param exp can be Duration.Inf (infinite) for not setting an expiration 116 | * @return either true (in case the compare-and-set succeeded) or false otherwise 117 | */ 118 | def compareAndSet[T](key: String, expecting: Option[T], newValue: T, exp: Duration)(implicit codec: Codec[T]): Future[Boolean] = 119 | expecting match { 120 | case None => 121 | add[T](key, newValue, exp) 122 | 123 | case Some(expectingValue) => 124 | instance.realAsyncGets(withPrefix(key), config.operationTimeout) flatMap { 125 | case SuccessfulResult(givenKey, None) => 126 | Future.successful(false) 127 | 128 | case SuccessfulResult(givenKey, Some((currentData, casID))) => 129 | if (codec.deserialize(currentData) == expectingValue) 130 | instance.realAsyncCAS(withPrefix(key), casID, 0, codec.serialize(newValue), exp, config.operationTimeout) map { 131 | case SuccessfulResult(_, bool) => 132 | bool 133 | case failure: FailedResult => 134 | throwExceptionOn(failure) 135 | } 136 | else 137 | Future.successful(false) 138 | case failure: FailedResult => 139 | throwExceptionOn(failure) 140 | } 141 | } 142 | 143 | /** 144 | * Used by both transformAndGet and getAndTransform for code reusability. 145 | * 146 | * @param f is the function that dictates what gets returned (either the old or the new value) 147 | */ 148 | private[this] def genericTransform[T, R](key: String, exp: Duration, cb: Option[T] => T)(f: (Option[T], T) => R)(implicit codec: Codec[T]): Future[R] = { 149 | val keyWithPrefix = withPrefix(key) 150 | val timeoutAt = System.currentTimeMillis() + config.operationTimeout.toMillis 151 | 152 | /* 153 | * Inner function used for retrying compare-and-set operations 154 | * with a maximum threshold of retries. 155 | * 156 | * @throws TransformOverflowException in case the maximum number of 157 | * retries is reached 158 | */ 159 | def loop(retry: Int): Future[R] = { 160 | val remainingTime = timeoutAt - System.currentTimeMillis() 161 | 162 | if (remainingTime <= 0) 163 | throw new TimeoutException(key) 164 | 165 | instance.realAsyncGets(keyWithPrefix, remainingTime.millis) flatMap { 166 | case SuccessfulResult(_, None) => 167 | val result = cb(None) 168 | add(key, result, exp) flatMap { 169 | case true => 170 | Future.successful(f(None, result)) 171 | case false => 172 | loop(retry + 1) 173 | } 174 | case SuccessfulResult(_, Some((current, casID))) => 175 | val currentOpt = Some(codec.deserialize(current)) 176 | val result = cb(currentOpt) 177 | 178 | instance.realAsyncCAS(keyWithPrefix, casID, 0, codec.serialize(result), exp, remainingTime.millis) flatMap { 179 | case SuccessfulResult(_, true) => 180 | Future.successful(f(currentOpt, result)) 181 | case SuccessfulResult(_, false) => 182 | loop(retry + 1) 183 | case failure: FailedResult => 184 | throwExceptionOn(failure) 185 | } 186 | 187 | case failure: FailedResult => 188 | throwExceptionOn(failure) 189 | } 190 | } 191 | 192 | loop(0) 193 | } 194 | 195 | /** 196 | * Transforms the given key and returns the new value. 197 | * 198 | * The cb callback receives the current value 199 | * (None in case the key is missing or Some(value) otherwise) 200 | * and should return the new value to store. 201 | * 202 | * The method retries until the compare-and-set operation succeeds, so 203 | * the callback should have no side-effects. 204 | * 205 | * This function can be used for atomic incrementers and stuff like that. 206 | * 207 | * @return the new value 208 | */ 209 | def transformAndGet[T](key: String, exp: Duration)(cb: (Option[T]) => T)(implicit codec: Codec[T]): Future[T] = 210 | genericTransform(key, exp, cb) { 211 | case (oldValue, newValue) => newValue 212 | } 213 | 214 | /** 215 | * Transforms the given key and returns the old value as an Option[T] 216 | * (None in case the key wasn't in the cache or Some(value) otherwise). 217 | * 218 | * The cb callback receives the current value 219 | * (None in case the key is missing or Some(value) otherwise) 220 | * and should return the new value to store. 221 | * 222 | * The method retries until the compare-and-set operation succeeds, so 223 | * the callback should have no side-effects. 224 | * 225 | * This function can be used for atomic incrementers and stuff like that. 226 | * 227 | * @return the old value 228 | */ 229 | def getAndTransform[T](key: String, exp: Duration)(cb: (Option[T]) => T)(implicit codec: Codec[T]): Future[Option[T]] = 230 | genericTransform(key, exp, cb) { 231 | case (oldValue, newValue) => oldValue 232 | } 233 | 234 | def close(): Unit = { 235 | instance.shutdown(3, TimeUnit.SECONDS) 236 | } 237 | 238 | /** 239 | * Atomically increments the given key by a non-negative integer amount 240 | * and returns the new value. 241 | * 242 | * The value is stored as the ASCII decimal representation of a 64-bit 243 | * unsigned integer. 244 | * 245 | * If the key does not exist and a default is provided, sets the value of the 246 | * key to the provided default and expiry time. 247 | * 248 | * If the key does not exist and no default is provided, or if the key exists 249 | * with a value that does not conform to the expected representation, the 250 | * operation will fail. 251 | * 252 | * If the operation succeeds, it returns the new value of the key. 253 | * 254 | * Note that the default value is always treated as None when using the text 255 | * protocol. 256 | * 257 | * The expiry time can be Duration.Inf (infinite duration). 258 | */ 259 | def increment(key: String, by: Long, default: Option[Long], exp: Duration): Future[Long] = 260 | instance.realAsyncMutate(withPrefix(key), by, Mutator.incr, default, exp, config.operationTimeout) map { 261 | case SuccessfulResult(_, value) => 262 | value 263 | case failure: FailedResult => 264 | throwExceptionOn(failure) 265 | } 266 | 267 | /** 268 | * Atomically decrements the given key by a non-negative integer amount 269 | * and returns the new value. 270 | * 271 | * The value is stored as the ASCII decimal representation of a 64-bit 272 | * unsigned integer. 273 | * 274 | * If the key does not exist and a default is provided, sets the value of the 275 | * key to the provided default and expiry time. 276 | * 277 | * If the key does not exist and no default is provided, or if the key exists 278 | * with a value that does not conform to the expected representation, the 279 | * operation will fail. 280 | * 281 | * If the operation succeeds, it returns the new value of the key. 282 | * 283 | * Note that the default value is always treated as None when using the text 284 | * protocol. 285 | * 286 | * The expiry time can be Duration.Inf (infinite duration). 287 | */ 288 | def decrement(key: String, by: Long, default: Option[Long], exp: Duration): Future[Long] = 289 | instance.realAsyncMutate(withPrefix(key), by, Mutator.decr, default, exp, config.operationTimeout) map { 290 | case SuccessfulResult(_, value) => 291 | value 292 | case failure: FailedResult => 293 | throwExceptionOn(failure) 294 | } 295 | 296 | private[this] def throwExceptionOn(failure: FailedResult) = failure match { 297 | case FailedResult(k, TimedOutStatus) => 298 | throw new TimeoutException(withoutPrefix(k)) 299 | case FailedResult(k, CancelledStatus) => 300 | throw new CancelledException(withoutPrefix(k)) 301 | case FailedResult(k, unhandled) => 302 | throw new UnhandledStatusException( 303 | s"For key ${withoutPrefix(k)} - ${unhandled.getClass.getName}" 304 | ) 305 | } 306 | 307 | @inline 308 | private[this] def withPrefix(key: String): String = 309 | if (prefix.isEmpty) 310 | key 311 | else 312 | prefix + "-" + key 313 | 314 | @inline 315 | private[this] def withoutPrefix[T](key: String): String = { 316 | if (!prefix.isEmpty && key.startsWith(prefix + "-")) 317 | key.substring(prefix.length + 1) 318 | else 319 | key 320 | } 321 | 322 | private[this] val prefix = config.keysPrefix.getOrElse("") 323 | private[this] val instance = { 324 | if (System.getProperty("net.spy.log.LoggerImpl") == null) { 325 | System.setProperty( 326 | "net.spy.log.LoggerImpl", 327 | "shade.memcached.internals.Slf4jLogger" 328 | ) 329 | } 330 | 331 | val conn = { 332 | val builder = new ConnectionFactoryBuilder() 333 | .setProtocol( 334 | if (config.protocol == Protocol.Binary) 335 | SpyProtocol.BINARY 336 | else 337 | SpyProtocol.TEXT 338 | ) 339 | .setDaemon(true) 340 | .setFailureMode(config.failureMode match { 341 | case FailureMode.Retry => 342 | SpyFailureMode.Retry 343 | case FailureMode.Cancel => 344 | SpyFailureMode.Cancel 345 | case FailureMode.Redistribute => 346 | SpyFailureMode.Redistribute 347 | }) 348 | .setOpQueueFactory(config.opQueueFactory.orNull) 349 | .setReadOpQueueFactory(config.readQueueFactory.orNull) 350 | .setWriteOpQueueFactory(config.writeQueueFactory.orNull) 351 | .setShouldOptimize(config.shouldOptimize) 352 | .setHashAlg(config.hashAlgorithm) 353 | .setLocatorType(config.locator) 354 | 355 | val withTimeout = config.operationTimeout match { 356 | case duration: FiniteDuration => 357 | builder.setOpTimeout(config.operationTimeout.toMillis) 358 | case _ => 359 | builder 360 | } 361 | 362 | val withTimeoutThreshold = config.timeoutThreshold match { 363 | case Some(threshold) => withTimeout.setTimeoutExceptionThreshold(threshold) 364 | case _ => withTimeout 365 | } 366 | 367 | val withAuth = config.authentication match { 368 | case Some(credentials) => 369 | withTimeoutThreshold.setAuthDescriptor( 370 | new AuthDescriptor( 371 | Array("PLAIN"), 372 | new PlainCallbackHandler(credentials.username, credentials.password) 373 | ) 374 | ) 375 | case None => 376 | withTimeoutThreshold 377 | } 378 | 379 | withAuth 380 | } 381 | 382 | import scala.collection.JavaConverters._ 383 | val addresses = AddrUtil.getAddresses(config.addresses).asScala 384 | new SpyMemcachedIntegration(conn.build(), addresses, Scheduler(context)) 385 | } 386 | } 387 | 388 | -------------------------------------------------------------------------------- /src/main/scala/shade/memcached/internals/SpyMemcachedIntegration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2012-2017 by its authors. Some rights reserved. 3 | * See the project homepage at: https://github.com/monix/shade 4 | * 5 | * Licensed under the MIT License (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy 7 | * of the License at: 8 | * 9 | * https://github.com/monix/shade/blob/master/LICENSE.txt 10 | */ 11 | 12 | package shade.memcached.internals 13 | 14 | import java.io.IOException 15 | import java.net.{ InetSocketAddress, SocketAddress } 16 | import java.util.concurrent.{ CountDownLatch, TimeUnit } 17 | 18 | import monix.execution.{ Cancelable, CancelableFuture, Scheduler } 19 | import monix.execution.atomic.{ Atomic, AtomicBoolean } 20 | import net.spy.memcached._ 21 | import net.spy.memcached.auth.{ AuthDescriptor, AuthThreadMonitor } 22 | import net.spy.memcached.compat.SpyObject 23 | import net.spy.memcached.ops._ 24 | import shade.UnhandledStatusException 25 | 26 | import scala.collection.JavaConverters._ 27 | import scala.concurrent.duration.{ Duration, FiniteDuration } 28 | import scala.concurrent.{ ExecutionContext, Promise } 29 | import scala.util.control.NonFatal 30 | import scala.util.{ Failure, Success, Try } 31 | 32 | /** 33 | * Hooking in the SpyMemcached Internals. 34 | * 35 | * @param cf is Spy's Memcached connection factory 36 | * @param addrs is a list of addresses to connect to 37 | * @param scheduler is for making timeouts work 38 | */ 39 | class SpyMemcachedIntegration(cf: ConnectionFactory, addrs: Seq[InetSocketAddress], scheduler: Scheduler) 40 | extends SpyObject with ConnectionObserver { 41 | 42 | require(cf != null, "Invalid connection factory") 43 | require(addrs != null && addrs.nonEmpty, "Invalid addresses list") 44 | assert(cf.getOperationTimeout > 0, "Operation timeout must be positive") 45 | 46 | protected final val opFact: OperationFactory = cf.getOperationFactory 47 | protected final val mconn: MemcachedConnection = cf.createConnection(addrs.asJava) 48 | protected final val authDescriptor: Option[AuthDescriptor] = Option(cf.getAuthDescriptor) 49 | protected final val authMonitor: AuthThreadMonitor = new AuthThreadMonitor 50 | protected final val shuttingDown: AtomicBoolean = Atomic(false) 51 | 52 | locally { 53 | if (authDescriptor.isDefined) 54 | addObserver(this) 55 | } 56 | 57 | /** 58 | * Add a connection observer. 59 | * 60 | * If connections are already established, your observer will be called with 61 | * the address and -1. 62 | * 63 | * @param obs the ConnectionObserver you wish to add 64 | * @return true if the observer was added. 65 | */ 66 | def addObserver(obs: ConnectionObserver): Boolean = { 67 | val rv = mconn.addObserver(obs) 68 | 69 | if (rv) 70 | for (node <- mconn.getLocator.getAll.asScala) 71 | if (node.isActive) 72 | obs.connectionEstablished(node.getSocketAddress, -1) 73 | rv 74 | } 75 | 76 | def connectionLost(sa: SocketAddress): Unit = { 77 | // Don't care? 78 | } 79 | 80 | /** 81 | * A connection has just successfully been established on the given socket. 82 | * 83 | * @param sa the address of the node whose connection was established 84 | * @param reconnectCount the number of attempts before the connection was 85 | * established 86 | */ 87 | def connectionEstablished(sa: SocketAddress, reconnectCount: Int): Unit = { 88 | for (authDescriptor <- this.authDescriptor) { 89 | if (authDescriptor.authThresholdReached) 90 | this.shutdown() 91 | authMonitor.authConnection(mconn, opFact, authDescriptor, findNode(sa)) 92 | } 93 | } 94 | 95 | /** 96 | * Wait for the queues to die down. 97 | * 98 | * @param timeout the amount of time time for shutdown 99 | * @param unit the TimeUnit for the timeout 100 | * @return result of the request for the wait 101 | * @throws IllegalStateException in the rare circumstance where queue is too 102 | * full to accept any more requests 103 | */ 104 | def waitForQueues(timeout: Long, unit: TimeUnit): Boolean = { 105 | val blatch: CountDownLatch = broadcastOp(new BroadcastOpFactory { 106 | def newOp(n: MemcachedNode, latch: CountDownLatch): Operation = { 107 | opFact.noop(new OperationCallback { 108 | def complete() { 109 | latch.countDown() 110 | } 111 | 112 | def receivedStatus(s: OperationStatus) {} 113 | }) 114 | } 115 | }, mconn.getLocator.getAll, checkShuttingDown = false) 116 | 117 | try { 118 | blatch.await(timeout, unit) 119 | } catch { 120 | case e: InterruptedException => 121 | throw new RuntimeException("Interrupted waiting for queues", e) 122 | } 123 | } 124 | 125 | def broadcastOp(of: BroadcastOpFactory): CountDownLatch = 126 | broadcastOp(of, mconn.getLocator.getAll, checkShuttingDown = true) 127 | 128 | def broadcastOp(of: BroadcastOpFactory, nodes: java.util.Collection[MemcachedNode]): CountDownLatch = 129 | broadcastOp(of, nodes, checkShuttingDown = true) 130 | 131 | /** 132 | * Broadcast an operation to a specific collection of nodes. 133 | */ 134 | private def broadcastOp(of: BroadcastOpFactory, nodes: java.util.Collection[MemcachedNode], checkShuttingDown: Boolean): CountDownLatch = { 135 | if (checkShuttingDown && shuttingDown.get) 136 | throw new IllegalStateException("Shutting down") 137 | mconn.broadcastOperation(of, nodes) 138 | } 139 | 140 | private def findNode(sa: SocketAddress): MemcachedNode = { 141 | val node = mconn.getLocator.getAll.asScala.find(_.getSocketAddress == sa) 142 | assert(node.isDefined, s"Couldn't find node connected to $sa") 143 | node.get 144 | } 145 | 146 | /** 147 | * Shut down immediately. 148 | */ 149 | def shutdown(): Unit = { 150 | shutdown(-1, TimeUnit.SECONDS) 151 | } 152 | 153 | def shutdown(timeout: Long, unit: TimeUnit): Boolean = { 154 | // Guard against double shutdowns (bug 8). 155 | if (!shuttingDown.compareAndSet(expect = false, update = true)) { 156 | getLogger.info("Suppressing duplicate attempt to shut down") 157 | false 158 | } else { 159 | val baseName: String = mconn.getName 160 | mconn.setName(s"$baseName - SHUTTING DOWN") 161 | 162 | try { 163 | if (timeout > 0) { 164 | mconn.setName(s"$baseName - SHUTTING DOWN (waiting)") 165 | waitForQueues(timeout, unit) 166 | } else 167 | true 168 | } finally { 169 | try { 170 | mconn.setName(s"$baseName - SHUTTING DOWN (telling client)") 171 | mconn.shutdown() 172 | mconn.setName(s"$baseName - SHUTTING DOWN (informed client)") 173 | } catch { 174 | case e: IOException => 175 | getLogger.warn("exception while shutting down": Any, e: Throwable) 176 | } 177 | } 178 | } 179 | } 180 | 181 | def realAsyncGet(key: String, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Option[Array[Byte]]]] = { 182 | val promise = Promise[Result[Option[Array[Byte]]]]() 183 | val result = new MutablePartialResult[Option[Array[Byte]]] 184 | 185 | val op: GetOperation = opFact.get(key, new GetOperation.Callback { 186 | def receivedStatus(opStatus: OperationStatus) { 187 | handleStatus(opStatus, key, result) { 188 | case CASNotFoundStatus => 189 | result.tryComplete(Success(SuccessfulResult(key, None))) 190 | case CASSuccessStatus => 191 | } 192 | } 193 | 194 | def gotData(k: String, flags: Int, data: Array[Byte]) { 195 | assert(key == k, "Wrong key returned") 196 | result.tryComplete(Success(SuccessfulResult(key, Option(data)))) 197 | } 198 | 199 | def complete() { 200 | result.completePromise(key, promise) 201 | } 202 | }) 203 | 204 | mconn.enqueueOperation(key, op) 205 | prepareFuture(key, op, promise, timeout) 206 | } 207 | 208 | def realAsyncSet(key: String, data: Array[Byte], flags: Int, exp: Duration, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Long]] = { 209 | val promise = Promise[Result[Long]]() 210 | val result = new MutablePartialResult[Long] 211 | 212 | val op: Operation = opFact.store(StoreType.set, key, flags, expiryToSeconds(exp).toInt, data, new StoreOperation.Callback { 213 | def receivedStatus(opStatus: OperationStatus) { 214 | handleStatus(opStatus, key, result) { 215 | case CASSuccessStatus => 216 | } 217 | } 218 | 219 | def gotData(key: String, cas: Long) { 220 | result.tryComplete(Success(SuccessfulResult(key, cas))) 221 | } 222 | 223 | def complete() { 224 | result.completePromise(key, promise) 225 | } 226 | }) 227 | 228 | mconn.enqueueOperation(key, op) 229 | prepareFuture(key, op, promise, timeout) 230 | } 231 | 232 | def realAsyncAdd(key: String, data: Array[Byte], flags: Int, exp: Duration, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Option[Long]]] = { 233 | val promise = Promise[Result[Option[Long]]]() 234 | val result = new MutablePartialResult[Option[Long]] 235 | 236 | val op: Operation = opFact.store(StoreType.add, key, flags, expiryToSeconds(exp).toInt, data, new StoreOperation.Callback { 237 | def receivedStatus(opStatus: OperationStatus) { 238 | handleStatus(opStatus, key, result) { 239 | case CASExistsStatus => 240 | result.tryComplete(Success(SuccessfulResult(key, None))) 241 | case CASSuccessStatus => 242 | } 243 | } 244 | 245 | def gotData(key: String, cas: Long) { 246 | result.tryComplete(Success(SuccessfulResult(key, Some(cas)))) 247 | } 248 | 249 | def complete() { 250 | result.completePromise(key, promise) 251 | } 252 | }) 253 | 254 | mconn.enqueueOperation(key, op) 255 | prepareFuture(key, op, promise, timeout) 256 | } 257 | 258 | def realAsyncDelete(key: String, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Boolean]] = { 259 | val promise = Promise[Result[Boolean]]() 260 | val result = new MutablePartialResult[Boolean] 261 | 262 | val op = opFact.delete(key, new DeleteOperation.Callback { 263 | def gotData(cas: Long): Unit = () 264 | 265 | def complete() { 266 | result.completePromise(key, promise) 267 | } 268 | 269 | def receivedStatus(opStatus: OperationStatus) { 270 | handleStatus(opStatus, key, result) { 271 | case CASSuccessStatus => 272 | result.tryComplete(Success(SuccessfulResult(key, true))) 273 | case CASNotFoundStatus => 274 | result.tryComplete(Success(SuccessfulResult(key, false))) 275 | } 276 | } 277 | }) 278 | 279 | mconn.enqueueOperation(key, op) 280 | prepareFuture(key, op, promise, timeout) 281 | } 282 | 283 | def realAsyncGets(key: String, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Option[(Array[Byte], Long)]]] = { 284 | val promise = Promise[Result[Option[(Array[Byte], Long)]]]() 285 | val result = new MutablePartialResult[Option[(Array[Byte], Long)]] 286 | 287 | val op: Operation = opFact.gets(key, new GetsOperation.Callback { 288 | def receivedStatus(opStatus: OperationStatus) { 289 | handleStatus(opStatus, key, result) { 290 | case CASNotFoundStatus => 291 | result.tryComplete(Success(SuccessfulResult(key, None))) 292 | case CASSuccessStatus => 293 | } 294 | } 295 | 296 | def gotData(receivedKey: String, flags: Int, cas: Long, data: Array[Byte]) { 297 | assert(key == receivedKey, "Wrong key returned") 298 | assert(cas > 0, s"CAS was less than zero: $cas") 299 | 300 | result.tryComplete(Try { 301 | SuccessfulResult(key, Option(data).map(d => (d, cas))) 302 | }) 303 | } 304 | 305 | def complete() { 306 | result.completePromise(key, promise) 307 | } 308 | }) 309 | 310 | mconn.enqueueOperation(key, op) 311 | prepareFuture(key, op, promise, timeout) 312 | } 313 | 314 | def realAsyncCAS(key: String, casID: Long, flags: Int, data: Array[Byte], exp: Duration, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Boolean]] = { 315 | val promise = Promise[Result[Boolean]]() 316 | val result = new MutablePartialResult[Boolean] 317 | 318 | val op = opFact.cas(StoreType.set, key, casID, flags, expiryToSeconds(exp).toInt, data, new StoreOperation.Callback { 319 | def receivedStatus(opStatus: OperationStatus) { 320 | handleStatus(opStatus, key, result) { 321 | case CASSuccessStatus => 322 | result.tryComplete(Success(SuccessfulResult(key, true))) 323 | case CASExistsStatus => 324 | result.tryComplete(Success(SuccessfulResult(key, false))) 325 | case CASNotFoundStatus => 326 | result.tryComplete(Success(SuccessfulResult(key, false))) 327 | } 328 | } 329 | 330 | def gotData(k: String, cas: Long) { 331 | assert(key == k, "Wrong key returned") 332 | } 333 | 334 | def complete() { 335 | result.completePromise(key, promise) 336 | } 337 | }) 338 | 339 | mconn.enqueueOperation(key, op) 340 | prepareFuture(key, op, promise, timeout) 341 | } 342 | 343 | def realAsyncMutate(key: String, by: Long, mutator: Mutator, default: Option[Long], exp: Duration, timeout: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[Long]] = { 344 | val promise = Promise[Result[Long]]() 345 | val result = new MutablePartialResult[Long] 346 | 347 | val expiry = default match { 348 | case Some(_) => expiryToSeconds(exp).toInt 349 | case None => -1 // expiry of all 1-bits disables setting default in case of nonexistent key 350 | } 351 | 352 | val op: Operation = opFact.mutate(mutator, key, by, default.getOrElse(0L), expiry, new OperationCallback { 353 | def receivedStatus(opStatus: OperationStatus) { 354 | handleStatus(opStatus, key, result) { 355 | case CASSuccessStatus => 356 | result.tryComplete(Success(SuccessfulResult(key, opStatus.getMessage.toLong))) 357 | } 358 | } 359 | 360 | def complete() { 361 | result.completePromise(key, promise) 362 | } 363 | }) 364 | 365 | mconn.enqueueOperation(key, op) 366 | prepareFuture(key, op, promise, timeout) 367 | } 368 | 369 | protected final def prepareFuture[T](key: String, op: Operation, promise: Promise[Result[T]], atMost: FiniteDuration)(implicit ec: ExecutionContext): CancelableFuture[Result[T]] = { 370 | val operationCancelable = Cancelable(() => { 371 | try { 372 | if (!op.isCancelled) 373 | op.cancel() 374 | } catch { 375 | case NonFatal(ex) => 376 | ec.reportFailure(ex) 377 | } 378 | }) 379 | 380 | val timeout = scheduler.scheduleOnce(atMost) { 381 | promise.tryComplete { 382 | if (op.hasErrored) 383 | Failure(op.getException) 384 | else if (op.isCancelled) 385 | Success(FailedResult(key, CancelledStatus)) 386 | else 387 | Success(FailedResult(key, TimedOutStatus)) 388 | } 389 | } 390 | 391 | val future = promise.future 392 | val mainCancelable = Cancelable { () => 393 | timeout.cancel() 394 | operationCancelable.cancel() 395 | } 396 | 397 | future.onComplete { msg => 398 | try { 399 | timeout.cancel() 400 | } catch { 401 | case NonFatal(ex) => 402 | ec.reportFailure(ex) 403 | } 404 | 405 | msg match { 406 | case Success(FailedResult(_, TimedOutStatus)) => 407 | MemcachedConnection.opTimedOut(op) 408 | op.timeOut() 409 | if (!op.isCancelled) try op.cancel() catch { 410 | case NonFatal(_) => 411 | } 412 | case Success(FailedResult(_, _)) => 413 | if (!op.isCancelled) try op.cancel() catch { 414 | case NonFatal(_) => 415 | } 416 | case _ => 417 | MemcachedConnection.opSucceeded(op) 418 | } 419 | } 420 | 421 | CancelableFuture(future, mainCancelable) 422 | } 423 | 424 | protected final val statusTranslation: PartialFunction[OperationStatus, Status] = { 425 | case _: CancelledOperationStatus => 426 | CancelledStatus 427 | case _: TimedOutOperationStatus => 428 | TimedOutStatus 429 | case status: CASOperationStatus => 430 | status.getCASResponse match { 431 | case CASResponse.EXISTS => 432 | CASExistsStatus 433 | case CASResponse.NOT_FOUND => 434 | CASNotFoundStatus 435 | case CASResponse.OK => 436 | CASSuccessStatus 437 | case CASResponse.OBSERVE_ERROR_IN_ARGS => 438 | CASObserveErrorInArgs 439 | case CASResponse.OBSERVE_MODIFIED => 440 | CASObserveModified 441 | case CASResponse.OBSERVE_TIMEOUT => 442 | CASObserveTimeout 443 | } 444 | case x if x.isSuccess => 445 | CASSuccessStatus 446 | } 447 | 448 | protected final def expiryToSeconds(duration: Duration): Long = duration match { 449 | case finite: FiniteDuration => 450 | val seconds = finite.toSeconds 451 | if (seconds < 60 * 60 * 24 * 30) 452 | seconds 453 | else 454 | System.currentTimeMillis() / 1000 + seconds 455 | case _ => 456 | // infinite duration (set to 0) 457 | 0 458 | } 459 | 460 | /** 461 | * Handles OperationStatuses from SpyMemcached 462 | * 463 | * The first argument list takes the SpyMemcached operation status, and also the key and result so that this method 464 | * itself can attach sane failure handling. 465 | * 466 | * The second argument list is a simple PartialFunction that allows you to side effect for the translated [[Status]]s you care about, 467 | * typically by completing the result. 468 | * 469 | * @param spyMemcachedStatus SpyMemcached OperationStatus to be translated 470 | * @param key String key involved in the operation 471 | * @param result MutablePartialResult 472 | * @param handler a partial function that takes a translated [[Status]] and side-effects 473 | */ 474 | private def handleStatus( 475 | spyMemcachedStatus: OperationStatus, 476 | key: String, 477 | result: MutablePartialResult[_])(handler: PartialFunction[Status, Unit]): Unit = { 478 | val status = statusTranslation.applyOrElse(spyMemcachedStatus, UnhandledStatus.fromSpyMemcachedStatus) 479 | handler.applyOrElse(status, { 480 | case UnhandledStatus(statusClass, statusMsg) => result.tryComplete(Failure(new UnhandledStatusException(s"$statusClass($statusMsg)"))) 481 | // nothing 482 | case failure => 483 | result.tryComplete(Success(FailedResult(key, failure))) 484 | }: Function[Status, Unit]) 485 | } 486 | } 487 | --------------------------------------------------------------------------------