New feature: refresh [patch]...

Sean Chittenden sean at chittenden.org
Wed Nov 24 12:15:28 PST 2004


Howdy.  Sessions, the bane of most web application developers' 
existence.  How to tell when they expire and how many of them happen in 
a day.  Most people use log analysis to figure this out in the form of 
IP tracking, but this falls apart with proxies, nevermind that it 
happens only once a day.  One could log every hit in a database... but 
we won't go there, that's just atrociously slow and doesn't scale.   
memcached nearly solves this in its current form, but it uses an 
absolute expiration (which is what's needed for nearly everything but 
sessions).

That said, I've added a new command to memcached(8) called "refresh".  
refresh behaves *exactly* the same as get, except that it refreshes the 
expiration of the item based off of the original expiration time 
provided by the client.  For example:

> % telnet localhost 11211
> add ses 0 10 1
> 1
> STORED
> refresh ses
> VALUE ses 0 1
> 1
> END
> [wait 9 seconds]
> refresh ses
> VALUE ses 0 1
> 1
> END
> [wait 9 seconds]
> refresh ses
> VALUE ses 0 1
> 1
> END
> [etc...]
> quit

On cache lookup failure, assuming that memcached hasn't pushed the 
session out, the client can now log a new session.  It's not perfect, 
but it's infinitely better than the current state of affairs.  I'm sure 
there are possibly other uses for this beyond sessions, but that's what 
I had in mind when I thumped this addition out.  If someone has a 
better name, please let me know.  :)  I have updated the C api to 
include this new functionality, but haven't posted it yet because this 
hasn't been accepted into the tree yet.  If someone wants the updated C 
API, please let me know offlist.

