memcached4d.d 11 KB

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