]> git.saurik.com Git - redis.git/blobdiff - src/ziplist.c
Replace ziplist stresser and fix regression
[redis.git] / src / ziplist.c
index a6383517d008152b1c591ea9dfedde38a47c085d..98ad3113e80dd89f4e74d4d8c7f21b0f2503f23a 100644 (file)
@@ -144,7 +144,7 @@ static unsigned int zipDecodeLength(unsigned char *p, unsigned int *lensize) {
             if (lensize) *lensize = 1;
             break;
         case ZIP_STR_14B:
-            len = ((p[0] & 0x3f) << 6) | p[1];
+            len = ((p[0] & 0x3f) << 8) | p[1];
             if (lensize) *lensize = 2;
             break;
         case ZIP_STR_32B:
@@ -226,6 +226,14 @@ static unsigned int zipPrevEncodeLength(unsigned char *p, unsigned int len) {
     }
 }
 
+/* Encode the length of the previous entry and write it to "p". This only
+ * uses the larger encoding (required in __ziplistCascadeUpdate). */
+static void zipPrevEncodeLengthForceLarge(unsigned char *p, unsigned int len) {
+    if (p == NULL) return;
+    p[0] = ZIP_BIGLEN;
+    memcpy(p+1,&len,sizeof(len));
+}
+
 /* Return the difference in number of bytes needed to store the new length
  * "len" on the entry pointed to by "p". */
 static int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
@@ -344,11 +352,87 @@ static unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
     return zl;
 }
 
+/* When an entry is inserted, we need to set the prevlen field of the next
+ * entry to equal the length of the inserted entry. It can occur that this
+ * length cannot be encoded in 1 byte and the next entry needs to be grow
+ * a bit larger to hold the 5-byte encoded prevlen. This can be done for free,
+ * because this only happens when an entry is already being inserted (which
+ * causes a realloc and memmove). However, encoding the prevlen may require
+ * that this entry is grown as well. This effect may cascade throughout
+ * the ziplist when there are consecutive entries with a size close to
+ * ZIP_BIGLEN, so we need to check that the prevlen can be encoded in every
+ * consecutive entry.
+ *
+ * Note that this effect can also happen in reverse, where the bytes required
+ * to encode the prevlen field can shrink. This effect is deliberately ignored,
+ * because it can cause a "flapping" effect where a chain prevlen fields is
+ * first grown and then shrunk again after consecutive inserts. Rather, the
+ * field is allowed to stay larger than necessary, because a large prevlen
+ * field implies the ziplist is holding large entries anyway.
+ *
+ * The pointer "p" points to the first entry that does NOT need to be
+ * updated, i.e. consecutive fields MAY need an update. */
+static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
+    unsigned int curlen = ZIPLIST_BYTES(zl), rawlen, rawlensize;
+    unsigned int offset, noffset, extra;
+    unsigned char *np;
+    zlentry cur, next;
+
+    while (p[0] != ZIP_END) {
+        cur = zipEntry(p);
+        rawlen = cur.headersize + cur.len;
+        rawlensize = zipPrevEncodeLength(NULL,rawlen);
+
+        /* Abort if there is no next entry. */
+        if (p[rawlen] == ZIP_END) break;
+        next = zipEntry(p+rawlen);
+
+        /* Abort when "prevlen" has not changed. */
+        if (next.prevrawlen == rawlen) break;
+
+        if (next.prevrawlensize < rawlensize) {
+            /* The "prevlen" field of "next" needs more bytes to hold
+             * the raw length of "cur". */
+            offset = p-zl;
+            extra = rawlensize-next.prevrawlensize;
+            zl = ziplistResize(zl,curlen+extra);
+            ZIPLIST_TAIL_OFFSET(zl) += extra;
+            p = zl+offset;
+
+            /* Move the tail to the back. */
+            np = p+rawlen;
+            noffset = np-zl;
+            memmove(np+rawlensize,
+                np+next.prevrawlensize,
+                curlen-noffset-next.prevrawlensize-1);
+            zipPrevEncodeLength(np,rawlen);
+
+            /* Advance the cursor */
+            p += rawlen;
+            curlen += extra;
+        } else {
+            if (next.prevrawlensize > rawlensize) {
+                /* This would result in shrinking, which we want to avoid.
+                 * So, set "rawlen" in the available bytes. */
+                zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
+            } else {
+                zipPrevEncodeLength(p+rawlen,rawlen);
+            }
+
+            /* Stop here, as the raw length of "next" has not changed. */
+            break;
+        }
+    }
+    return zl;
+}
+
 /* Delete "num" entries, starting at "p". Returns pointer to the ziplist. */
 static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
     unsigned int i, totlen, deleted = 0;