There is a small problem with the stats (I think memory is being 
clobbered somewhere... stack problem?).  It looks like the stats 
structure is getting stomped on someplace, but I haven't figured out 
where yet.  :(  Operationally, this works without a hitch and I've 
benched the hell out of it without incident.  It just looks like a 
stats problem to me, though I haven't figured out how or why.  Here's a 
transcript:

> % telnet localhost 11211
> stats
> STAT pid 6233
> STAT uptime 6
> STAT time 1101325249
> STAT version 1.1.11
> STAT rusage_user 0.010000
> STAT rusage_system 0.030000
> STAT curr_items 0
> STAT total_items 0
> STAT bytes 0
> STAT curr_connections 1
> STAT total_connections 2
> STAT connection_structures 2

Does it preallocate a connection structure?  There is only one 
connection to the backend at the moment.  Just an oddity that's always 
caught my attention.

> STAT cmd_get 0
> STAT cmd_refresh 0
> STAT cmd_set 0
> STAT get_hits 0
> STAT get_misses 0
> STAT refresh_hits 0
> STAT refresh_misses 0
> STAT bytes_read 7

"stats\n" should only be six bytes, not seven.  Something seems fishy 
here... use of sizeof being mixed up with strlen(3)?

> STAT bytes_written 0
> STAT limit_maxbytes 67108864
> END
> add foo 0 10 1
> 1
> STORED
> get foo
> VALUE foo 0 1
> 1
> END
> stats
> STAT pid 6233
> STAT uptime 18
> STAT time 1101325261
> STAT version 1.1.11
> STAT rusage_user 0.020000
> STAT rusage_system 0.040000
> STAT curr_items 1
> STAT total_items 1
> STAT bytes 43
> STAT curr_connections 1
> STAT total_connections 2
> STAT connection_structures 2
> STAT cmd_get 1
> STAT cmd_refresh 0
> STAT cmd_set 1
> STAT get_hits 1
> STAT get_misses 0
> STAT refresh_hits 0
> STAT refresh_misses 0
> STAT bytes_read 42
> STAT bytes_written 502
> STAT limit_maxbytes 67108864
> END

... so far so good...

> refresh foo
> VALUE foo 0 1
> 1
> END
> stats
> STAT pid 6233
> STAT uptime 26
> STAT time 1101325269
> STAT version 1.1.11
> STAT rusage_user 0.020000
> STAT rusage_system 0.040000
> STAT curr_items 1
> STAT total_items 1
> STAT bytes 43
> STAT curr_connections 1
> STAT total_connections 2
> STAT connection_structures 2
> STAT cmd_get 1
> STAT cmd_refresh 2

???  That's not right... only one refresh command has been issued.

> STAT cmd_set 1
> STAT get_hits 1
> STAT get_misses 0
> STAT refresh_hits 1
> STAT refresh_misses 1

And there haven't been any cache misses.  :(

> STAT bytes_read 62
> STAT bytes_written 1001
> STAT limit_maxbytes 67108864
> END
> quit

But if you look at the patch, it's stupid simple and 1:1 emulates how 
"get" updates its stats.  I'm hard pressed to think it's a bug in my 
patch, but it very likely could be.  I just don't see how it's 
possible.  Anyway, here's the patch.  Feedback is very welcome.  I 
threw in a few whitespace cleanups along the way.  cvs di -b is 
probably going to be a bit more useful than cvs di to see the actual 
changes w/o whitespace foo clogging up the unified output.  If you add:

;; (setq font-lock-mode-maximum-decoration t)
;; (require 'font-lock)
(if (>= emacs-major-version 21)
         (setq-default show-trailing-whitespace t))

to your ~/.emacs file and you'll see what I was cleaning up.  -sc

-------------- next part --------------
Index: memcached.h
===================================================================
RCS file: /home/cvspub/wcmtools/memcached/memcached.h,v
retrieving revision 1.21
diff -u -b -r1.21 memcached.h
--- memcached.h	24 Feb 2004 23:42:02 -0000	1.21
+++ memcached.h	24 Nov 2004 20:11:58 -0000
@@ -15,9 +15,12 @@
     unsigned int  total_conns;
     unsigned int  conn_structs;
     unsigned int  get_cmds;
+    unsigned int  refresh_cmds;
     unsigned int  set_cmds;
     unsigned int  get_hits;
     unsigned int  get_misses;
+    unsigned int  refresh_hits;
+    unsigned int  refresh_misses;
     time_t        started;          /* when the process was started */
     unsigned long long bytes_read;
     unsigned long long bytes_written;
@@ -51,6 +54,7 @@
     int    nbytes;  /* size of data */
     time_t time;    /* least recent access */
     time_t exptime; /* expire time */
+    time_t expire; /* Original expiration from client: used by refresh */
     unsigned char it_flags;     /* ITEM_* above */
     unsigned char slabs_clsid;
     unsigned char nkey;         /* key length, with terminating null and padding */
@@ -77,6 +81,8 @@
 #define NREAD_ADD 1
 #define NREAD_SET 2
 #define NREAD_REPLACE 3
+#define NREAD_GET		4
+#define NREAD_REFRESH	5
 
 typedef struct {
     int    sfd;
Index: memcached.c
===================================================================
RCS file: /home/cvspub/wcmtools/memcached/memcached.c,v
retrieving revision 1.52
diff -u -b -r1.52 memcached.c
--- memcached.c	23 Nov 2004 12:25:40 -0000	1.52
+++ memcached.c	24 Nov 2004 20:11:58 -0000
@@ -73,15 +73,14 @@
 }
 
 void stats_init(void) {
-    stats.curr_items = stats.total_items = stats.curr_conns = stats.total_conns = stats.conn_structs = 0;
-    stats.get_cmds = stats.set_cmds = stats.get_hits = stats.get_misses = 0;
-    stats.curr_bytes = stats.bytes_read = stats.bytes_written = 0;
+    memset(&stats, 0, sizeof(struct stats));
     stats.started = time(0);
 }
 
 void stats_reset(void) {
     stats.total_items = stats.total_conns = 0;
-    stats.get_cmds = stats.set_cmds = stats.get_hits = stats.get_misses = 0;
+    stats.get_cmds = stats.refresh_cmds = stats.set_cmds = stats.get_hits =
+        stats.refresh_hits = stats.get_misses = stats.refresh_misses = 0;
     stats.bytes_read = stats.bytes_written = 0;
 }
 
@@ -338,10 +337,13 @@
         pos += sprintf(pos, "STAT curr_connections %u\r\n", stats.curr_conns - 1); /* ignore listening conn */
         pos += sprintf(pos, "STAT total_connections %u\r\n", stats.total_conns);
         pos += sprintf(pos, "STAT connection_structures %u\r\n", stats.conn_structs);
-        pos += sprintf(pos, "STAT cmd_get %u\r\n", stats.get_cmds);
         pos += sprintf(pos, "STAT cmd_set %u\r\n", stats.set_cmds);
+        pos += sprintf(pos, "STAT cmd_get %u\r\n", stats.get_cmds);
         pos += sprintf(pos, "STAT get_hits %u\r\n", stats.get_hits);
         pos += sprintf(pos, "STAT get_misses %u\r\n", stats.get_misses);
+        pos += sprintf(pos, "STAT cmd_refresh %u\r\n", stats.refresh_cmds);
+        pos += sprintf(pos, "STAT refresh_hits %u\r\n", stats.refresh_hits);
+        pos += sprintf(pos, "STAT refresh_misses %u\r\n", stats.refresh_misses);
         pos += sprintf(pos, "STAT bytes_read %llu\r\n", stats.bytes_read);
         pos += sprintf(pos, "STAT bytes_written %llu\r\n", stats.bytes_written);
         pos += sprintf(pos, "STAT limit_maxbytes %u\r\n", settings.maxbytes);
@@ -516,7 +518,6 @@
             out_string(c, "CLIENT_ERROR bad command line format");
             return;
         }
-        expire = realtime(expire);
         it = item_alloc(key, flags, expire, len+2);
         if (it == 0) {
             out_string(c, "SERVER_ERROR out of memory");
@@ -597,7 +598,8 @@
         return;
     }
         
-    if (strncmp(command, "get ", 4) == 0) {
+    if ((strncmp(command, "get ", 4) == 0 && (comm = NREAD_GET)) ||
+        (strncmp(command, "refresh ", 8) == 0 && (comm = NREAD_REFRESH))) {
 
         char *start = command + 4;
         char key[251];
@@ -608,7 +610,14 @@
 
         while(sscanf(start, " %250s%n", key, &next) >= 1) {
             start+=next;
+            switch (comm) {
+            case NREAD_GET:
             stats.get_cmds++;
+                break;
+            case NREAD_REFRESH:
+                stats.refresh_cmds++;
+                break;
+            }
             it = assoc_find(key);
             if (it && (it->it_flags & ITEM_DELETED)) {
                 it = 0;
@@ -630,12 +639,30 @@
                         c->ilist = new_list;
                     } else break;
                 }
+
+                item_update(it);
+                switch (comm) {
+                case NREAD_GET:
                 stats.get_hits++;
+                    break;
+                case NREAD_REFRESH:
+                    it->exptime = realtime(it->expire);
+                    stats.refresh_hits++;
+                    break;
+                }
                 it->refcount++;
-                item_update(it);
                 *(c->ilist + i) = it;
                 i++;
-            } else stats.get_misses++;
+            } else {
+                switch (comm) {
+                case NREAD_GET:
+                    stats.get_misses++;
+                    break;
+                case NREAD_REFRESH:
+                    stats.refresh_misses++;
+                    break;
+                }
+            }
         }
         c->icurr = c->ilist;
         c->ileft = i;
