Skip to content

Commit f3fd419

Browse files
committed
Support for read-only slaves. Semantical fixes.
This commit introduces support for read only slaves via redis.conf and CONFIG GET/SET commands. Also various semantical fixes are implemented here: 1) MULTI/EXEC with only read commands now work where the server is into a state where writes (or commands increasing memory usage) are not allowed. Before this patch everything inside a transaction would fail in this conditions. 2) Scripts just calling read-only commands will work against read only slaves, when the server is out of memory, or when persistence is into an error condition. Before the patch EVAL always failed in this condition.
1 parent 7a0c72f commit f3fd419

File tree

6 files changed

+85
-17
lines changed

6 files changed

+85
-17
lines changed

redis.conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ dir ./
156156
#
157157
slave-serve-stale-data yes
158158

159+
# You can configure a slave instance to accept writes or not. Writing against
160+
# a slave instance may be useful to store some ephemeral data (because data
161+
# written on a slave will be easily deleted after resync with the master) but
162+
# may also cause problems if clients are writing to it for an error.
163+
#
164+
# Since Redis 2.6 by default slaves are read-only.
165+
slave-read-only yes
166+
159167
# Slaves send PINGs to server in a predefined interval. It's possible to change
160168
# this interval with the repl_ping_slave_period option. The default value is 10
161169
# seconds.

