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