Index: items.c
===================================================================
RCS file: /home/cvspub/wcmtools/memcached/items.c,v
retrieving revision 1.23
diff -u -b -r1.23 items.c
--- items.c	13 Sep 2004 22:31:53 -0000	1.23
+++ items.c	24 Nov 2004 20:11:58 -0000
@@ -96,7 +96,8 @@
     it->nkey = len;
     it->nbytes = nbytes;
     strcpy(ITEM_key(it), key);
-    it->exptime = exptime;
+    it->expire = exptime;
+    it->exptime = realtime(exptime);
     it->flags = flags;
     return it;
 }
Index: doc/protocol.txt
===================================================================
RCS file: /home/cvspub/wcmtools/memcached/doc/protocol.txt,v
retrieving revision 1.3
diff -u -b -r1.3 protocol.txt
--- doc/protocol.txt	1 Dec 2003 21:43:35 -0000	1.3
+++ doc/protocol.txt	24 Nov 2004 20:11:58 -0000
@@ -177,13 +177,19 @@
 
 The retrieval command looks like this:
 
-get <key>*\r\n
+<cmd> <key>*\r\n
+
+- <cmd> can be either "get" or "refresh"
 
 - <key>* means one or more key strings separated by whitespace.
 
 After this command, the client expects zero or more items, each of
-which is received as a text line followed by a data block. After all
-the items have been transmitted, the server sends the string
+which is received as a text line followed by a data block.  Unlike the
+"get" <cmd>, "refresh" resets the expiration time to be now + whatever
+the expiration time was set to (ex: assuming memcached(8) is not
+memory constrained, a cache lookup failure means a session expired or
+doesn't exist).  After all the items have been transmitted, the server
+sends the string:
 
 "END\r\n"
 
@@ -224,9 +230,10 @@
   the client wishes the server to refuse "add" and "replace" commands
   with this key. For this amount of item, the item is put into a
   delete queue, which means that it won't possible to retrieve it by
-  the "get" command, but "add" and "replace" command with this key
-  will also fail (the "set" command will succeed, however). After the
-  time passes, the item is finally deleted from server memory.
+  the "get" or "refresh" command, but "add" and "replace" command with
+  this key will also fail (the "set" command will succeed,
+  however). After the time passes, the item is finally deleted from
+  server memory.
 
   The parameter <time> is optional, and, if absent, defaults to 0
   (which means that the item will be deleted immediately and further
@@ -341,12 +348,17 @@
                            the server started running
 connection_structures 32u  Number of connection structures allocated 
                            by the server
-cmd_get           32u      Cumulative number of retrieval requests
+cmd_get           32u      Cumulative number of "get" requests
+cmd_refresh       32u      Cumulative number of "refresh" requests
 cmd_set           32u      Cumulative number of storage requests
 get_hits          32u      Number of keys that have been requested and 
-                           found present
+                           found present with the "get" command
 get_misses        32u      Number of items that have been requested 
-                           and not found
+                           and not found with "get" command
+refresh_hits      32u      Number of keys that have been requested and
+                           found present with the "refresh" command
+refresh_misses    32u      Number of items that have been requested
+                           and not found with the "refresh" command
 bytes_read        64u      Total number of bytes read by this server 
                            from network
 bytes_written     64u      Total number of bytes sent by this server to 
-------------- next part --------------


-- 
Sean Chittenden


More information about the memcached mailing list