|
| 1 | +package com.twitter.finagle.memcached.integration |
| 2 | + |
| 3 | +import com.twitter.conversions.DurationOps._ |
| 4 | +import com.twitter.finagle.Address |
| 5 | +import com.twitter.finagle.Name |
| 6 | +import com.twitter.finagle.memcached.Client |
| 7 | +import com.twitter.finagle.memcached.integration.external.ExternalMemcached |
| 8 | +import com.twitter.finagle.memcached.integration.external.MemcachedServer |
| 9 | +import com.twitter.finagle.memcached.protocol._ |
| 10 | +import com.twitter.io.Buf |
| 11 | +import com.twitter.util.Await |
| 12 | +import com.twitter.util.Awaitable |
| 13 | +import org.scalatest.BeforeAndAfter |
| 14 | +import org.scalatest.funsuite.AnyFunSuite |
| 15 | + |
| 16 | +// These tests run against either a test Memcached server or a real MemcachedServer, |
| 17 | +// depending on the value of `ExternalMemcached.use` |
| 18 | +abstract class ClientAPITest extends AnyFunSuite with BeforeAndAfter { |
| 19 | + |
| 20 | + protected var client: Client = _ |
| 21 | + private var testServer: MemcachedServer = _ |
| 22 | + |
| 23 | + protected def awaitResult[T](awaitable: Awaitable[T]): T = Await.result(awaitable, 5.seconds) |
| 24 | + |
| 25 | + protected def mkClient(dest: Name): Client |
| 26 | + |
| 27 | + before { |
| 28 | + testServer = MemcachedServer.start() |
| 29 | + client = mkClient(Name.bound(Seq(Address(testServer.address)): _*)) |
| 30 | + } |
| 31 | + |
| 32 | + after { |
| 33 | + testServer.stop() |
| 34 | + } |
| 35 | + |
| 36 | + test("set & get") { |
| 37 | + awaitResult(client.delete("foo")) |
| 38 | + assert(awaitResult(client.get("foo")) == None) |
| 39 | + awaitResult(client.set("foo", Buf.Utf8("bar"))) |
| 40 | + assert(awaitResult(client.get("foo")).get == Buf.Utf8("bar")) |
| 41 | + } |
| 42 | + |
| 43 | + test("set & get data containing newlines") { |
| 44 | + awaitResult(client.delete("bob")) |
| 45 | + assert(awaitResult(client.get("bob")) == None) |
| 46 | + awaitResult(client.set("bob", Buf.Utf8("hello there \r\n nice to meet \r\n you"))) |
| 47 | + assert( |
| 48 | + awaitResult(client.get("bob")).get == Buf.Utf8("hello there \r\n nice to meet \r\n you"), |
| 49 | + 3.seconds |
| 50 | + ) |
| 51 | + } |
| 52 | + |
| 53 | + test("get") { |
| 54 | + awaitResult(client.set("foo", Buf.Utf8("bar"))) |
| 55 | + awaitResult(client.set("baz", Buf.Utf8("boing"))) |
| 56 | + val result = awaitResult(client.get(Seq("foo", "baz", "notthere"))) |
| 57 | + .map { case (key, Buf.Utf8(value)) => (key, value) } |
| 58 | + assert( |
| 59 | + result == Map( |
| 60 | + "foo" -> "bar", |
| 61 | + "baz" -> "boing" |
| 62 | + ) |
| 63 | + ) |
| 64 | + } |
| 65 | + |
| 66 | + test("getWithFlag") { |
| 67 | + awaitResult(client.set("foo", Buf.Utf8("bar"))) |
| 68 | + awaitResult(client.set("baz", Buf.Utf8("boing"))) |
| 69 | + val result = awaitResult(client.getWithFlag(Seq("foo", "baz", "notthere"))) |
| 70 | + .map { case (key, ((Buf.Utf8(value), Buf.Utf8(flag)))) => (key, (value, flag)) } |
| 71 | + assert( |
| 72 | + result == Map( |
| 73 | + "foo" -> (("bar", "0")), |
| 74 | + "baz" -> (("boing", "0")) |
| 75 | + ) |
| 76 | + ) |
| 77 | + } |
| 78 | + |
| 79 | + test("append & prepend") { |
| 80 | + awaitResult(client.set("foo", Buf.Utf8("bar"))) |
| 81 | + awaitResult(client.append("foo", Buf.Utf8("rab"))) |
| 82 | + val Buf.Utf8(res) = awaitResult(client.get("foo")).get |
| 83 | + assert(res == "barrab") |
| 84 | + awaitResult(client.prepend("foo", Buf.Utf8("rab"))) |
| 85 | + val Buf.Utf8(res2) = awaitResult(client.get("foo")).get |
| 86 | + assert(res2 == "rabbarrab") |
| 87 | + } |
| 88 | + |
| 89 | + test("incr & decr") { |
| 90 | + // As of memcached 1.4.8 (issue 221), empty values are no longer treated as integers |
| 91 | + awaitResult(client.set("foo", Buf.Utf8("0"))) |
| 92 | + assert(awaitResult(client.incr("foo")) == Some(1L)) |
| 93 | + assert(awaitResult(client.incr("foo", 2)) == Some(3L)) |
| 94 | + assert(awaitResult(client.decr("foo")) == Some(2L)) |
| 95 | + |
| 96 | + awaitResult(client.set("foo", Buf.Utf8("0"))) |
| 97 | + assert(awaitResult(client.incr("foo")) == Some(1L)) |
| 98 | + val l = 1L << 50 |
| 99 | + assert(awaitResult(client.incr("foo", l)) == Some(l + 1L)) |
| 100 | + assert(awaitResult(client.decr("foo")) == Some(l)) |
| 101 | + assert(awaitResult(client.decr("foo", l)) == Some(0L)) |
| 102 | + } |
| 103 | + |
| 104 | + test("send malformed keys") { |
| 105 | + // test key validation trait |
| 106 | + intercept[ClientError] { |
| 107 | + awaitResult(client.get("fo o")) |
| 108 | + } |
| 109 | + intercept[ClientError] { |
| 110 | + awaitResult(client.set("", Buf.Utf8("bar"))) |
| 111 | + } |
| 112 | + intercept[ClientError] { |
| 113 | + awaitResult(client.get(" foo")) |
| 114 | + } |
| 115 | + intercept[ClientError] { |
| 116 | + awaitResult(client.get("foo ")) |
| 117 | + } |
| 118 | + intercept[ClientError] { |
| 119 | + awaitResult(client.get(" foo")) |
| 120 | + } |
| 121 | + intercept[ClientError] { |
| 122 | + awaitResult(client.get(null: String)) |
| 123 | + } |
| 124 | + intercept[ClientError] { |
| 125 | + awaitResult(client.set(null: String, Buf.Utf8("bar"))) |
| 126 | + } |
| 127 | + intercept[ClientError] { |
| 128 | + awaitResult(client.set(" ", Buf.Utf8("bar"))) |
| 129 | + } |
| 130 | + |
| 131 | + try { |
| 132 | + awaitResult(client.set("\t", Buf.Utf8("bar"))) |
| 133 | + } catch { |
| 134 | + case _: ClientError => fail("\t is allowed") |
| 135 | + } |
| 136 | + |
| 137 | + intercept[ClientError] { |
| 138 | + awaitResult(client.set("\r", Buf.Utf8("bar"))) |
| 139 | + } |
| 140 | + intercept[ClientError] { |
| 141 | + awaitResult(client.set("\n", Buf.Utf8("bar"))) |
| 142 | + } |
| 143 | + intercept[ClientError] { |
| 144 | + awaitResult(client.set("\u0000", Buf.Utf8("bar"))) |
| 145 | + } |
| 146 | + |
| 147 | + val veryLongKey = |
| 148 | + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" |
| 149 | + intercept[ClientError] { |
| 150 | + awaitResult(client.get(veryLongKey)) |
| 151 | + } |
| 152 | + intercept[ClientError] { |
| 153 | + awaitResult(client.set(veryLongKey, Buf.Utf8("bar"))) |
| 154 | + } |
| 155 | + |
| 156 | + // test other keyed command validation |
| 157 | + intercept[ClientError] { |
| 158 | + awaitResult(client.gets(Seq(null))) |
| 159 | + } |
| 160 | + intercept[ClientError] { |
| 161 | + awaitResult(client.gets(Seq(""))) |
| 162 | + } |
| 163 | + intercept[ClientError] { |
| 164 | + awaitResult(client.gets(Seq("foos", "bad key", "somethingelse"))) |
| 165 | + } |
| 166 | + intercept[ClientError] { |
| 167 | + awaitResult(client.append("bad key", Buf.Utf8("rab"))) |
| 168 | + } |
| 169 | + intercept[ClientError] { |
| 170 | + awaitResult(client.prepend("bad key", Buf.Utf8("rab"))) |
| 171 | + } |
| 172 | + intercept[ClientError] { |
| 173 | + awaitResult(client.replace("bad key", Buf.Utf8("bar"))) |
| 174 | + } |
| 175 | + intercept[ClientError] { |
| 176 | + awaitResult(client.add("bad key", Buf.Utf8("2"))) |
| 177 | + } |
| 178 | + intercept[ClientError] { |
| 179 | + awaitResult(client.checkAndSet("bad key", Buf.Utf8("z"), Buf.Utf8("2"))) |
| 180 | + } |
| 181 | + intercept[ClientError] { |
| 182 | + awaitResult(client.incr("bad key")) |
| 183 | + } |
| 184 | + intercept[ClientError] { |
| 185 | + awaitResult(client.decr("bad key")) |
| 186 | + } |
| 187 | + intercept[ClientError] { |
| 188 | + awaitResult(client.delete("bad key")) |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + // Only run these if we have a real memcached server to use |
| 193 | + if (ExternalMemcached.use()) { |
| 194 | + test("gets") { |
| 195 | + assert(awaitResult(client.gets("foos")).isEmpty) |
| 196 | + awaitResult(client.set("foos", Buf.Utf8("xyz"))) |
| 197 | + awaitResult(client.set("bazs", Buf.Utf8("xyz"))) |
| 198 | + awaitResult(client.set("bazs", Buf.Utf8("zyx"))) |
| 199 | + val result = awaitResult(client.gets(Seq("foos", "bazs", "somethingelse"))) |
| 200 | + .map { |
| 201 | + case (key, (Buf.Utf8(value), Buf.Utf8(casUnique))) => |
| 202 | + (key, (value, casUnique)) |
| 203 | + } |
| 204 | + |
| 205 | + assert( |
| 206 | + result == Map( |
| 207 | + "foos" -> ( |
| 208 | + ( |
| 209 | + "xyz", |
| 210 | + "2" |
| 211 | + ) |
| 212 | + ), // the "cas unique" values are predictable from a fresh memcached |
| 213 | + "bazs" -> (("zyx", "4")) |
| 214 | + ) |
| 215 | + ) |
| 216 | + } |
| 217 | + |
| 218 | + test("getsWithFlag") { |
| 219 | + awaitResult(client.set("foos1", Buf.Utf8("xyz"))) |
| 220 | + awaitResult(client.set("bazs1", Buf.Utf8("xyz"))) |
| 221 | + awaitResult(client.set("bazs1", Buf.Utf8("zyx"))) |
| 222 | + val result = |
| 223 | + awaitResult(client.getsWithFlag(Seq("foos1", "bazs1", "somethingelse"))) |
| 224 | + .map { |
| 225 | + case (key, (Buf.Utf8(value), Buf.Utf8(flag), Buf.Utf8(casUnique))) => |
| 226 | + (key, (value, flag, casUnique)) |
| 227 | + } |
| 228 | + |
| 229 | + // the "cas unique" values are predictable from a fresh memcached |
| 230 | + assert( |
| 231 | + result == Map( |
| 232 | + "foos1" -> (("xyz", "0", "2")), |
| 233 | + "bazs1" -> (("zyx", "0", "4")) |
| 234 | + ) |
| 235 | + ) |
| 236 | + } |
| 237 | + |
| 238 | + test("cas") { |
| 239 | + awaitResult(client.set("x", Buf.Utf8("y"))) |
| 240 | + val Some((value, casUnique)) = awaitResult(client.gets("x")) |
| 241 | + assert(value == Buf.Utf8("y")) |
| 242 | + assert(casUnique == Buf.Utf8("2")) |
| 243 | + |
| 244 | + assert(!awaitResult(client.checkAndSet("x", Buf.Utf8("z"), Buf.Utf8("1")).map(_.replaced))) |
| 245 | + assert( |
| 246 | + awaitResult(client.checkAndSet("x", Buf.Utf8("z"), casUnique).map(_.replaced)).booleanValue |
| 247 | + ) |
| 248 | + val res = awaitResult(client.get("x")) |
| 249 | + assert(res.isDefined) |
| 250 | + assert(res.get == Buf.Utf8("z")) |
| 251 | + } |
| 252 | + } |
| 253 | +} |
0 commit comments