├── .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 | [](https://travis-ci.org/monix/shade)
4 | [](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 |
--------------------------------------------------------------------------------