redis/redis
Patterns and conventions
These are the conventions that have stuck in 17 years of Redis development. Follow them to keep PRs reviewable.
Style
- Indentation: 4 spaces per level. The older parts of
src/use a mix; new code is 4-space. - Braces: K&R style, opening brace on the same line.
- Line length: ~120 chars is the practical limit; older code often stays at 80.
- Comments: only
/* ... */. Never//line comments. Multi-line C comments use a leading*on continuation lines. - Naming:
- Functions:
camelCase(e.g.processCommand,addReplyBulkCString). - Macros and constants:
UPPER_SNAKE_CASE(e.g.OBJ_ENCODING_LISTPACK). - Local variables:
lower_snake_caseor shorti,j,n. - Struct types:
lowerCamelCase(e.g.client,redisDb,redisObject).
- Functions:
- Headers: declare functions with
staticif file-local; never expose internals. Public APIs live insrc/server.hor in a dedicated header (src/cluster.h,src/aof.h, …).
Memory rules
Redis tracks every byte of dynamically allocated memory so INFO memory is accurate. The rules:
- Always use
zmalloc/zcalloc/zrealloc/zfree/zstrdupinstead of the libc equivalents. Header:src/zmalloc.h. The wrapper bumps a global counter and forwards to jemalloc/libc. - Strings are SDS unless they're really C strings. SDS strings know their length, can hold
\0, and are returned bysdsnew()/sdscatprintf()etc. See SDS. Free withsdsfree. - Refcount wins over copy. When a value can be shared, increment its refcount (
incrRefCount(robj*)). Shared integer objects are pre-allocated inserver.sharedand reused everywhere. - Lazy free for big objects. Anything that could free hundreds of MB synchronously goes through
freeObjAsync()/dbAsyncDelete()so the BIO_LAZY_FREE thread does the actual work. Seesrc/lazyfree.c. - No allocations in signal handlers. Use the
serverLogFromHandlerpath; it pre-formats with a static buffer and callswrite(2)directly.
Command implementation pattern
A command handler has the signature:
void exampleCommand(client *c) {
/* 1. Argument parsing -------------------------------------------------- */
long long n;
if (getLongLongFromObjectOrReply(c, c->argv[1], &n, NULL) != C_OK)
return;
/* 2. Lookup ------------------------------------------------------------ */
robj *o = lookupKeyWriteOrReply(c, c->argv[2], shared.czero);
if (o == NULL || checkType(c, o, OBJ_LIST)) return;
/* 3. Mutation ---------------------------------------------------------- */
listTypePush(o, ...);
/* 4. Reply ------------------------------------------------------------- */
addReplyLongLong(c, listTypeLength(o));
/* 5. Notification + replication --------------------------------------- */
notifyKeyspaceEvent(NOTIFY_LIST, "lpush", c->argv[2], c->db->id);
signalModifiedKey(c, c->db, c->argv[2]);
server.dirty++;
}Five conventional phases: parse arguments, look up the key, mutate, reply, notify-and-bump-dirty. The dirty counter is what triggers AOF flushing and replication; forgetting to bump it is a common bug. signalModifiedKey notifies tracking subscribers and Pub/Sub keyspace listeners.
The addReply* family (addReplyLongLong, addReplyBulk, addReplyError, addReplyArrayLen, …) is the only correct way to write to a client. It handles output-buffer growth, RESP2/RESP3 differences, and replication propagation. Direct write(2) is wrong.
Error handling
- Parse functions return
C_OK/C_ERRand emit a-ERRreply on failure if asked. The*OrReplyvariants do that automatically. - Type checks:
checkType(c, obj, OBJ_LIST)replies withWRONGTYPEand returns non-zero. - Out of memory: explicit OOM handling is rare. Most allocations call
serverPanicon failure (the OOM handler installed viazmalloc_set_oom_handler). Eviction is the user-facing OOM. - Asserts: use
serverAssertfor invariants andserverAssertWithInfo(c, o, cond)when a client/object context helps debug a failure.
RESP2 vs RESP3
The protocol negotiation happens via the HELLO command (src/networking.c). After HELLO 3 the client expects RESP3 frames. Most command handlers don't care because they call addReply*. When a reply shape differs between the two protocols (e.g. maps vs flat key-value arrays), the handler branches on c->resp == 3.
Locking
The dataset is single-writer. The places where multi-threading creeps in:
- BIO threads (
src/bio.c) — pop jobs from a per-thread queue. Jobs themselves never touch the dataset. - IO threads (
src/iothread.c) — read/write socket buffers. They do not run command logic; they only fill in the query buffer (parsed later by the main thread) and drain the output buffer. - Modules with thread-safe contexts (
src/module.c) — mustRedisModule_ThreadSafeContextLockbefore mutating data, which acquires the GIL-equivalent. - Replication backlog is updated under specific guarded paths.
If you are tempted to add a mutex anywhere else, the right answer is almost always "post a job to BIO".
Notifications and replication propagation
Commands that mutate a key must:
- Call
signalModifiedKey(c, db, key)to invalidate client-side cache trackers and informWATCH. - Call
notifyKeyspaceEvent(...)if the operation has a documented keyspace-notification class (seeredis.confnotify-keyspace-events). - Call
server.dirty++so persistence and replication know there's pending work. - Optionally call
propagate(...)if the command needs to be rewritten for the AOF/replicas (e.g.EXPIREbecomesPEXPIREAT).
alsoPropagate(...) is used when a command needs to emit additional commands (e.g. SET with EXPIRE flag becoming a SET + PEXPIREAT pair downstream).
Configuration
Configuration options are declared in a table in src/config.c. There are several macro families: createBoolConfig, createStringConfig, createEnumConfig, createSizeTConfig, createLongLongConfig, etc. Each entry includes:
- The user-facing name.
- The internal alias (often a
_get/_setaccessor). - Default value.
- Hidden flag (some options are diagnostic-only).
- An
applycallback if the change requires action (e.g. resizing the backlog).
After adding a row, document the option in redis.conf and ideally write a CONFIG SET/CONFIG GET test.
Cluster awareness
Most commands work transparently in cluster mode because the dispatcher in processCommand() routes by slot before invocation. Commands that touch multiple keys must declare key positions correctly in the JSON spec so the cluster code can validate that all keys live on the same slot. The CMD_NO_MULTI and CMD_ALLOW_BUSY flags in src/server.h let commands opt out of the standard checks.
If your command iterates the keyspace (e.g. KEYS, SCAN), be aware of which database / slot you're scanning. The kvstore API in src/kvstore.c exposes per-slot iteration.
Forks and copy-on-write
The persistence subsystem fork()s the parent. Anything in src/ should be careful to keep memory write patterns COW-friendly during BGSAVE — that's why object refcounts and shared integer objects are designed the way they are. Do not write to large pages just to flip a flag.
RESP wire-level conventions
- Strings are bulk strings (
$<len>\r\n<bytes>\r\n). - Numbers as integers (
:N\r\n) or RESP3 doubles, big numbers, booleans. - Errors begin with
-(-ERR ...,-WRONGTYPE ...,-NOAUTH ...,-MOVED ...,-ASK ...,-LOADING ...,-MASTERDOWN ...). - The full RESP3 type list is in
src/resp_parser.c.
The encoder helpers (addReply*) handle this for you. Don't write bytes by hand.
Tests
- New behaviour deserves a test. Failing tests should be a single change away from passing — large omnibus tests are a code smell.
- Tag tests with
slowif they exceed ~1 second. The CI fast lane skips slow tests; the daily lane runs them. assert_equal,assert_error,wait_for_conditionare the helper macros most tests use. Seetests/support/util.tcl.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.