#include <limits.h>
#include <math.h>
+#if defined(__sun)
+#include "solarisfixes.h"
+#endif
+
#include "redis.h"
#include "ae.h" /* Event driven programming library */
#include "sds.h" /* Dynamic safe strings */
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
+/* Append only defines */
+#define APPENDFSYNC_NO 0
+#define APPENDFSYNC_ALWAYS 1
+#define APPENDFSYNC_EVERYSEC 2
+
/*================================= Data types ============================== */
/* A redis object, that is a type able to hold a string / list / set */
int maxidletime;
int dbnum;
int daemonize;
+ int appendonly;
+ int appendfsync;
+ time_t lastfsync;
+ int appendfd;
+ int appendseldb;
char *pidfile;
int bgsaveinprogress;
pid_t bgsavechildpid;
char *logfile;
char *bindaddr;
char *dbfilename;
+ char *appendfilename;
char *requirepass;
int shareobjects;
/* Replication related */
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
- long length;
+ unsigned long length;
int level;
} zskiplist;
static int rdbSaveBackground(char *filename);
static robj *createStringObject(char *ptr, size_t len);
static void replicationFeedSlaves(list *slaves, struct redisCommand *cmd, int dictid, robj **argv, int argc);
+static void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc);
static int syncWithMaster(void);
static robj *tryObjectSharing(robj *o);
static int tryObjectEncoding(robj *o);
static void mgetCommand(redisClient *c);
static void monitorCommand(redisClient *c);
static void expireCommand(redisClient *c);
+static void expireatCommand(redisClient *c);
static void getsetCommand(redisClient *c);
static void ttlCommand(redisClient *c);
static void slaveofCommand(redisClient *c);
static void zrangeCommand(redisClient *c);
static void zrangebyscoreCommand(redisClient *c);
static void zrevrangeCommand(redisClient *c);
-static void zlenCommand(redisClient *c);
+static void zcardCommand(redisClient *c);
static void zremCommand(redisClient *c);
+static void zscoreCommand(redisClient *c);
+static void zremrangebyscoreCommand(redisClient *c);
/*================================= Globals ================================= */
{"smembers",sinterCommand,2,REDIS_CMD_INLINE},
{"zadd",zaddCommand,4,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
{"zrem",zremCommand,3,REDIS_CMD_BULK},
+ {"zremrangebyscore",zremrangebyscoreCommand,4,REDIS_CMD_INLINE},
{"zrange",zrangeCommand,4,REDIS_CMD_INLINE},
{"zrangebyscore",zrangebyscoreCommand,4,REDIS_CMD_INLINE},
{"zrevrange",zrevrangeCommand,4,REDIS_CMD_INLINE},
- {"zlen",zlenCommand,2,REDIS_CMD_INLINE},
+ {"zcard",zcardCommand,2,REDIS_CMD_INLINE},
+ {"zscore",zscoreCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
{"incrby",incrbyCommand,3,REDIS_CMD_INLINE|REDIS_CMD_DENYOOM},
{"decrby",decrbyCommand,3,REDIS_CMD_INLINE|REDIS_CMD_DENYOOM},
{"getset",getsetCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},
{"rename",renameCommand,3,REDIS_CMD_INLINE},
{"renamenx",renamenxCommand,3,REDIS_CMD_INLINE},
{"expire",expireCommand,3,REDIS_CMD_INLINE},
+ {"expireat",expireatCommand,3,REDIS_CMD_INLINE},
{"keys",keysCommand,2,REDIS_CMD_INLINE},
{"dbsize",dbsizeCommand,1,REDIS_CMD_INLINE},
{"auth",authCommand,2,REDIS_CMD_INLINE},
time_t now;
now = time(NULL);
- strftime(buf,64,"%d %b %H:%M:%S",gmtime(&now));
+ strftime(buf,64,"%d %b %H:%M:%S",localtime(&now));
fprintf(fp,"%s %c ",buf,c[level]);
vfprintf(fp, fmt, ap);
fprintf(fp,"\n");
server.bindaddr = NULL;
server.glueoutputbuf = 1;
server.daemonize = 0;
+ server.appendonly = 0;
+ server.appendfsync = APPENDFSYNC_ALWAYS;
+ server.lastfsync = time(NULL);
+ server.appendfd = -1;
+ server.appendseldb = -1; /* Make sure the first time will not match */
server.pidfile = "/var/run/redis.pid";
server.dbfilename = "dump.rdb";
+ server.appendfilename = "appendonly.log";
server.requirepass = NULL;
server.shareobjects = 0;
server.sharingpoolsize = 1024;
server.stat_numconnections = 0;
server.stat_starttime = time(NULL);
aeCreateTimeEvent(server.el, 1000, serverCron, NULL, NULL);
+
+ if (server.appendonly) {
+ server.appendfd = open(server.appendfilename,O_WRONLY|O_APPEND|O_CREAT,0644);
+ if (server.appendfd == -1) {
+ redisLog(REDIS_WARNING, "Can't open the append-only file: %s",
+ strerror(errno));
+ exit(1);
+ }
+ }
}
/* Empty the whole database */
if ((server.daemonize = yesnotoi(argv[1])) == -1) {
err = "argument must be 'yes' or 'no'"; goto loaderr;
}
+ } else if (!strcasecmp(argv[0],"appendonly") && argc == 2) {
+ if ((server.appendonly = yesnotoi(argv[1])) == -1) {
+ err = "argument must be 'yes' or 'no'"; goto loaderr;
+ }
+ } else if (!strcasecmp(argv[0],"appendfsync") && argc == 2) {
+ if (strcasecmp(argv[1],"no")) {
+ server.appendfsync = APPENDFSYNC_NO;
+ } else if (strcasecmp(argv[1],"always")) {
+ server.appendfsync = APPENDFSYNC_ALWAYS;
+ } else if (strcasecmp(argv[1],"everysec")) {
+ server.appendfsync = APPENDFSYNC_EVERYSEC;
+ } else {
+ err = "argument must be 'no', 'always' or 'everysec'";
+ goto loaderr;
+ }
} else if (!strcasecmp(argv[0],"requirepass") && argc == 2) {
server.requirepass = zstrdup(argv[1]);
} else if (!strcasecmp(argv[0],"pidfile") && argc == 2) {
/* Exec the command */
dirty = server.dirty;
cmd->proc(c);
+ if (server.appendonly != 0)
+ feedAppendOnlyFile(cmd,c->db->id,c->argv,c->argc);
if (server.dirty-dirty != 0 && listLength(server.slaves))
replicationFeedSlaves(server.slaves,cmd,c->db->id,c->argv,c->argc);
if (listLength(server.monitors))
}
}
+/* Compare two string objects via strcmp() or alike.
+ * Note that the objects may be integer-encoded. In such a case we
+ * use snprintf() to get a string representation of the numbers on the stack
+ * and compare the strings, it's much faster than calling getDecodedObject(). */
static int compareStringObjects(robj *a, robj *b) {
assert(a->type == REDIS_STRING && b->type == REDIS_STRING);
+ char bufa[128], bufb[128], *astr, *bstr;
+ int bothsds = 1;
if (a == b) return 0;
- if (a->encoding == REDIS_ENCODING_INT && b->encoding == REDIS_ENCODING_INT){
- return (long)a->ptr - (long)b->ptr;
+ if (a->encoding != REDIS_ENCODING_RAW) {
+ snprintf(bufa,sizeof(bufa),"%ld",(long) a->ptr);
+ astr = bufa;
+ bothsds = 0;
} else {
- int retval;
-
- incrRefCount(a);
- incrRefCount(b);
- if (a->encoding != REDIS_ENCODING_RAW) a = getDecodedObject(a);
- if (b->encoding != REDIS_ENCODING_RAW) b = getDecodedObject(a);
- retval = sdscmp(a->ptr,b->ptr);
- decrRefCount(a);
- decrRefCount(b);
- return retval;
+ astr = a->ptr;
+ }
+ if (b->encoding != REDIS_ENCODING_RAW) {
+ snprintf(bufb,sizeof(bufb),"%ld",(long) b->ptr);
+ bstr = bufb;
+ bothsds = 0;
+ } else {
+ bstr = b->ptr;
}
+ return bothsds ? sdscmp(astr,bstr) : strcmp(astr,bstr);
}
static size_t stringObjectLen(robj *o) {
eoferr: /* unexpected end of file is handled here with a fatal exit */
if (keyobj) decrRefCount(keyobj);
- redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, exiting now.");
+ redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now.");
exit(1);
return REDIS_ERR; /* Just to avoid warning */
}
addReply(c,shared.ok);
} else {
c->authenticated = 0;
- addReply(c,shared.err);
+ addReplySds(c,sdscatprintf(sdsempty(),"-ERR invalid password\r\n"));
}
}
case REDIS_STRING: type = "+string"; break;
case REDIS_LIST: type = "+list"; break;
case REDIS_SET: type = "+set"; break;
+ case REDIS_ZSET: type = "+zset"; break;
default: type = "unknown"; break;
}
}
return 0; /* not found */
}
+/* Delete all the elements with score between min and max from the skiplist.
+ * Min and mx are inclusive, so a score >= min || score <= max is deleted.
+ * Note that this function takes the reference to the hash table view of the
+ * sorted set, in order to remove the elements from the hash table too. */
+static unsigned long zslDeleteRange(zskiplist *zsl, double min, double max, dict *dict) {
+ zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
+ unsigned long removed = 0;
+ int i;
+
+ x = zsl->header;
+ for (i = zsl->level-1; i >= 0; i--) {
+ while (x->forward[i] && x->forward[i]->score < min)
+ x = x->forward[i];
+ update[i] = x;
+ }
+ /* We may have multiple elements with the same score, what we need
+ * is to find the element with both the right score and object. */
+ x = x->forward[0];
+ while (x && x->score <= max) {
+ zskiplistNode *next;
+
+ for (i = 0; i < zsl->level; i++) {
+ if (update[i]->forward[i] != x) break;
+ update[i]->forward[i] = x->forward[i];
+ }
+ if (x->forward[0]) {
+ x->forward[0]->backward = (x->backward == zsl->header) ?
+ NULL : x->backward;
+ } else {
+ zsl->tail = x->backward;
+ }
+ next = x->forward[0];
+ dictDelete(dict,x->obj);
+ zslFreeNode(x);
+ while(zsl->level > 1 && zsl->header->forward[zsl->level-1] == NULL)
+ zsl->level--;
+ zsl->length--;
+ removed++;
+ x = next;
+ }
+ return removed; /* not found */
+}
+
/* Find the first node having a score equal or greater than the specified one.
* Returns NULL if there is no match. */
static zskiplistNode *zslFirstWithScore(zskiplist *zsl, double score) {
}
}
+static void zremrangebyscoreCommand(redisClient *c) {
+ double min = strtod(c->argv[2]->ptr,NULL);
+ double max = strtod(c->argv[3]->ptr,NULL);
+ robj *zsetobj;
+ zset *zs;
+
+ zsetobj = lookupKeyWrite(c->db,c->argv[1]);
+ if (zsetobj == NULL) {
+ addReply(c,shared.czero);
+ } else {
+ long deleted;
+
+ if (zsetobj->type != REDIS_ZSET) {
+ addReply(c,shared.wrongtypeerr);
+ return;
+ }
+ zs = zsetobj->ptr;
+ deleted = zslDeleteRange(zs->zsl,min,max,zs->dict);
+ if (htNeedsResize(zs->dict)) dictResize(zs->dict);
+ server.dirty += deleted;
+ addReplySds(c,sdscatprintf(sdsempty(),":%lu\r\n",deleted));
+ }
+}
+
static void zrangeGenericCommand(redisClient *c, int reverse) {
robj *o;
int start = atoi(c->argv[2]->ptr);
lenobj = createObject(REDIS_STRING,NULL);
addReply(c,lenobj);
- while(ln->score <= max) {
+ while(ln && ln->score <= max) {
ele = ln->obj;
addReplyBulkLen(c,ele);
addReply(c,ele);
}
}
-static void zlenCommand(redisClient *c) {
+static void zcardCommand(redisClient *c) {
robj *o;
zset *zs;
}
}
+static void zscoreCommand(redisClient *c) {
+ robj *o;
+ zset *zs;
+
+ o = lookupKeyRead(c->db,c->argv[1]);
+ if (o == NULL) {
+ addReply(c,shared.czero);
+ return;
+ } else {
+ if (o->type != REDIS_ZSET) {
+ addReply(c,shared.wrongtypeerr);
+ } else {
+ dictEntry *de;
+
+ zs = o->ptr;
+ de = dictFind(zs->dict,c->argv[2]);
+ if (!de) {
+ addReply(c,shared.nullbulk);
+ } else {
+ char buf[128];
+ double *score = dictGetEntryVal(de);
+
+ snprintf(buf,sizeof(buf),"%.16g",*score);
+ addReplySds(c,sdscatprintf(sdsempty(),"$%d\r\n%s\r\n",
+ strlen(buf),buf));
+ }
+ }
+ }
+}
+
/* ========================= Non type-specific commands ==================== */
static void flushdbCommand(redisClient *c) {
server.masterport,
(server.replstate == REDIS_REPL_CONNECTED) ?
"up" : "down",
- (int)(time(NULL)-server.master->lastinteraction)
+ server.master ? ((int)(time(NULL)-server.master->lastinteraction)) : -1
);
}
for (j = 0; j < server.dbnum; j++) {
return dictDelete(db->dict,key) == DICT_OK;
}
-static void expireCommand(redisClient *c) {
+static void expireGenericCommand(redisClient *c, robj *key, time_t seconds) {
dictEntry *de;
- int seconds = atoi(c->argv[2]->ptr);
- de = dictFind(c->db->dict,c->argv[1]);
+ de = dictFind(c->db->dict,key);
if (de == NULL) {
addReply(c,shared.czero);
return;
}
- if (seconds <= 0) {
- addReply(c, shared.czero);
+ if (seconds < 0) {
+ if (deleteKey(c->db,key)) server.dirty++;
+ addReply(c, shared.cone);
return;
} else {
time_t when = time(NULL)+seconds;
- if (setExpire(c->db,c->argv[1],when)) {
+ if (setExpire(c->db,key,when)) {
addReply(c,shared.cone);
server.dirty++;
} else {
}
}
+static void expireCommand(redisClient *c) {
+ expireGenericCommand(c,c->argv[1],strtol(c->argv[2]->ptr,NULL,10));
+}
+
+static void expireatCommand(redisClient *c) {
+ expireGenericCommand(c,c->argv[1],strtol(c->argv[2]->ptr,NULL,10)-time(NULL));
+}
+
static void ttlCommand(redisClient *c) {
time_t expire;
int ttl = -1;
strerror(errno));
return REDIS_ERR;
}
+ if (buf[0] != '$') {
+ close(fd);
+ redisLog(REDIS_WARNING,"Bad protocol from MASTER, the first byte is not '$', are you sure the host and port are right?");
+ return REDIS_ERR;
+ }
dumpsize = atoi(buf+1);
redisLog(REDIS_NOTICE,"Receiving %d bytes data dump from MASTER",dumpsize);
/* Read the bulk write data on a temp file */
}
}
+/* ============================== Append Only file ========================== */
+
+static void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
+ sds buf = sdsempty();
+ int j;
+ ssize_t nwritten;
+ time_t now;
+ robj *tmpargv[3];
+
+ /* The DB this command was targetting is not the same as the last command
+ * we appendend. To issue a SELECT command is needed. */
+ if (dictid != server.appendseldb) {
+ char seldb[64];
+
+ snprintf(seldb,sizeof(seldb),"%d",dictid);
+ buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%d\r\n%s\r\n",
+ strlen(seldb),seldb);
+ server.appendseldb = dictid;
+ }
+
+ /* "Fix" the argv vector if the command is EXPIRE. We want to translate
+ * EXPIREs into EXPIREATs calls */
+ if (cmd->proc == expireCommand) {
+ long when;
+
+ tmpargv[0] = createStringObject("EXPIREAT",8);
+ tmpargv[1] = argv[1];
+ incrRefCount(argv[1]);
+ when = time(NULL)+strtol(argv[2]->ptr,NULL,10);
+ tmpargv[2] = createObject(REDIS_STRING,
+ sdscatprintf(sdsempty(),"%ld",when));
+ argv = tmpargv;
+ }
+
+ /* Append the actual command */
+ buf = sdscatprintf(buf,"*%d\r\n",argc);
+ for (j = 0; j < argc; j++) {
+ robj *o = argv[j];
+
+ if (o->encoding != REDIS_ENCODING_RAW)
+ o = getDecodedObject(o);
+ buf = sdscatprintf(buf,"$%d\r\n",sdslen(o->ptr));
+ buf = sdscatlen(buf,o->ptr,sdslen(o->ptr));
+ buf = sdscatlen(buf,"\r\n",2);
+ if (o != argv[j])
+ decrRefCount(o);
+ }
+
+ /* Free the objects from the modified argv for EXPIREAT */
+ if (cmd->proc == expireCommand) {
+ for (j = 0; j < 3; j++)
+ decrRefCount(argv[j]);
+ }
+
+ /* We want to perform a single write. This should be guaranteed atomic
+ * at least if the filesystem we are writing is a real physical one.
+ * While this will save us against the server being killed I don't think
+ * there is much to do about the whole server stopping for power problems
+ * or alike */
+ nwritten = write(server.appendfd,buf,sdslen(buf));
+ if (nwritten != (signed)sdslen(buf)) {
+ /* Ooops, we are in troubles. The best thing to do for now is
+ * to simply exit instead to give the illusion that everything is
+ * working as expected. */
+ if (nwritten == -1) {
+ redisLog(REDIS_WARNING,"Exiting on error writing to the append-only file: %s",strerror(errno));
+ } else {
+ redisLog(REDIS_WARNING,"Exiting on short write while writing to the append-only file: %s",strerror(errno));
+ }
+ exit(1);
+ }
+ now = time(NULL);
+ if (server.appendfsync == APPENDFSYNC_ALWAYS ||
+ (server.appendfsync == APPENDFSYNC_EVERYSEC &&
+ now-server.lastfsync > 1))
+ {
+ fsync(server.appendfd); /* Let's try to get this data on the disk */
+ server.lastfsync = now;
+ }
+}
+
+/* In Redis commands are always executed in the context of a client, so in
+ * order to load the append only file we need to create a fake client. */
+static struct redisClient *createFakeClient(void) {
+ struct redisClient *c = zmalloc(sizeof(*c));
+
+ selectDb(c,0);
+ c->fd = -1;
+ c->querybuf = sdsempty();
+ c->argc = 0;
+ c->argv = NULL;
+ c->flags = 0;
+ /* We set the fake client as a slave waiting for the synchronization
+ * so that Redis will not try to send replies to this client. */
+ c->replstate = REDIS_REPL_WAIT_BGSAVE_START;
+ c->reply = listCreate();
+ listSetFreeMethod(c->reply,decrRefCount);
+ listSetDupMethod(c->reply,dupClientReplyValue);
+ return c;
+}
+
+static void freeFakeClient(struct redisClient *c) {
+ sdsfree(c->querybuf);
+ listRelease(c->reply);
+ zfree(c);
+}
+
+/* Replay the append log file. On error REDIS_OK is returned. On non fatal
+ * error (the append only file is zero-length) REDIS_ERR is returned. On
+ * fatal error an error message is logged and the program exists. */
+int loadAppendOnlyFile(char *filename) {
+ struct redisClient *fakeClient;
+ FILE *fp = fopen(filename,"r");
+ struct redis_stat sb;
+
+ if (redis_fstat(fileno(fp),&sb) != -1 && sb.st_size == 0)
+ return REDIS_ERR;
+
+ if (fp == NULL) {
+ redisLog(REDIS_WARNING,"Fatal error: can't open the append log file for reading: %s",strerror(errno));
+ exit(1);
+ }
+
+ fakeClient = createFakeClient();
+ while(1) {
+ int argc, j;
+ unsigned long len;
+ robj **argv;
+ char buf[128];
+ sds argsds;
+ struct redisCommand *cmd;
+
+ if (fgets(buf,sizeof(buf),fp) == NULL) {
+ if (feof(fp))
+ break;
+ else
+ goto readerr;
+ }
+ if (buf[0] != '*') goto fmterr;
+ argc = atoi(buf+1);
+ argv = zmalloc(sizeof(robj*)*argc);
+ for (j = 0; j < argc; j++) {
+ if (fgets(buf,sizeof(buf),fp) == NULL) goto readerr;
+ if (buf[0] != '$') goto fmterr;
+ len = strtol(buf+1,NULL,10);
+ argsds = sdsnewlen(NULL,len);
+ if (fread(argsds,len,1,fp) == 0) goto fmterr;
+ argv[j] = createObject(REDIS_STRING,argsds);
+ if (fread(buf,2,1,fp) == 0) goto fmterr; /* discard CRLF */
+ }
+
+ /* Command lookup */
+ cmd = lookupCommand(argv[0]->ptr);
+ if (!cmd) {
+ redisLog(REDIS_WARNING,"Unknown command '%s' reading the append only file", argv[0]->ptr);
+ exit(1);
+ }
+ /* Try object sharing and encoding */
+ if (server.shareobjects) {
+ int j;
+ for(j = 1; j < argc; j++)
+ argv[j] = tryObjectSharing(argv[j]);
+ }
+ if (cmd->flags & REDIS_CMD_BULK)
+ tryObjectEncoding(argv[argc-1]);
+ /* Run the command in the context of a fake client */
+ fakeClient->argc = argc;
+ fakeClient->argv = argv;
+ cmd->proc(fakeClient);
+ /* Discard the reply objects list from the fake client */
+ while(listLength(fakeClient->reply))
+ listDelNode(fakeClient->reply,listFirst(fakeClient->reply));
+ /* Clean up, ready for the next command */
+ for (j = 0; j < argc; j++) decrRefCount(argv[j]);
+ zfree(argv);
+ }
+ fclose(fp);
+ freeFakeClient(fakeClient);
+ return REDIS_OK;
+
+readerr:
+ if (feof(fp)) {
+ redisLog(REDIS_WARNING,"Unexpected end of file reading the append only file");
+ } else {
+ redisLog(REDIS_WARNING,"Unrecoverable error reading the append only file: %s", strerror(errno));
+ }
+ exit(1);
+fmterr:
+ redisLog(REDIS_WARNING,"Bad file format reading the append only file");
+ exit(1);
+}
+
/* ================================= Debugging ============================== */
static void debugCommand(redisClient *c) {
{"mgetCommand", (unsigned long)mgetCommand},
{"monitorCommand", (unsigned long)monitorCommand},
{"expireCommand", (unsigned long)expireCommand},
+{"expireatCommand", (unsigned long)expireatCommand},
{"getsetCommand", (unsigned long)getsetCommand},
{"ttlCommand", (unsigned long)ttlCommand},
{"slaveofCommand", (unsigned long)slaveofCommand},
{"zremCommand",(unsigned long)zremCommand},
{"rdbSaveDoubleValue",(unsigned long)rdbSaveDoubleValue},
{"rdbLoadDoubleValue",(unsigned long)rdbLoadDoubleValue},
+{"feedAppendOnlyFile",(unsigned long)feedAppendOnlyFile},
{NULL,0}
};
#ifdef __linux__
linuxOvercommitMemoryWarning();
#endif
- if (rdbLoad(server.dbfilename) == REDIS_OK)
- redisLog(REDIS_NOTICE,"DB loaded from disk");
+ if (server.appendonly) {
+ if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK)
+ redisLog(REDIS_NOTICE,"DB loaded from append only file");
+ } else {
+ if (rdbLoad(server.dbfilename) == REDIS_OK)
+ redisLog(REDIS_NOTICE,"DB loaded from disk");
+ }
if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
acceptHandler, NULL, NULL) == AE_ERR) oom("creating file event");
redisLog(REDIS_NOTICE,"The server is now ready to accept connections on port %d", server.port);