1 module memcached4d; 2 3 // TODO: 4 // documentation 5 // unit tests 6 // 7 8 9 /** 10 Memcached client for the D programming language. 11 memcached is a distributed caching system (http://www.memcached.org) 12 13 The basic idea is this: if you need to share/cache objects between applications you dump them into memcache in a serialized form. 14 the data can be read back from other programs - even from other programming language - provided that the reader knows how to deserialize the data. 15 a common way to serialize data is json. Memcached4d uses vibe serialization library to dump your data to json, but you can provide your own serialization method 16 by implementing a JSON toJson method in your objects. 17 18 19 20 A similar tool - with a lot more features is Redis - you may have a look at vibe.db.redis 21 22 23 usage 24 auto cache = memcachedConnect('127.0.0.1'); 25 auto cache = memcachedConnect('127.0.0.1:11211'); 26 auto cache = memcachedConnect( ['127.0.0.1', '127.0.0.1'] ); // you can connect the the same server multiplie times 27 auto cache = memcachedConnect( '127.0.0.1, 127.0.0.1' ); 28 auto cache = memcachedConnect( '127.0.0.1:11211, 127.0.0.1:11212' ); 29 30 31 32 if( cache.store("str_var", "lorem ipsum") == RETURN_STATE.SUCCESS ) { 33 writeln("stored successfully"); 34 writeln( " get back the stored data : {", cache.get!string("str_var") , "}" ); 35 }else { 36 writeln("not stored") 37 } 38 39 40 */ 41 42 43 import std.stdio; 44 import std.string, 45 std.conv, 46 std.digest.md; 47 48 49 50 import vibe.core.net, 51 vibe.stream.operations, 52 vibe.data.serialization, 53 vibe.data.json; 54 55 56 57 struct MemcachedServer { 58 string host; 59 ushort port = 11211; 60 TCPConnection conn; 61 this(string hostString){ 62 if( hostString.indexOf(':') != -1){ 63 auto parts = split(hostString, ':'); 64 this.host = strip(parts[0]); 65 this.port = strip(parts[1]).to!ushort; 66 } else { 67 this.host = strip(hostString); 68 } 69 } 70 } 71 72 alias MemcachedServer[] MemcachedServers; 73 74 75 /** 76 * Use this to connectect to a memcached cluster 77 * 78 * 79 */ 80 class MemcachedClusterClient : MemcachedClient { 81 82 83 protected MemcachedServers servers; 84 85 this(MemcachedServers servers) { 86 this.servers = servers; 87 } 88 89 90 override void connect(string key) { 91 auto server = getServer(key); 92 this.conn = server.conn; 93 if (!conn || !conn.connected) { 94 try conn = connectTCP(server.host, server.port); 95 catch (Exception e) { 96 throw new Exception(format("Failed to connect to memcached server at %s:%s.", server.host, server.port), __FILE__, __LINE__, e); 97 } 98 } 99 } 100 101 override void disconnect() { 102 foreach(server; servers) { 103 if (server.conn && server.conn.connected) 104 server.conn.close(); 105 } 106 } 107 108 109 110 MemcachedServer getServer(string key) { 111 return servers[ determineServer(key) ]; 112 } 113 114 int determineServer(string key){ 115 return equalWeights(key); 116 } 117 118 /** 119 * use this for sharding - when all servers have equal weights * 120 * 121 * hash the key, get the last byte 122 * get the modulo of that last byte to the length of servers 123 */ 124 int equalWeights(string key){ 125 return md5Of(key)[$-1] % servers.length; 126 } 127 128 } 129 130 enum RETURN_STATE { SUCCESS, ERROR, STORED, EXISTS, NOT_FOUND, NOT_STORED }; 131 132 /** 133 * Use this to connect and query a memcached server 134 * 135 * 136 */ 137 class MemcachedClient { 138 TCPConnection conn; 139 protected MemcachedServer server; 140 141 this() {} // allow adding a different constructor in subclasses 142 143 this(string host, ushort port = 11211) { 144 server.host = host; 145 server.port = port; 146 conn = server.conn; 147 } 148 149 this(MemcachedServer server){ 150 this.server = server; 151 conn = server.conn; 152 } 153 154 155 void connect(string key) { 156 if (!conn || !conn.connected) { 157 try conn = connectTCP(server.host, server.port); 158 catch (Exception e) { 159 throw new Exception(format("Failed to connect to memcached server at %s:%s.", server.host, server.port), __FILE__, __LINE__, e); 160 } 161 } 162 } 163 164 void disconnect() { 165 if( conn && conn.connected ){ 166 conn.close(); 167 } 168 } 169 170 171 /** 172 * convert User data type to a string representation 173 */ 174 protected string serialize(T)(T data) { 175 alias Unqual!T Unqualified; 176 177 string value; 178 static if (is(Unqualified : string)) { 179 value = data; 180 } else static if(is(Unqualified : int) 181 || is(Unqualified == bool) 182 || is(Unqualified == double) 183 || is(Unqualified == float) 184 || is(Unqualified : long) 185 ){ 186 value = data.to!string; 187 } 188 else static if( isJsonSerializable!Unqualified ){ 189 value = data.toJson; 190 } else { 191 value = vibe.data.serialization.serialize!JsonSerializer(data).toString; 192 } 193 return value; 194 } 195 196 /** 197 * store data with key, make it expire after expires secconds 198 * if expires == 0 - data will not expire 199 * 200 */ 201 RETURN_STATE store(T)(string key, T data, int expires = 0) { 202 connect(key); 203 string value = serialize(data); 204 conn.write( format("set %s 0 %s %s\r\n%s\r\n", key, expires, value.length.to!string, value) ); 205 auto retval = cast(string) conn.readLine(); 206 207 if(retval == "STORED") { return RETURN_STATE.SUCCESS ; } 208 else { return RETURN_STATE.ERROR; } 209 } 210 211 212 213 RETURN_STATE add(T)(string key, T data, int expires = 0){ 214 connect(key); 215 string value = serialize(data); 216 conn.write( format("add %s 0 %s %s\r\n%s\r\n", key, expires, value.length.to!string, value) ); 217 218 auto retval = cast(string) conn.readLine(); 219 220 if(retval == "STORED") { return RETURN_STATE.SUCCESS ; } 221 else if(retval == "NOT_STORED") { return RETURN_STATE.NOT_STORED ; } 222 else { return RETURN_STATE.ERROR; } 223 } 224 225 RETURN_STATE replace(T)(string key, T data, int expires = 0){ 226 connect(key); 227 string value = serialize(data); 228 conn.write( format("replace %s 0 %s %s\r\n%s\r\n", key, expires, value.length.to!string, value) ); 229 230 auto retval = cast(string) conn.readLine(); 231 232 if(retval == "STORED") { return RETURN_STATE.SUCCESS ; } 233 else if(retval == "NOT_STORED") { return RETURN_STATE.NOT_STORED ; } 234 else { return RETURN_STATE.ERROR; } 235 } 236 237 238 RETURN_STATE append(T)(string key, T data){ 239 connect(key); 240 string value = serialize(data); 241 conn.write( format("append %s 0 0 %s\r\n%s\r\n", key, value.length.to!string, value) ); 242 243 auto retval = cast(string) conn.readLine(); 244 if(retval == "STORED") { return RETURN_STATE.SUCCESS ; } 245 else if(retval == "NOT_STORED") { return RETURN_STATE.NOT_STORED ; } 246 else { return RETURN_STATE.ERROR; } 247 } 248 249 RETURN_STATE prepend(T)(string key, T data){ 250 connect(key); 251 string value = serialize(data); 252 conn.write( format("prepend %s 0 0 %s\r\n%s\r\n", key, value.length.to!string, value) ); 253 254 auto retval = cast(string) conn.readLine(); 255 if(retval == "STORED") { return RETURN_STATE.SUCCESS ; } 256 else if(retval == "NOT_STORED") { return RETURN_STATE.NOT_STORED ; } 257 else { return RETURN_STATE.ERROR; } 258 } 259 260 261 RETURN_STATE cas(T)(string key, T data, int casId, int expires = 0){ 262 connect(key); 263 string value = serialize(data); 264 conn.write( format("cas %s 0 %s %s %s\r\n%s\r\n", key, expires, value.length.to!string, casId, value) ); 265 266 auto retval = cast(string) conn.readLine(); 267 if(retval == "STORED") { return RETURN_STATE.SUCCESS ; } 268 else if(retval == "NOT_FOUND") { return RETURN_STATE.NOT_FOUND ; } 269 else if(retval == "EXISTS") { return RETURN_STATE.EXISTS ; } 270 else { return RETURN_STATE.ERROR; } 271 } 272 273 274 RETURN_STATE remove(string key, int time = 0){ 275 connect(key); 276 conn.write( format("delete %s %s\r\n", key, time)); 277 278 auto retval = cast(string) conn.readLine(); 279 if(retval == "DELETED") { return RETURN_STATE.SUCCESS ; } 280 else if(retval == "NOT_FOUND") { return RETURN_STATE.NOT_FOUND ; } 281 else { return RETURN_STATE.ERROR; } 282 } 283 alias remove del; 284 285 RETURN_STATE increment(string key, int inc){ 286 connect(key); 287 conn.write( format("incr %s %s\r\n", key, inc)); 288 289 auto retval = cast(string) conn.readLine(); 290 if( retval.isNumeric() ) { return RETURN_STATE.SUCCESS; } 291 else if(retval == "NOT_FOUND") { return RETURN_STATE.NOT_FOUND ; } 292 else if(retval.startsWith("CLIENT_ERROR")) { return RETURN_STATE.ERROR ; } 293 else if(retval == "ERROR" ) { return RETURN_STATE.ERROR ; } 294 else { return RETURN_STATE.ERROR; } 295 296 } 297 alias increment incr; 298 299 300 RETURN_STATE decrement(string key, int dec){ 301 connect(key); 302 conn.write( format("decr %s %s\r\n", key, dec)); 303 304 auto retval = cast(string) conn.readLine(); 305 if( retval.isNumeric() ) { return RETURN_STATE.SUCCESS; } 306 else if(retval == "NOT_FOUND") { return RETURN_STATE.NOT_FOUND ; } 307 else if(retval.startsWith("CLIENT_ERROR")) { return RETURN_STATE.ERROR ; } 308 else if(retval == "ERROR" ) { return RETURN_STATE.ERROR ; } 309 else { return RETURN_STATE.SUCCESS; } 310 } 311 alias decrement decr; 312 313 314 RETURN_STATE touch(string key, int expires){ 315 connect(key); 316 conn.write( format("touch %s %s\r\n", key, expires) ); 317 318 auto retval = cast(string) conn.readLine(); 319 if(retval == "TOUCHED") { return RETURN_STATE.SUCCESS ; } 320 else if(retval == "NOT_FOUND") { return RETURN_STATE.NOT_FOUND ; } 321 else { return RETURN_STATE.ERROR; } 322 } 323 324 325 /** 326 * convert data stored in memcached to the requested type 327 */ 328 protected T deserialize(T)(string data) { 329 alias Unqual!T Unqualified; 330 331 static if(is(Unqualified: string)){ 332 return data; 333 }else static if(is(Unqualified : int) 334 || is(Unqualified == bool) 335 || is(Unqualified == double) 336 || is(Unqualified == float) 337 || is(Unqualified : long) 338 ){ 339 340 return chomp(data).to!T; 341 } else { 342 Json j = parseJsonString( data); 343 return deserializeJson!T(j); 344 } 345 } 346 347 348 349 /** 350 * get back data from the memcached server, 351 * return it as type T 352 * 353 * if the key is not found, it will return an empty string 354 */ 355 T get(T)(string key){ 356 connect(key); 357 conn.write( std..string.format("get %s \r\n", key ) ); 358 auto tmp = std.array.appender!string; 359 do{ 360 string ln = cast(string) conn.readLine(); 361 if(ln.startsWith("VALUE " ~ key)) 362 continue; 363 else if(ln == "END") 364 break; 365 366 tmp.put(ln ~ "\r\n"); 367 368 } while( true); 369 370 return deserialize!T( tmp.data ); 371 } 372 373 374 /** 375 * get back data form the memcached server, 376 * return it as type T, 377 * 378 * populate the casId variable with the cas_unique_id 379 * 380 */ 381 T gets(T)(string key, out int casId) { 382 connect(key); 383 conn.write( std..string.format("gets %s \r\n", key ) ); 384 385 auto tmp = std.array.appender!string; 386 do{ 387 string ln = cast(string) conn.readLine(); 388 if(ln.startsWith("VALUE " ~ key)) { 389 auto parts = split(ln); 390 casId = parts[ $-1 ].to!int; 391 continue; 392 } else if(ln == "END") { 393 break; 394 } 395 396 tmp.put(ln ~ "\r\n"); 397 398 } while( true); 399 400 return deserialize!T( tmp.data ); 401 } 402 403 } 404 405 /** 406 * connect to a memcached server or server cluster 407 * 408 * 409 */ 410 MemcachedClient memcachedConnect(string hostString){ 411 if( hostString.indexOf(',') >=0 ) { 412 auto hosts = split(hostString, ','); 413 return memcachedConnect( hosts); 414 } 415 return new MemcachedClient( MemcachedServer(hostString) ); 416 } 417 418 /** 419 * connect to a memcached cluster 420 * 421 * 422 */ 423 MemcachedClusterClient memcachedConnect( string[] hostStrings ){ 424 MemcachedServers servers; 425 foreach(host; hostStrings){ 426 servers ~= MemcachedServer(host); 427 } 428 return new MemcachedClusterClient(servers); 429 } 430