-    int nextdiff = 0;
-    zlentry first = zipEntry(p);
+    int offset, nextdiff = 0;
+    zlentry first, tail;
+
+    first = zipEntry(p);
     for (i = 0; p[0] != ZIP_END && i < num; i++) {
         p += zipRawEntryLength(p);
         deleted++;
@@ -365,7 +449,14 @@ static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsig
             zipPrevEncodeLength(p-nextdiff,first.prevrawlen);
 
             /* Update offset for tail */
-            ZIPLIST_TAIL_OFFSET(zl) -= totlen+nextdiff;
+            ZIPLIST_TAIL_OFFSET(zl) -= totlen;
+
+            /* When the tail contains more than one entry, we need to take
+             * "nextdiff" in account as well. Otherwise, a change in the
+             * size of prevlen doesn't have an effect on the *tail* offset. */
+            tail = zipEntry(p);
+            if (p[tail.headersize+tail.len] != ZIP_END)
+                ZIPLIST_TAIL_OFFSET(zl) += nextdiff;
 
             /* Move tail to the front of the ziplist */
             memmove(first.p,p-nextdiff,ZIPLIST_BYTES(zl)-(p-zl)-1+nextdiff);
@@ -375,8 +466,15 @@ static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsig
         }
 
         /* Resize and update length */
+        offset = first.p-zl;
         zl = ziplistResize(zl, ZIPLIST_BYTES(zl)-totlen+nextdiff);
         ZIPLIST_INCR_LENGTH(zl,-deleted);
+        p = zl+offset;
+
+        /* When nextdiff != 0, the raw length of the next entry has changed, so
+         * we need to cascade the update throughout the ziplist */
+        if (nextdiff != 0)
+            zl = __ziplistCascadeUpdate(zl,p);
     }
     return zl;
 }
@@ -385,19 +483,18 @@ static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsig
 static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
     unsigned int curlen = ZIPLIST_BYTES(zl), reqlen, prevlen = 0;
     unsigned int offset, nextdiff = 0;
-    unsigned char *tail;
     unsigned char encoding = 0;
     long long value;
-    zlentry entry;
+    zlentry entry, tail;
 
     /* Find out prevlen for the entry that is inserted. */
     if (p[0] != ZIP_END) {
         entry = zipEntry(p);
         prevlen = entry.prevrawlen;
     } else {
-        tail = ZIPLIST_ENTRY_TAIL(zl);
-        if (tail[0] != ZIP_END) {
-            prevlen = zipRawEntryLength(tail);
+        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
+        if (ptail[0] != ZIP_END) {
+            prevlen = zipRawEntryLength(ptail);
         }
     }
 
@@ -429,15 +526,32 @@ static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsig
     if (p[0] != ZIP_END) {
         /* Subtract one because of the ZIP_END bytes */
         memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
+
         /* Encode this entry's raw length in the next entry. */
         zipPrevEncodeLength(p+reqlen,reqlen);
+
         /* Update offset for tail */
-        ZIPLIST_TAIL_OFFSET(zl) += reqlen+nextdiff;
+        ZIPLIST_TAIL_OFFSET(zl) += reqlen;
+
+        /* When the tail contains more than one entry, we need to take
+         * "nextdiff" in account as well. Otherwise, a change in the
+         * size of prevlen doesn't have an effect on the *tail* offset. */
+        tail = zipEntry(p+reqlen);
+        if (p[reqlen+tail.headersize+tail.len] != ZIP_END)
+            ZIPLIST_TAIL_OFFSET(zl) += nextdiff;
     } else {
         /* This element will be the new tail. */
         ZIPLIST_TAIL_OFFSET(zl) = p-zl;
     }
 
+    /* When nextdiff != 0, the raw length of the next entry has changed, so
+     * we need to cascade the update throughout the ziplist */
+    if (nextdiff != 0) {
+        offset = p-zl;
+        zl = __ziplistCascadeUpdate(zl,p+reqlen);
+        p = zl+offset;
+    }
+
     /* Write the entry */
     p += zipPrevEncodeLength(p,prevlen);
     p += zipEncodeLength(p,encoding,slen);
@@ -510,6 +624,7 @@ unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p) {
         return NULL;
     } else {
         entry = zipEntry(p);
+        assert(entry.prevrawlen > 0);
         return p-entry.prevrawlen;
     }
 }
