X-Git-Url: https://git.saurik.com/redis.git/blobdiff_plain/054e426dbd7f5d4788c68fca653e1d4819adeec0..59146ef3c107d59b6522b8a2f5539a55703adee1:/redis.c diff --git a/redis.c b/redis.c index fd912b49..5da103d8 100644 --- a/redis.c +++ b/redis.c @@ -27,7 +27,7 @@ * POSSIBILITY OF SUCH DAMAGE. */ -#define REDIS_VERSION "1.3.2" +#define REDIS_VERSION "1.3.3" #include "fmacros.h" #include "config.h" @@ -75,11 +75,6 @@ #include "lzf.h" /* LZF compression library */ #include "pqsort.h" /* Partial qsort for SORT+LIMIT */ -/* #define REDIS_HELGRIND_FRIENDLY */ -#if defined(__GNUC__) && defined(REDIS_HELGRIND_FRIENDLY) -#warning "Remember to undef REDIS_HELGRIND_FRIENDLY before to commit" -#endif - /* Error codes */ #define REDIS_OK 0 #define REDIS_ERR -1 @@ -170,21 +165,19 @@ #define REDIS_VM_MAX_RANDOM_JUMP 4096 #define REDIS_VM_MAX_THREADS 32 #define REDIS_THREAD_STACK_SIZE (1024*1024*4) -/* The following is the number of completed I/O jobs to process when the - * handelr is called. 1 is the minimum, and also the default, as it allows - * to block as little as possible other accessing clients. While Virtual - * Memory I/O operations are performed by threads, this operations must - * be processed by the main thread when completed to take effect. */ +/* The following is the *percentage* of completed I/O jobs to process when the + * handelr is called. While Virtual Memory I/O operations are performed by + * threads, this operations must be processed by the main thread when completed + * in order to take effect. */ #define REDIS_MAX_COMPLETED_JOBS_PROCESSED 1 /* Client flags */ -#define REDIS_CLOSE 1 /* This client connection should be closed ASAP */ -#define REDIS_SLAVE 2 /* This client is a slave server */ -#define REDIS_MASTER 4 /* This client is a master server */ -#define REDIS_MONITOR 8 /* This client is a slave monitor, see MONITOR */ -#define REDIS_MULTI 16 /* This client is in a MULTI context */ -#define REDIS_BLOCKED 32 /* The client is waiting in a blocking operation */ -#define REDIS_IO_WAIT 64 /* The client is waiting for Virtual Memory I/O */ +#define REDIS_SLAVE 1 /* This client is a slave server */ +#define REDIS_MASTER 2 /* This client is a master server */ +#define REDIS_MONITOR 4 /* This client is a slave monitor, see MONITOR */ +#define REDIS_MULTI 8 /* This client is in a MULTI context */ +#define REDIS_BLOCKED 16 /* The client is waiting in a blocking operation */ +#define REDIS_IO_WAIT 32 /* The client is waiting for Virtual Memory I/O */ /* Slave replication state - slave side */ #define REDIS_REPL_NONE 0 /* No active replication */ @@ -228,7 +221,7 @@ #define APPENDFSYNC_EVERYSEC 2 /* We can print the stacktrace, so our assert is defined this way: */ -#define redisAssert(_e) ((_e)?(void)0 : (_redisAssert(#_e,__FILE__,__LINE__),exit(1))) +#define redisAssert(_e) ((_e)?(void)0 : (_redisAssert(#_e,__FILE__,__LINE__),_exit(1))) static void _redisAssert(char *estr, char *file, int line); /*================================= Data types ============================== */ @@ -275,6 +268,7 @@ typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ dict *blockingkeys; /* Keys with clients waiting for data (BLPOP) */ + dict *io_keys; /* Keys with clients waiting for VM I/O */ int id; } redisDb; @@ -304,8 +298,7 @@ typedef struct redisClient { list *reply; int sentlen; time_t lastinteraction; /* time of the last interaction, used for timeout */ - int flags; /* REDIS_CLOSE | REDIS_SLAVE | REDIS_MONITOR */ - /* REDIS_MULTI */ + int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */ int slaveseldb; /* slave selected db, if this client is a slave */ int authenticated; /* when requirepass is non-NULL */ int replstate; /* replication state if this is a slave */ @@ -313,7 +306,7 @@ typedef struct redisClient { long repldboff; /* replication DB file offset */ off_t repldbsize; /* replication DB file size */ multiState mstate; /* MULTI/EXEC state */ - robj **blockingkeys; /* The key we waiting to terminate a blocking + robj **blockingkeys; /* The key we are waiting to terminate a blocking * operation such as BLPOP. Otherwise NULL. */ int blockingkeysnum; /* Number of blocking keys */ time_t blockingto; /* Blocking operation timeout. If UNIX current time @@ -342,7 +335,6 @@ struct redisServer { int cronloops; /* number of times the cron function run */ list *objfreelist; /* A list of freed objects to avoid malloc() */ time_t lastsave; /* Unix time of last save succeeede */ - size_t usedmemory; /* Used memory in megabytes */ /* Fields used only for stats */ time_t stat_starttime; /* server start time */ long long stat_numcommands; /* number of processed commands */ @@ -380,7 +372,8 @@ struct redisServer { int replstate; unsigned int maxclients; unsigned long long maxmemory; - unsigned int blockedclients; + unsigned int blpop_blocked_clients; + unsigned int vm_blocked_clients; /* Sort parameters - qsort_r() is only available under BSD so we * have to take this state global, in order to pass it to sortCompare() */ int sort_desc; @@ -406,7 +399,7 @@ struct redisServer { list *io_newjobs; /* List of VM I/O jobs yet to be processed */ list *io_processing; /* List of VM I/O jobs being processed */ list *io_processed; /* List of VM I/O jobs already processed */ - list *io_clients; /* All the clients waiting for SWAP I/O operations */ + list *io_ready_clients; /* Clients ready to be unblocked. All keys loaded */ pthread_mutex_t io_mutex; /* lock to access io_jobs/io_done/io_thread_job */ pthread_mutex_t obj_freelist_mutex; /* safe redis objects creation/free */ pthread_mutex_t io_swapfile_mutex; /* So we can lseek + write */ @@ -494,7 +487,7 @@ static double R_Zero, R_PosInf, R_NegInf, R_Nan; #define REDIS_IOJOB_LOAD 0 /* Load from disk to memory */ #define REDIS_IOJOB_PREPARE_SWAP 1 /* Compute needed pages */ #define REDIS_IOJOB_DO_SWAP 2 /* Swap from memory to disk */ -typedef struct iojon { +typedef struct iojob { int type; /* Request type, REDIS_IOJOB_* */ redisDb *db;/* Redis database */ robj *key; /* This I/O request is about swapping this key */ @@ -549,7 +542,7 @@ static void sendReplyToClientWritev(aeEventLoop *el, int fd, void *privdata, int static void initClientMultiState(redisClient *c); static void freeClientMultiState(redisClient *c); static void queueMultiCommand(redisClient *c, struct redisCommand *cmd); -static void unblockClient(redisClient *c); +static void unblockClientWaitingData(redisClient *c); static int handleClientsWaitingListPush(redisClient *c, robj *key, robj *ele); static void vmInit(void); static void vmMarkPagesFree(off_t page, off_t count); @@ -571,6 +564,14 @@ static int vmWriteObjectOnSwap(robj *o, off_t page); static robj *vmReadObjectFromSwap(off_t page, int type); static void waitEmptyIOJobsQueue(void); static void vmReopenSwapFile(void); +static int vmFreePage(off_t page); +static int blockClientOnSwappedKeys(struct redisCommand *cmd, redisClient *c); +static int dontWaitForSwappedKey(redisClient *c, robj *key); +static void handleClientsBlockedOnSwappedKey(redisDb *db, robj *key); +static void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask); +static struct redisCommand *lookupCommand(char *name); +static void call(redisClient *c, struct redisCommand *cmd); +static void resetClient(redisClient *c); static void authCommand(redisClient *c); static void pingCommand(redisClient *c); @@ -649,6 +650,7 @@ static void multiCommand(redisClient *c); static void execCommand(redisClient *c); static void blpopCommand(redisClient *c); static void brpopCommand(redisClient *c); +static void appendCommand(redisClient *c); /*================================= Globals ================================= */ @@ -658,6 +660,7 @@ static struct redisCommand cmdTable[] = { {"get",getCommand,2,REDIS_CMD_INLINE}, {"set",setCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM}, {"setnx",setnxCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM}, + {"append",appendCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM}, {"del",delCommand,-2,REDIS_CMD_INLINE}, {"exists",existsCommand,2,REDIS_CMD_INLINE}, {"incr",incrCommand,2,REDIS_CMD_INLINE|REDIS_CMD_DENYOOM}, @@ -675,7 +678,7 @@ static struct redisCommand cmdTable[] = { {"lrange",lrangeCommand,4,REDIS_CMD_INLINE}, {"ltrim",ltrimCommand,4,REDIS_CMD_INLINE}, {"lrem",lremCommand,4,REDIS_CMD_BULK}, - {"rpoplpush",rpoplpushcommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM}, + {"rpoplpush",rpoplpushcommand,3,REDIS_CMD_INLINE|REDIS_CMD_DENYOOM}, {"sadd",saddCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM}, {"srem",sremCommand,3,REDIS_CMD_BULK}, {"smove",smoveCommand,4,REDIS_CMD_BULK}, @@ -870,7 +873,7 @@ static void redisLog(int level, const char *fmt, ...) { va_start(ap, fmt); if (level >= server.verbosity) { - char *c = ".-*"; + char *c = ".-*#"; char buf[64]; time_t now; @@ -953,10 +956,24 @@ static int dictEncObjKeyCompare(void *privdata, const void *key1, static unsigned int dictEncObjHash(const void *key) { robj *o = (robj*) key; - o = getDecodedObject(o); - unsigned int hash = dictGenHashFunction(o->ptr, sdslen((sds)o->ptr)); - decrRefCount(o); - return hash; + if (o->encoding == REDIS_ENCODING_RAW) { + return dictGenHashFunction(o->ptr, sdslen((sds)o->ptr)); + } else { + if (o->encoding == REDIS_ENCODING_INT) { + char buf[32]; + int len; + + len = snprintf(buf,32,"%ld",(long)o->ptr); + return dictGenHashFunction((unsigned char*)buf, len); + } else { + unsigned int hash; + + o = getDecodedObject(o); + hash = dictGenHashFunction(o->ptr, sdslen((sds)o->ptr)); + decrRefCount(o); + return hash; + } + } } /* Sets type and expires */ @@ -1000,7 +1017,8 @@ static dictType keyptrDictType = { }; /* Keylist hash table type has unencoded redis objects as keys and - * lists as values. It's used for blocking operations (BLPOP) */ + * lists as values. It's used for blocking operations (BLPOP) and to + * map swapped keys to a list of clients waiting for this keys to be loaded. */ static dictType keylistDictType = { dictObjHash, /* hash function */ NULL, /* key dup */ @@ -1043,7 +1061,7 @@ static void closeTimedoutClients(void) { } else if (c->flags & REDIS_BLOCKED) { if (c->blockingto != 0 && c->blockingto < now) { addReply(c,shared.nullmultibulk); - unblockClient(c); + unblockClientWaitingData(c); } } } @@ -1170,9 +1188,6 @@ static int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientD * To access a global var is faster than calling time(NULL) */ server.unixtime = time(NULL); - /* Update the global state with the amount of used memory */ - server.usedmemory = zmalloc_used_memory(); - /* Show some info about non-empty databases */ for (j = 0; j < server.dbnum; j++) { long long size, used, vkeys; @@ -1199,12 +1214,12 @@ static int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientD redisLog(REDIS_VERBOSE,"%d clients connected (%d slaves), %zu bytes in use, %d shared objects", listLength(server.clients)-listLength(server.slaves), listLength(server.slaves), - server.usedmemory, + zmalloc_used_memory(), dictSize(server.sharingpool)); } /* Close connections of timedout clients */ - if ((server.maxidletime && !(loops % 10)) || server.blockedclients) + if ((server.maxidletime && !(loops % 10)) || server.blpop_blocked_clients) closeTimedoutClients(); /* Check if a background saving or AOF rewrite in progress terminated */ @@ -1303,6 +1318,38 @@ static int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientD return 1000; } +/* This function gets called every time Redis is entering the + * main loop of the event driven library, that is, before to sleep + * for ready file descriptors. */ +static void beforeSleep(struct aeEventLoop *eventLoop) { + REDIS_NOTUSED(eventLoop); + + if (server.vm_enabled && listLength(server.io_ready_clients)) { + listIter li; + listNode *ln; + + listRewind(server.io_ready_clients,&li); + while((ln = listNext(&li))) { + redisClient *c = ln->value; + struct redisCommand *cmd; + + /* Resume the client. */ + listDelNode(server.io_ready_clients,ln); + c->flags &= (~REDIS_IO_WAIT); + server.vm_blocked_clients--; + aeCreateFileEvent(server.el, c->fd, AE_READABLE, + readQueryFromClient, c); + cmd = lookupCommand(c->argv[0]->ptr); + assert(cmd != NULL); + call(c,cmd); + resetClient(c); + /* There may be more data to process in the input buffer. */ + if (c->querybuf && sdslen(c->querybuf) > 0) + processInputBuffer(c); + } + } +} + static void createSharedObjects(void) { shared.crlf = createObject(REDIS_STRING,sdsnew("\r\n")); shared.ok = createObject(REDIS_STRING,sdsnew("+OK\r\n")); @@ -1376,7 +1423,7 @@ static void initServerConfig() { server.rdbcompression = 1; server.sharingpoolsize = 1024; server.maxclients = 0; - server.blockedclients = 0; + server.blpop_blocked_clients = 0; server.maxmemory = 0; server.vm_enabled = 0; server.vm_swap_file = zstrdup("/tmp/redis-%p.vm"); @@ -1384,6 +1431,7 @@ static void initServerConfig() { server.vm_pages = 1024*1024*100; /* 104 millions of pages */ server.vm_max_memory = 1024LL*1024*1024*1; /* 1 GB of RAM */ server.vm_max_threads = 4; + server.vm_blocked_clients = 0; resetServerSaveParams(); @@ -1434,6 +1482,8 @@ static void initServer() { server.db[j].dict = dictCreate(&hashDictType,NULL); server.db[j].expires = dictCreate(&keyptrDictType,NULL); server.db[j].blockingkeys = dictCreate(&keylistDictType,NULL); + if (server.vm_enabled) + server.db[j].io_keys = dictCreate(&keylistDictType,NULL); server.db[j].id = j; } server.cronloops = 0; @@ -1442,7 +1492,6 @@ static void initServer() { server.bgrewritebuf = sdsempty(); server.lastsave = time(NULL); server.dirty = 0; - server.usedmemory = 0; server.stat_numcommands = 0; server.stat_numconnections = 0; server.stat_starttime = time(NULL); @@ -1633,6 +1682,7 @@ static void loadServerConfig(char *filename) { err = "argument must be 'yes' or 'no'"; goto loaderr; } } else if (!strcasecmp(argv[0],"vm-swap-file") && argc == 2) { + zfree(server.vm_swap_file); server.vm_swap_file = zstrdup(argv[1]); } else if (!strcasecmp(argv[0],"vm-max-memory") && argc == 2) { server.vm_max_memory = strtoll(argv[1], NULL, 10); @@ -1676,14 +1726,14 @@ static void freeClient(redisClient *c) { listNode *ln; /* Note that if the client we are freeing is blocked into a blocking - * call, we have to set querybuf to NULL *before* to call unblockClient() - * to avoid processInputBuffer() will get called. Also it is important - * to remove the file events after this, because this call adds - * the READABLE event. */ + * call, we have to set querybuf to NULL *before* to call + * unblockClientWaitingData() to avoid processInputBuffer() will get + * called. Also it is important to remove the file events after + * this, because this call adds the READABLE event. */ sdsfree(c->querybuf); c->querybuf = NULL; if (c->flags & REDIS_BLOCKED) - unblockClient(c); + unblockClientWaitingData(c); aeDeleteFileEvent(server.el,c->fd,AE_READABLE); aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); @@ -1694,11 +1744,17 @@ static void freeClient(redisClient *c) { ln = listSearchKey(server.clients,c); redisAssert(ln != NULL); listDelNode(server.clients,ln); - /* Remove from the list of clients waiting for VM operations */ - if (server.vm_enabled && listLength(c->io_keys)) { - ln = listSearchKey(server.io_clients,c); - if (ln) listDelNode(server.io_clients,ln); - listRelease(c->io_keys); + /* Remove from the list of clients waiting for swapped keys */ + if (c->flags & REDIS_IO_WAIT && listLength(c->io_keys) == 0) { + ln = listSearchKey(server.io_ready_clients,c); + if (ln) { + listDelNode(server.io_ready_clients,ln); + server.vm_blocked_clients--; + } + } + while (server.vm_enabled && listLength(c->io_keys)) { + ln = listFirst(c->io_keys); + dontWaitForSwappedKey(c,ln->value); } listRelease(c->io_keys); /* Other cleanup */ @@ -2011,6 +2067,9 @@ static int processCommand(redisClient *c) { freeClient(c); return 0; } + + /* Now lookup the command and check ASAP about trivial error conditions + * such wrong arity, bad command name and so forth. */ cmd = lookupCommand(c->argv[0]->ptr); if (!cmd) { addReplySds(c, @@ -2031,6 +2090,7 @@ static int processCommand(redisClient *c) { resetClient(c); return 1; } else if (cmd->flags & REDIS_CMD_BULK && c->bulklen == -1) { + /* This is a bulk command, we have to read the last argument yet. */ int bulklen = atoi(c->argv[c->argc-1]->ptr); decrRefCount(c->argv[c->argc-1]); @@ -2052,6 +2112,8 @@ static int processCommand(redisClient *c) { c->argc++; c->querybuf = sdsrange(c->querybuf,c->bulklen,-1); } else { + /* Otherwise return... there is to read the last argument + * from the socket. */ return 1; } } @@ -2077,14 +2139,12 @@ static int processCommand(redisClient *c) { queueMultiCommand(c,cmd); addReply(c,shared.queued); } else { + if (server.vm_enabled && server.vm_max_threads > 0 && + blockClientOnSwappedKeys(cmd,c)) return 1; call(c,cmd); } /* Prepare the client for the next command */ - if (c->flags & REDIS_CLOSE) { - freeClient(c); - return 0; - } resetClient(c); return 1; } @@ -2502,7 +2562,8 @@ static void incrRefCount(robj *o) { static void decrRefCount(void *obj) { robj *o = obj; - /* Object is swapped out, or in the process of being loaded. */ + /* Object is a key of a swapped out value, or in the process of being + * loaded. */ if (server.vm_enabled && (o->storage == REDIS_VM_SWAPPED || o->storage == REDIS_VM_LOADING)) { @@ -2558,10 +2619,16 @@ static robj *lookupKey(redisDb *db, robj *key) { /* Update the access time of the key for the aging algorithm. */ key->vm.atime = server.unixtime; } else { + int notify = (key->storage == REDIS_VM_LOADING); + /* Our value was swapped on disk. Bring it at home. */ redisAssert(val == NULL); val = vmLoadObject(key); dictGetEntryVal(de) = val; + + /* Clients blocked by the VM subsystem may be waiting for + * this key... */ + if (notify) handleClientsBlockedOnSwappedKey(db,key); } } return val; @@ -3011,7 +3078,12 @@ static int rdbSave(char *filename) { int j; time_t now = time(NULL); - waitEmptyIOJobsQueue(); /* Otherwise other threads may fseek() the swap */ + /* Wait for I/O therads to terminate, just in case this is a + * foreground-saving, to avoid seeking the swap file descriptor at the + * same time. */ + if (server.vm_enabled) + waitEmptyIOJobsQueue(); + snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { @@ -3107,9 +3179,9 @@ static int rdbSaveBackground(char *filename) { if (server.vm_enabled) vmReopenSwapFile(); close(server.fd); if (rdbSave(filename) == REDIS_OK) { - exit(0); + _exit(0); } else { - exit(1); + _exit(1); } } else { /* Parent */ @@ -3279,6 +3351,10 @@ static robj *rdbLoadObject(int type, FILE *fp) { if ((listlen = rdbLoadLen(fp,NULL)) == REDIS_RDB_LENERR) return NULL; o = (type == REDIS_LIST) ? createListObject() : createSetObject(); + /* It's faster to expand the dict to the right size asap in order + * to avoid rehashing */ + if (type == REDIS_SET && listlen > DICT_HT_INITIAL_SIZE) + dictExpand(o->ptr,listlen); /* Load every single element of the list/set */ while(listlen--) { robj *ele; @@ -3621,6 +3697,52 @@ static void decrbyCommand(redisClient *c) { incrDecrCommand(c,-incr); } +static void appendCommand(redisClient *c) { + int retval; + size_t totlen; + robj *o; + + o = lookupKeyWrite(c->db,c->argv[1]); + if (o == NULL) { + /* Create the key */ + retval = dictAdd(c->db->dict,c->argv[1],c->argv[2]); + incrRefCount(c->argv[1]); + incrRefCount(c->argv[2]); + totlen = stringObjectLen(c->argv[2]); + } else { + dictEntry *de; + + de = dictFind(c->db->dict,c->argv[1]); + assert(de != NULL); + + o = dictGetEntryVal(de); + if (o->type != REDIS_STRING) { + addReply(c,shared.wrongtypeerr); + return; + } + /* If the object is specially encoded or shared we have to make + * a copy */ + if (o->refcount != 1 || o->encoding != REDIS_ENCODING_RAW) { + robj *decoded = getDecodedObject(o); + + o = createStringObject(decoded->ptr, sdslen(decoded->ptr)); + decrRefCount(decoded); + dictReplace(c->db->dict,c->argv[1],o); + } + /* APPEND! */ + if (c->argv[2]->encoding == REDIS_ENCODING_RAW) { + o->ptr = sdscatlen(o->ptr, + c->argv[2]->ptr, sdslen(c->argv[2]->ptr)); + } else { + o->ptr = sdscatprintf(o->ptr, "%ld", + (unsigned long) c->argv[2]->ptr); + } + totlen = sdslen(o->ptr); + } + server.dirty++; + addReplySds(c,sdscatprintf(sdsempty(),":%lu\r\n",(unsigned long)totlen)); +} + /* ========================= Type agnostic commands ========================= */ static void delCommand(redisClient *c) { @@ -5579,7 +5701,7 @@ static void bytesToHuman(char *s, unsigned long long n) { sprintf(s,"%.2fM",d); } else if (n < (1024LL*1024*1024*1024)) { d = (double)n/(1024LL*1024*1024); - sprintf(s,"%.2fM",d); + sprintf(s,"%.2fG",d); } } @@ -5592,7 +5714,7 @@ static sds genRedisInfoString(void) { int j; char hmem[64]; - bytesToHuman(hmem,server.usedmemory); + bytesToHuman(hmem,zmalloc_used_memory()); info = sdscatprintf(sdsempty(), "redis_version:%s\r\n" "arch_bits:%s\r\n" @@ -5621,8 +5743,8 @@ static sds genRedisInfoString(void) { uptime/(3600*24), listLength(server.clients)-listLength(server.slaves), listLength(server.slaves), - server.blockedclients, - server.usedmemory, + server.blpop_blocked_clients, + zmalloc_used_memory(), hmem, server.dirty, server.bgsavechildpid != -1, @@ -5659,8 +5781,8 @@ static sds genRedisInfoString(void) { "vm_stats_io_newjobs_len:%lu\r\n" "vm_stats_io_processing_len:%lu\r\n" "vm_stats_io_processed_len:%lu\r\n" - "vm_stats_io_waiting_clients:%lu\r\n" "vm_stats_io_active_threads:%lu\r\n" + "vm_stats_blocked_clients:%lu\r\n" ,(unsigned long long) server.vm_max_memory, (unsigned long long) server.vm_page_size, (unsigned long long) server.vm_pages, @@ -5671,8 +5793,8 @@ static sds genRedisInfoString(void) { (unsigned long) listLength(server.io_newjobs), (unsigned long) listLength(server.io_processing), (unsigned long) listLength(server.io_processed), - (unsigned long) listLength(server.io_clients), - (unsigned long) server.io_active_threads + (unsigned long) server.io_active_threads, + (unsigned long) server.vm_blocked_clients ); unlockThreadedIO(); } @@ -5945,11 +6067,11 @@ static void blockForKeys(redisClient *c, robj **keys, int numkeys, time_t timeou /* Mark the client as a blocked client */ c->flags |= REDIS_BLOCKED; aeDeleteFileEvent(server.el,c->fd,AE_READABLE); - server.blockedclients++; + server.blpop_blocked_clients++; } /* Unblock a client that's waiting in a blocking operation such as BLPOP */ -static void unblockClient(redisClient *c) { +static void unblockClientWaitingData(redisClient *c) { dictEntry *de; list *l; int j; @@ -5971,18 +6093,19 @@ static void unblockClient(redisClient *c) { zfree(c->blockingkeys); c->blockingkeys = NULL; c->flags &= (~REDIS_BLOCKED); - server.blockedclients--; + server.blpop_blocked_clients--; /* Ok now we are ready to get read events from socket, note that we - * can't trap errors here as it's possible that unblockClients() is + * can't trap errors here as it's possible that unblockClientWaitingDatas() is * called from freeClient() itself, and the only thing we can do * if we failed to register the READABLE event is to kill the client. * Still the following function should never fail in the real world as * we are sure the file descriptor is sane, and we exit on out of mem. */ aeCreateFileEvent(server.el, c->fd, AE_READABLE, readQueryFromClient, c); /* As a final step we want to process data if there is some command waiting - * in the input buffer. Note that this is safe even if unblockClient() - * gets called from freeClient() because freeClient() will be smart - * enough to call this function *after* c->querybuf was set to NULL. */ + * in the input buffer. Note that this is safe even if + * unblockClientWaitingData() gets called from freeClient() because + * freeClient() will be smart enough to call this function + * *after* c->querybuf was set to NULL. */ if (c->querybuf && sdslen(c->querybuf) > 0) processInputBuffer(c); } @@ -6016,7 +6139,7 @@ static int handleClientsWaitingListPush(redisClient *c, robj *key, robj *ele) { addReplyBulkLen(receiver,ele); addReply(receiver,ele); addReply(receiver,shared.crlf); - unblockClient(receiver); + unblockClientWaitingData(receiver); return 1; } @@ -6935,9 +7058,9 @@ static int rewriteAppendOnlyFileBackground(void) { close(server.fd); snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid()); if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) { - exit(0); + _exit(0); } else { - exit(1); + _exit(1); } } else { /* Parent */ @@ -7029,9 +7152,13 @@ static void vmInit(void) { expandVmSwapFilename(); redisLog(REDIS_NOTICE,"Using '%s' as swap file",server.vm_swap_file); - server.vm_fp = fopen(server.vm_swap_file,"r+b"); + if ((server.vm_fp = fopen(server.vm_swap_file,"r+b")) == NULL) { + server.vm_fp = fopen(server.vm_swap_file,"w+b"); + } if (server.vm_fp == NULL) { - redisLog(REDIS_WARNING,"Impossible to open the swap file. Exiting."); + redisLog(REDIS_WARNING, + "Impossible to open the swap file: %s. Exiting.", + strerror(errno)); exit(1); } server.vm_fd = fileno(server.vm_fp); @@ -7059,7 +7186,7 @@ static void vmInit(void) { server.io_newjobs = listCreate(); server.io_processing = listCreate(); server.io_processed = listCreate(); - server.io_clients = listCreate(); + server.io_ready_clients = listCreate(); pthread_mutex_init(&server.io_mutex,NULL); pthread_mutex_init(&server.obj_freelist_mutex,NULL); pthread_mutex_init(&server.io_swapfile_mutex,NULL); @@ -7087,6 +7214,7 @@ static void vmInit(void) { static void vmMarkPageUsed(off_t page) { off_t byte = page/8; int bit = page&7; + redisAssert(vmFreePage(page) == 1); server.vm_bitmap[byte] |= 1< 100000000) { + *((char*)-1) = 'x'; + } } /* Test if the page is free */ @@ -7156,7 +7290,6 @@ static int vmFindContiguousPages(off_t *first, off_t n) { while(offset < server.vm_pages) { off_t this = base+offset; - redisLog(REDIS_DEBUG, "THIS: %lld (%c)\n", (long long) this, vmFreePage(this) ? 'F' : 'X'); /* If we overflow, restart from page zero */ if (this >= server.vm_pages) { this -= server.vm_pages; @@ -7166,6 +7299,7 @@ static int vmFindContiguousPages(off_t *first, off_t n) { numfree = 0; } } + redisLog(REDIS_DEBUG, "THIS: %lld (%c)\n", (long long) this, vmFreePage(this) ? 'F' : 'X'); if (vmFreePage(this)) { /* This is a free page */ numfree++; @@ -7245,14 +7379,14 @@ static robj *vmReadObjectFromSwap(off_t page, int type) { if (server.vm_enabled) pthread_mutex_lock(&server.io_swapfile_mutex); if (fseeko(server.vm_fp,page*server.vm_page_size,SEEK_SET) == -1) { redisLog(REDIS_WARNING, - "Unrecoverable VM problem in vmLoadObject(): can't seek: %s", + "Unrecoverable VM problem in vmReadObjectFromSwap(): can't seek: %s", strerror(errno)); - exit(1); + _exit(1); } o = rdbLoadObject(type,server.vm_fp); if (o == NULL) { - redisLog(REDIS_WARNING, "Unrecoverable VM problem in vmLoadObject(): can't load object from swap file: %s", strerror(errno)); - exit(1); + redisLog(REDIS_WARNING, "Unrecoverable VM problem in vmReadObjectFromSwap(): can't load object from swap file: %s", strerror(errno)); + _exit(1); } if (server.vm_enabled) pthread_mutex_unlock(&server.io_swapfile_mutex); return o; @@ -7266,7 +7400,7 @@ static robj *vmReadObjectFromSwap(off_t page, int type) { static robj *vmGenericLoadObject(robj *key, int preview) { robj *val; - redisAssert(key->storage == REDIS_VM_SWAPPED); + redisAssert(key->storage == REDIS_VM_SWAPPED || key->storage == REDIS_VM_LOADING); val = vmReadObjectFromSwap(key->vm.page,key->vtype); if (!preview) { key->storage = REDIS_VM_MEMORY; @@ -7382,7 +7516,10 @@ static int vmSwapOneObject(int usethreads) { for (j = 0; j < server.dbnum; j++) { redisDb *db = server.db+j; - int maxtries = 1000; + /* Why maxtries is set to 100? + * Because this way (usually) we'll find 1 object even if just 1% - 2% + * are swappable objects */ + int maxtries = 100; if (dictSize(db->dict) == 0) continue; for (i = 0; i < 5; i++) { @@ -7473,8 +7610,9 @@ static int deleteIfSwapped(redisDb *db, robj *key) { /* =================== Virtual Memory - Threaded I/O ======================= */ static void freeIOJob(iojob *j) { - if (j->type == REDIS_IOJOB_PREPARE_SWAP || - j->type == REDIS_IOJOB_DO_SWAP) + if ((j->type == REDIS_IOJOB_PREPARE_SWAP || + j->type == REDIS_IOJOB_DO_SWAP || + j->type == REDIS_IOJOB_LOAD) && j->val != NULL) decrRefCount(j->val); decrRefCount(j->key); zfree(j); @@ -7487,8 +7625,7 @@ static void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, int mask) { char buf[1]; - int retval; - int processed = 0; + int retval, processed = 0, toprocess = -1, trytoswap = 1; REDIS_NOTUSED(el); REDIS_NOTUSED(mask); REDIS_NOTUSED(privdata); @@ -7506,6 +7643,10 @@ static void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, /* Get the processed element (the oldest one) */ lockThreadedIO(); assert(listLength(server.io_processed) != 0); + if (toprocess == -1) { + toprocess = (listLength(server.io_processed)*REDIS_MAX_COMPLETED_JOBS_PROCESSED)/100; + if (toprocess <= 0) toprocess = 1; + } ln = listFirst(server.io_processed); j = ln->value; listDelNode(server.io_processed,ln); @@ -7522,6 +7663,8 @@ static void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, assert(de != NULL); key = dictGetEntryKey(de); if (j->type == REDIS_IOJOB_LOAD) { + redisDb *db; + /* Key loaded, bring it at home */ key->storage = REDIS_VM_MEMORY; key->vm.atime = server.unixtime; @@ -7530,7 +7673,12 @@ static void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, (unsigned char*) key->ptr); server.vm_stats_swapped_objects--; server.vm_stats_swapins++; + dictGetEntryVal(de) = j->val; + incrRefCount(j->val); + db = j->db; freeIOJob(j); + /* Handle clients waiting for this key to be loaded. */ + handleClientsBlockedOnSwappedKey(db,key); } else if (j->type == REDIS_IOJOB_PREPARE_SWAP) { /* Now we know the amount of pages required to swap this object. * Let's find some space for it, and queue this task again @@ -7581,7 +7729,9 @@ static void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, freeIOJob(j); /* Put a few more swap requests in queue if we are still * out of memory */ - if (vmCanSwapOut() && zmalloc_used_memory() > server.vm_max_memory){ + if (trytoswap && vmCanSwapOut() && + zmalloc_used_memory() > server.vm_max_memory) + { int more = 1; while(more) { lockThreadedIO(); @@ -7589,12 +7739,15 @@ static void vmThreadedIOCompletedJob(aeEventLoop *el, int fd, void *privdata, (unsigned) server.vm_max_threads; unlockThreadedIO(); /* Don't waste CPU time if swappable objects are rare. */ - if (vmSwapOneObjectThreaded() == REDIS_ERR) break; + if (vmSwapOneObjectThreaded() == REDIS_ERR) { + trytoswap = 0; + break; + } } } } processed++; - if (processed == REDIS_MAX_COMPLETED_JOBS_PROCESSED) return; + if (processed == toprocess) return; } if (retval < 0 && errno != EAGAIN) { redisLog(REDIS_WARNING, @@ -7635,11 +7788,11 @@ again: if (job->canceled) continue; /* Skip this, already canceled. */ if (compareStringObjects(job->key,o) == 0) { - redisLog(REDIS_DEBUG,"*** CANCELED %p (%s) (LIST ID %d)\n", - (void*)job, (char*)o->ptr, i); + redisLog(REDIS_DEBUG,"*** CANCELED %p (%s) (type %d) (LIST ID %d)\n", + (void*)job, (char*)o->ptr, job->type, i); /* Mark the pages as free since the swap didn't happened * or happened but is now discarded. */ - if (job->type == REDIS_IOJOB_DO_SWAP) + if (i != 1 && job->type == REDIS_IOJOB_DO_SWAP) vmMarkPagesFree(job->page,job->pages); /* Cancel the job. It depends on the list the job is * living in. */ @@ -7651,23 +7804,25 @@ again: listDelNode(lists[i],ln); break; case 1: /* io_processing */ - /* Oh Shi- the thread is messing with the Job, and - * probably with the object if this is a - * PREPARE_SWAP or DO_SWAP job. Better to wait for the - * job to move into the next queue... */ - if (job->type != REDIS_IOJOB_LOAD) { - /* Yes, we try again and again until the job - * is completed. */ - unlockThreadedIO(); - /* But let's wait some time for the I/O thread - * to finish with this job. After all this condition - * should be very rare. */ - usleep(1); - goto again; - } else { - job->canceled = 1; - break; - } + /* Oh Shi- the thread is messing with the Job: + * + * Probably it's accessing the object if this is a + * PREPARE_SWAP or DO_SWAP job. + * If it's a LOAD job it may be reading from disk and + * if we don't wait for the job to terminate before to + * cancel it, maybe in a few microseconds data can be + * corrupted in this pages. So the short story is: + * + * Better to wait for the job to move into the + * next queue (processed)... */ + + /* We try again and again until the job is completed. */ + unlockThreadedIO(); + /* But let's wait some time for the I/O thread + * to finish with this job. After all this condition + * should be very rare. */ + usleep(1); + goto again; case 2: /* io_processed */ /* The job was already processed, that's easy... * just mark it as canceled so that we'll ignore it @@ -7700,15 +7855,6 @@ static void *IOThreadEntryPoint(void *arg) { /* Get a new job to process */ lockThreadedIO(); if (listLength(server.io_newjobs) == 0) { -#ifdef REDIS_HELGRIND_FRIENDLY - /* No new jobs? Wait and retry, because to be Helgrind - * (valgrind --tool=helgrind) what's needed is to take - * the same threads running instead to create/destroy threads - * as needed (otherwise valgrind will fail) */ - unlockThreadedIO(); - usleep(1); /* Give some time for the I/O thread to work. */ - continue; -#endif /* No new jobs in queue, exit. */ redisLog(REDIS_DEBUG,"Thread %lld exiting, nothing to do", (long long) pthread_self()); @@ -7729,6 +7875,7 @@ static void *IOThreadEntryPoint(void *arg) { /* Process the Job */ if (j->type == REDIS_IOJOB_LOAD) { + j->val = vmReadObjectFromSwap(j->page,j->key->vtype); } else if (j->type == REDIS_IOJOB_PREPARE_SWAP) { FILE *fp = fopen("/dev/null","w+"); j->pages = rdbSavedObjectPages(j->val,fp); @@ -7754,8 +7901,15 @@ static void *IOThreadEntryPoint(void *arg) { static void spawnIOThread(void) { pthread_t thread; + sigset_t mask, omask; + sigemptyset(&mask); + sigaddset(&mask,SIGCHLD); + sigaddset(&mask,SIGHUP); + sigaddset(&mask,SIGPIPE); + pthread_sigmask(SIG_SETMASK, &mask, &omask); pthread_create(&thread,&server.io_threads_attr,IOThreadEntryPoint,NULL); + pthread_sigmask(SIG_SETMASK, &omask, NULL); server.io_active_threads++; } @@ -7763,6 +7917,8 @@ static void spawnIOThread(void) { * fork() in order to BGSAVE or BGREWRITEAOF. */ static void waitEmptyIOJobsQueue(void) { while(1) { + int io_processed_len; + lockThreadedIO(); if (listLength(server.io_newjobs) == 0 && listLength(server.io_processing) == 0 && @@ -7771,18 +7927,29 @@ static void waitEmptyIOJobsQueue(void) { unlockThreadedIO(); return; } + /* While waiting for empty jobs queue condition we post-process some + * finshed job, as I/O threads may be hanging trying to write against + * the io_ready_pipe_write FD but there are so much pending jobs that + * it's blocking. */ + io_processed_len = listLength(server.io_processed); unlockThreadedIO(); - usleep(10000); /* 10 milliseconds */ + if (io_processed_len) { + vmThreadedIOCompletedJob(NULL,server.io_ready_pipe_read,NULL,0); + usleep(1000); /* 1 millisecond */ + } else { + usleep(10000); /* 10 milliseconds */ + } } } static void vmReopenSwapFile(void) { - fclose(server.vm_fp); + /* Note: we don't close the old one as we are in the child process + * and don't want to mess at all with the original file object. */ server.vm_fp = fopen(server.vm_swap_file,"r+b"); if (server.vm_fp == NULL) { redisLog(REDIS_WARNING,"Can't re-open the VM swap file: %s. Exiting.", server.vm_swap_file); - exit(1); + _exit(1); } server.vm_fd = fileno(server.vm_fp); } @@ -7818,6 +7985,157 @@ static int vmSwapObjectThreaded(robj *key, robj *val, redisDb *db) { return REDIS_OK; } +/* ============ Virtual Memory - Blocking clients on missing keys =========== */ + +/* This function makes the clinet 'c' waiting for the key 'key' to be loaded. + * If there is not already a job loading the key, it is craeted. + * The key is added to the io_keys list in the client structure, and also + * in the hash table mapping swapped keys to waiting clients, that is, + * server.io_waited_keys. */ +static int waitForSwappedKey(redisClient *c, robj *key) { + struct dictEntry *de; + robj *o; + list *l; + + /* If the key does not exist or is already in RAM we don't need to + * block the client at all. */ + de = dictFind(c->db->dict,key); + if (de == NULL) return 0; + o = dictGetEntryKey(de); + if (o->storage == REDIS_VM_MEMORY) { + return 0; + } else if (o->storage == REDIS_VM_SWAPPING) { + /* We were swapping the key, undo it! */ + vmCancelThreadedIOJob(o); + return 0; + } + + /* OK: the key is either swapped, or being loaded just now. */ + + /* Add the key to the list of keys this client is waiting for. + * This maps clients to keys they are waiting for. */ + listAddNodeTail(c->io_keys,key); + incrRefCount(key); + + /* Add the client to the swapped keys => clients waiting map. */ + de = dictFind(c->db->io_keys,key); + if (de == NULL) { + int retval; + + /* For every key we take a list of clients blocked for it */ + l = listCreate(); + retval = dictAdd(c->db->io_keys,key,l); + incrRefCount(key); + assert(retval == DICT_OK); + } else { + l = dictGetEntryVal(de); + } + listAddNodeTail(l,c); + + /* Are we already loading the key from disk? If not create a job */ + if (o->storage == REDIS_VM_SWAPPED) { + iojob *j; + + o->storage = REDIS_VM_LOADING; + j = zmalloc(sizeof(*j)); + j->type = REDIS_IOJOB_LOAD; + j->db = c->db; + j->key = dupStringObject(key); + j->key->vtype = o->vtype; + j->page = o->vm.page; + j->val = NULL; + j->canceled = 0; + j->thread = (pthread_t) -1; + lockThreadedIO(); + queueIOJob(j); + unlockThreadedIO(); + } + return 1; +} + +/* Is this client attempting to run a command against swapped keys? + * If so, block it ASAP, load the keys in background, then resume it. + * + * The important idea about this function is that it can fail! If keys will + * still be swapped when the client is resumed, this key lookups will + * just block loading keys from disk. In practical terms this should only + * happen with SORT BY command or if there is a bug in this function. + * + * Return 1 if the client is marked as blocked, 0 if the client can + * continue as the keys it is going to access appear to be in memory. */ +static int blockClientOnSwappedKeys(struct redisCommand *cmd, redisClient *c) { + if (cmd->proc == getCommand) { + waitForSwappedKey(c,c->argv[1]); + } + /* If the client was blocked for at least one key, mark it as blocked. */ + if (listLength(c->io_keys)) { + c->flags |= REDIS_IO_WAIT; + aeDeleteFileEvent(server.el,c->fd,AE_READABLE); + server.vm_blocked_clients++; + return 1; + } else { + return 0; + } +} + +/* Remove the 'key' from the list of blocked keys for a given client. + * + * The function returns 1 when there are no longer blocking keys after + * the current one was removed (and the client can be unblocked). */ +static int dontWaitForSwappedKey(redisClient *c, robj *key) { + list *l; + listNode *ln; + listIter li; + struct dictEntry *de; + + /* Remove the key from the list of keys this client is waiting for. */ + listRewind(c->io_keys,&li); + while ((ln = listNext(&li)) != NULL) { + if (compareStringObjects(ln->value,key) == 0) { + listDelNode(c->io_keys,ln); + break; + } + } + assert(ln != NULL); + + /* Remove the client form the key => waiting clients map. */ + de = dictFind(c->db->io_keys,key); + assert(de != NULL); + l = dictGetEntryVal(de); + ln = listSearchKey(l,c); + assert(ln != NULL); + listDelNode(l,ln); + if (listLength(l) == 0) + dictDelete(c->db->io_keys,key); + + return listLength(c->io_keys) == 0; +} + +static void handleClientsBlockedOnSwappedKey(redisDb *db, robj *key) { + struct dictEntry *de; + list *l; + listNode *ln; + int len; + + de = dictFind(db->io_keys,key); + if (!de) return; + + l = dictGetEntryVal(de); + len = listLength(l); + /* Note: we can't use something like while(listLength(l)) as the list + * can be freed by the calling function when we remove the last element. */ + while (len--) { + ln = listFirst(l); + redisClient *c = ln->value; + + if (dontWaitForSwappedKey(c,key)) { + /* Put the client in the list of clients ready to go as we + * loaded all the keys about it. */ + listAddNodeTail(server.io_ready_clients,c); + } + } +} + /* ================================= Debugging ============================== */ static void debugCommand(redisClient *c) { @@ -7853,13 +8171,13 @@ static void debugCommand(redisClient *c) { } key = dictGetEntryKey(de); val = dictGetEntryVal(de); - if (server.vm_enabled && (key->storage == REDIS_VM_MEMORY || - key->storage == REDIS_VM_SWAPPING)) { + if (!server.vm_enabled || (key->storage == REDIS_VM_MEMORY || + key->storage == REDIS_VM_SWAPPING)) { addReplySds(c,sdscatprintf(sdsempty(), "+Key at:%p refcount:%d, value at:%p refcount:%d " "encoding:%d serializedlength:%lld\r\n", (void*)key, key->refcount, (void*)val, val->refcount, - val->encoding, rdbSavedObjectLen(val,NULL))); + val->encoding, (long long) rdbSavedObjectLen(val,NULL))); } else { addReplySds(c,sdscatprintf(sdsempty(), "+Key at:%p refcount:%d, value swapped at: page %llu " @@ -7960,6 +8278,8 @@ static void daemonize(void) { } int main(int argc, char **argv) { + time_t start; + initServerConfig(); if (argc == 2) { resetServerSaveParams(); @@ -7976,14 +8296,16 @@ int main(int argc, char **argv) { #ifdef __linux__ linuxOvercommitMemoryWarning(); #endif + start = time(NULL); if (server.appendonly) { if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK) - redisLog(REDIS_NOTICE,"DB loaded from append only file"); + redisLog(REDIS_NOTICE,"DB loaded from append only file: %ld seconds",time(NULL)-start); } else { if (rdbLoad(server.dbfilename) == REDIS_OK) - redisLog(REDIS_NOTICE,"DB loaded from disk"); + redisLog(REDIS_NOTICE,"DB loaded from disk: %ld seconds",time(NULL)-start); } redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port); + aeSetBeforeSleepProc(server.el,beforeSleep); aeMain(server.el); aeDeleteEventLoop(server.el); return 0; @@ -8054,7 +8376,7 @@ static void segvHandler(int sig, siginfo_t *info, void *secret) { } } /* free(messages); Don't call free() with possibly corrupted memory. */ - exit(0); + _exit(0); } static void setupSigSegvAction(void) {