src/config.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ void loadServerConfigFromString(char *config) {
202202
if ((server.repl_serve_stale_data = yesnotoi(argv[1])) == -1) {
203203
err = "argument must be 'yes' or 'no'"; goto loaderr;
204204
}
205+
} else if (!strcasecmp(argv[0],"slave-read-only") && argc == 2) {
206+
if ((server.repl_slave_ro = yesnotoi(argv[1])) == -1) {
207+
err = "argument must be 'yes' or 'no'"; goto loaderr;
208+
}
205209
} else if (!strcasecmp(argv[0],"rdbcompression") && argc == 2) {
206210
if ((server.rdb_compression = yesnotoi(argv[1])) == -1) {
207211
err = "argument must be 'yes' or 'no'"; goto loaderr;
@@ -514,6 +518,11 @@ void configSetCommand(redisClient *c) {
514518

515519
if (yn == -1) goto badfmt;
516520
server.repl_serve_stale_data = yn;
521+
} else if (!strcasecmp(c->argv[2]->ptr,"slave-read-only")) {
522+
int yn = yesnotoi(o->ptr);
523+
524+
if (yn == -1) goto badfmt;
525+
server.repl_slave_ro = yn;
517526
} else if (!strcasecmp(c->argv[2]->ptr,"dir")) {
518527
if (chdir((char*)o->ptr) == -1) {
519528
addReplyErrorFormat(c,"Changing directory: %s", strerror(errno));
@@ -712,6 +721,8 @@ void configGetCommand(redisClient *c) {
712721
server.aof_no_fsync_on_rewrite);
713722
config_get_bool_field("slave-serve-stale-data",
714723
server.repl_serve_stale_data);
724+
config_get_bool_field("slave-read-only",
725+
server.repl_slave_ro);
715726
config_get_bool_field("stop-writes-on-bgsave-error",
716727
server.stop_writes_on_bgsave_err);
717728
config_get_bool_field("daemonize", server.daemonize);

src/multi.c

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ void queueMultiCommand(redisClient *c) {
4040
c->mstate.count++;
4141
}
4242

43+
void discardTransaction(redisClient *c) {
44+
freeClientMultiState(c);
45+
initClientMultiState(c);
46+
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
47+
unwatchAllKeys(c);
48+
}
49+
4350
void multiCommand(redisClient *c) {
4451
if (c->flags & REDIS_MULTI) {
4552
addReplyError(c,"MULTI calls can not be nested");
@@ -54,11 +61,7 @@ void discardCommand(redisClient *c) {
5461
addReplyError(c,"DISCARD without MULTI");
5562
return;
5663
}
57-
58-
freeClientMultiState(c);
59-
initClientMultiState(c);
60-
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
61-
unwatchAllKeys(c);
64+
discardTransaction(c);
6265
addReply(c,shared.ok);
6366
}
6467

src/redis.c

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ struct redisCommand redisCommandTable[] = {
211211
{"lastsave",lastsaveCommand,1,"r",0,NULL,0,0,0,0,0},
212212
{"type",typeCommand,2,"r",0,NULL,1,1,1,0,0},
213213
{"multi",multiCommand,1,"rs",0,NULL,0,0,0,0,0},
214-
{"exec",execCommand,1,"wms",0,NULL,0,0,0,0,0},
214+
{"exec",execCommand,1,"s",0,NULL,0,0,0,0,0},
215215
{"discard",discardCommand,1,"rs",0,NULL,0,0,0,0,0},
216216
{"sync",syncCommand,1,"ars",0,NULL,0,0,0,0,0},
217217
{"flushdb",flushdbCommand,1,"w",0,NULL,0,0,0,0,0},
@@ -239,8 +239,8 @@ struct redisCommand redisCommandTable[] = {
239239
{"dump",dumpCommand,2,"ar",0,NULL,1,1,1,0,0},
240240
{"object",objectCommand,-2,"r",0,NULL,2,2,2,0,0},
241241
{"client",clientCommand,-2,"ar",0,NULL,0,0,0,0,0},
242-
{"eval",evalCommand,-3,"wms",0,zunionInterGetKeys,0,0,0,0,0},
243-
{"evalsha",evalShaCommand,-3,"wms",0,zunionInterGetKeys,0,0,0,0,0},
242+
{"eval",evalCommand,-3,"s",0,zunionInterGetKeys,0,0,0,0,0},
243+
{"evalsha",evalShaCommand,-3,"s",0,zunionInterGetKeys,0,0,0,0,0},
244244
{"slowlog",slowlogCommand,-2,"r",0,NULL,0,0,0,0,0},
245245
{"script",scriptCommand,-2,"ras",0,NULL,0,0,0,0,0},
246246
{"time",timeCommand,1,"rR",0,NULL,0,0,0,0,0}
@@ -939,7 +939,11 @@ void createSharedObjects(void) {
939939
shared.slowscripterr = createObject(REDIS_STRING,sdsnew(
940940
"-BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.\r\n"));
941941
shared.bgsaveerr = createObject(REDIS_STRING,sdsnew(
942-
"-MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Write commands are disabled. Please check Redis logs for details about the error.\r\n"));
942+
"-MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.\r\n"));
943+
shared.roslaveerr = createObject(REDIS_STRING,sdsnew(
944+
"-READONLY You can't write against a read only slave.\r\n"));
945+
shared.oomerr = createObject(REDIS_STRING,
946+
"-OOM command not allowed when used memory > 'maxmemory'.\r\n");
943947
shared.space = createObject(REDIS_STRING,sdsnew(" "));
944948
shared.colon = createObject(REDIS_STRING,sdsnew(":"));
945949
shared.plus = createObject(REDIS_STRING,sdsnew("+"));
@@ -1048,6 +1052,7 @@ void initServerConfig() {
10481052
server.repl_state = REDIS_REPL_NONE;
10491053
server.repl_syncio_timeout = REDIS_REPL_SYNCIO_TIMEOUT;
10501054
server.repl_serve_stale_data = 1;
1055+
server.repl_slave_ro = 1;
10511056
server.repl_down_since = -1;
10521057

10531058
/* Client output buffer limits */
@@ -1483,8 +1488,7 @@ int processCommand(redisClient *c) {
14831488
if (server.maxmemory) {
14841489
int retval = freeMemoryIfNeeded();
14851490
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
1486-
addReplyError(c,
1487-
"command not allowed when used memory > 'maxmemory'");
1491+
addReply(c, shared.oomerr);
14881492
return REDIS_OK;
14891493
}
14901494
}
@@ -1499,6 +1503,16 @@ int processCommand(redisClient *c) {
14991503
return REDIS_OK;
15001504
}
15011505

1506+
/* Don't accept wirte commands if this is a read only slave. But
1507+
* accept write commands if this is our master. */
1508+
if (server.masterhost && server.repl_slave_ro &&
1509+
!(c->flags & REDIS_MASTER) &&
1510+
c->cmd->flags & REDIS_CMD_WRITE)
1511+
{
1512+
addReply(c, shared.roslaveerr);
1513+
return REDIS_OK;
1514+
}
1515+
15021516
/* Only allow SUBSCRIBE and UNSUBSCRIBE in the context of Pub/Sub */
15031517
if ((dictSize(c->pubsub_channels) > 0 || listLength(c->pubsub_patterns) > 0)
15041518
&&

src/redis.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ struct sharedObjectsStruct {
366366
*colon, *nullbulk, *nullmultibulk, *queued,
367367
*emptymultibulk, *wrongtypeerr, *nokeyerr, *syntaxerr, *sameobjecterr,
368368
*outofrangeerr, *noscripterr, *loadingerr, *slowscripterr, *bgsaveerr,
369-
*plus, *select0, *select1, *select2, *select3, *select4,
370-
*select5, *select6, *select7, *select8, *select9,
369+
*roslaveerr, *oomerr, *plus, *select0, *select1, *select2, *select3,
370+
*select4, *select5, *select6, *select7, *select8, *select9,
371371
*messagebulk, *pmessagebulk, *subscribebulk, *unsubscribebulk,
372372
*psubscribebulk, *punsubscribebulk, *del, *rpop, *lpop,
373373
*integers[REDIS_SHARED_INTEGERS],
@@ -671,6 +671,7 @@ struct redisServer {
671671
char *repl_transfer_tmpfile; /* Slave-> master SYNC temp file name */
672672
time_t repl_transfer_lastio; /* Unix time of the latest read, for timeout */
673673
int repl_serve_stale_data; /* Serve stale data when link is down? */
674+
int repl_slave_ro; /* Slave is read only? */
674675
time_t repl_down_since; /* Unix time at which link with master went down */
675676
/* Limits */
676677
unsigned int maxclients; /* Max number of simultaneous clients */
@@ -901,6 +902,7 @@ void freeClientMultiState(redisClient *c);
901902
void queueMultiCommand(redisClient *c);
902903
void touchWatchedKey(redisDb *db, robj *key);
903904
void touchWatchedKeysOnFlush(int dbid);
905+
void discardTransaction(redisClient *c);
904906

905907
/* Redis object implementation */
906908
void decrRefCount(void *o);

src/scripting.c

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,45 @@ int luaRedisGenericCommand(lua_State *lua, int raise_error) {
206206
goto cleanup;
207207
}
208208

209+
/* There are commands that are not allowed inside scripts. */
209210
if (cmd->flags & REDIS_CMD_NOSCRIPT) {
210211
luaPushError(lua, "This Redis command is not allowed from scripts");
211212
goto cleanup;
212213
}
213214

214-
if (cmd->flags & REDIS_CMD_WRITE && server.lua_random_dirty) {
215-
luaPushError(lua,
216-
"Write commands not allowed after non deterministic commands");
217-
goto cleanup;
215+
/* Write commands are forbidden against read-only slaves, or if a
216+
* command marked as non-deterministic was already called in the context
217+
* of this script. */
218+
if (cmd->flags & REDIS_CMD_WRITE) {
219+
if (server.lua_random_dirty) {
220+
luaPushError(lua,
221+
"Write commands not allowed after non deterministic commands");
222+
goto cleanup;
223+
} else if (server.masterhost && server.repl_slave_ro &&
224+
!(server.lua_caller->flags & REDIS_MASTER))
225+
{
226+
luaPushError(lua, shared.roslaveerr->ptr);
227+
goto cleanup;
228+
} else if (server.stop_writes_on_bgsave_err &&
229+
server.saveparamslen > 0 &&
230+
server.lastbgsave_status == REDIS_ERR)
231+
{
232+
luaPushError(lua, shared.bgsaveerr->ptr);
233+
goto cleanup;
234+
}
235+
}
236+
237+
/* If we reached the memory limit configured via maxmemory, commands that
238+
* could enlarge the memory usage are not allowed, but only if this is the
239+
* first write in the context of this script, otherwise we can't stop
240+
* in the middle. */
241+
if (server.maxmemory && server.lua_write_dirty == 0 &&
242+
(cmd->flags & REDIS_CMD_DENYOOM))
243+
{
244+
if (freeMemoryIfNeeded() == REDIS_ERR) {
245+
luaPushError(lua, shared.oomerr->ptr);
246+
goto cleanup;
247+
}
218248
}
219249

220250
if (cmd->flags & REDIS_CMD_RANDOM) server.lua_random_dirty = 1;

0 commit comments

Comments
 (0)