@@ -615,27 +730,62 @@ unsigned int ziplistSize(unsigned char *zl) {
 
 void ziplistRepr(unsigned char *zl) {
     unsigned char *p;
+    int index = 0;
     zlentry entry;
 
-    printf("{total bytes %d} {length %u}\n",ZIPLIST_BYTES(zl), ZIPLIST_LENGTH(zl));
+    printf(
+        "{total bytes %d} "
+        "{length %u}\n"
+        "{tail offset %u}\n",
+        ZIPLIST_BYTES(zl),
+        ZIPLIST_LENGTH(zl),
+        ZIPLIST_TAIL_OFFSET(zl));
     p = ZIPLIST_ENTRY_HEAD(zl);
     while(*p != ZIP_END) {
         entry = zipEntry(p);
-        printf("{offset %ld, header %u, payload %u} ",p-zl,entry.headersize,entry.len);
+        printf(
+            "{"
+                "addr 0x%08lx, "
+                "index %2d, "
+                "offset %5ld, "
+                "rl: %5u, "
+                "hs %2u, "
+                "pl: %5u, "
+                "pls: %2u, "
+                "payload %5u"
+            "} ",
+            (long unsigned int)p,
+            index,
+            p-zl,
+            entry.headersize+entry.len,
+            entry.headersize,
+            entry.prevrawlen,
+            entry.prevrawlensize,
+            entry.len);
         p += entry.headersize;
         if (ZIP_IS_STR(entry.encoding)) {
-            fwrite(p,entry.len,1,stdout);
+            if (entry.len > 40) {
+                fwrite(p,40,1,stdout);
+                printf("...");
+            } else {
+                fwrite(p,entry.len,1,stdout);
+            }
         } else {
             printf("%lld", (long long) zipLoadInteger(p,entry.encoding));
         }
         printf("\n");
         p += entry.len;
+        index++;
     }
     printf("{end}\n\n");
 }
 
 #ifdef ZIPLIST_TEST_MAIN
 #include <sys/time.h>
+#include "adlist.h"
+#include "sds.h"
+
+#define debug(f, ...) { if (DEBUG) printf(f, __VA_ARGS__); }
 
 unsigned char *createList() {
     unsigned char *zl = ziplistNew();
@@ -719,12 +869,42 @@ void pop(unsigned char *zl, int where) {
     }
 }
 
+void randstring(char *target, unsigned int min, unsigned int max) {
+    int p, len = min+rand()%(max-min+1);
+    int minval, maxval;
+    switch(rand() % 3) {
+    case 0:
+        minval = 0;
+        maxval = 255;
+    break;
+    case 1:
+        minval = 48;
+        maxval = 122;
+    break;
+    case 2:
+        minval = 48;
+        maxval = 52;
+    break;
+    default:
+        assert(NULL);
+    }
+
+    while(p < len)
+        target[p++] = minval+rand()%(maxval-minval+1);
+    return;
+}
+
+
 int main(int argc, char **argv) {
     unsigned char *zl, *p;
     unsigned char *entry;
     unsigned int elen;
     long long value;
 
+    /* If an argument is given, use it as the random seed. */
+    if (argc == 2)
+        srand(atoi(argv[1]));
+
     zl = createIntList();
     ziplistRepr(zl);
 
@@ -976,6 +1156,25 @@ int main(int argc, char **argv) {
         ziplistRepr(zl);
     }
 
+    printf("Regression test for >255 byte strings:\n");
+    {
+        char v1[257],v2[257];
+        memset(v1,'x',256);
+        memset(v2,'y',256);
+        zl = ziplistNew();
+        zl = ziplistPush(zl,(unsigned char*)v1,strlen(v1),ZIPLIST_TAIL);
+        zl = ziplistPush(zl,(unsigned char*)v2,strlen(v2),ZIPLIST_TAIL);
+
+        /* Pop values again and compare their value. */
+        p = ziplistIndex(zl,0);
+        assert(ziplistGet(p,&entry,&elen,&value));
+        assert(strncmp(v1,(char*)entry,elen) == 0);
+        p = ziplistIndex(zl,1);
+        assert(ziplistGet(p,&entry,&elen,&value));
+        assert(strncmp(v2,(char*)entry,elen) == 0);
+        printf("SUCCESS\n\n");
+    }
+
     printf("Create long list and check indices:\n");
     {
         zl = ziplistNew();
@@ -1019,7 +1218,85 @@ int main(int argc, char **argv) {
             printf("ERROR: \"1025\"\n");
             return 1;
         }
-        printf("SUCCESS\n");
+        printf("SUCCESS\n\n");
+    }
+
+    printf("Stress with random payloads of different encoding:\n");
+    {
+        int i,j,len,where;
+        unsigned char *p;
+        char buf[1024];
+        list *ref;
+        listNode *refnode;
+
+        /* Hold temp vars from ziplist */
+        unsigned char *sstr;
+        unsigned int slen;
+        long long sval;
+
+        /* In the regression for the cascade bug, it was triggered
+         * with a random seed of 2. */
+        srand(2);
+
+        for (i = 0; i < 20000; i++) {
+            zl = ziplistNew();
+            ref = listCreate();
+            listSetFreeMethod(ref,sdsfree);
+            len = rand() % 256;
+
+            /* Create lists */
+            for (j = 0; j < len; j++) {
+                where = (rand() & 1) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
+                switch(rand() % 4) {
+                case 0:
+                    sprintf(buf,"%lld",(0LL + rand()) >> 20);
+                    break;
+                case 1:
+                    sprintf(buf,"%lld",(0LL + rand()));
+                    break;
+                case 2:
+                    sprintf(buf,"%lld",(0LL + rand()) << 20);
+                    break;
+                case 3:
+                    randstring(buf,0,256);
+                break;
+                default:
+                    assert(NULL);
+                }
+
+                /* Add to ziplist */
+                zl = ziplistPush(zl, (unsigned char*)buf, strlen(buf), where);
+
+                /* Add to reference list */
+                if (where == ZIPLIST_HEAD) {
+                    listAddNodeHead(ref,sdsnew(buf));
+                } else if (where == ZIPLIST_TAIL) {
+                    listAddNodeTail(ref,sdsnew(buf));
+                } else {
+                    assert(NULL);
+                }
+            }
+
+            assert(listLength(ref) == ziplistLen(zl));
+            for (j = 0; j < len; j++) {
+                /* Naive way to get elements, but similar to the stresser
+                 * executed from the Tcl test suite. */
+                p = ziplistIndex(zl,j);
+                refnode = listIndex(ref,j);
+
+                assert(ziplistGet(p,&sstr,&slen,&sval));
+                if (sstr == NULL) {
+                    sprintf(buf,"%lld",sval);
+                } else {
+                    memcpy(buf,sstr,slen);
+                    buf[slen] = '\0';
+                }
+                assert(strcmp(buf,listNodeValue(refnode)) == 0);
+            }
+            zfree(zl);
+            listRelease(ref);
+        }
+        printf("SUCCESS\n\n");
     }
 
     printf("Stress with variable ziplist size:\n");