Open-Source Wikis

/

Redis

/

How to contribute

/

Patterns and conventions

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_case or short i, j, n.
    • Struct types: lowerCamelCase (e.g. client, redisDb, redisObject).
  • Headers: declare functions with static if file-local; never expose internals. Public APIs live in src/server.h or 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/zstrdup instead 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 by sdsnew()/sdscatprintf() etc. See SDS. Free with sdsfree.
  • Refcount wins over copy. When a value can be shared, increment its refcount (incrRefCount(robj*)). Shared integer objects are pre-allocated in server.shared and 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. See src/lazyfree.c.
  • No allocations in signal handlers. Use the serverLogFromHandler path; it pre-formats with a static buffer and calls write(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_ERR and emit a -ERR reply on failure if asked. The *OrReply variants do that automatically.
  • Type checks: checkType(c, obj, OBJ_LIST) replies with WRONGTYPE and returns non-zero.
  • Out of memory: explicit OOM handling is rare. Most allocations call serverPanic on failure (the OOM handler installed via zmalloc_set_oom_handler). Eviction is the user-facing OOM.
  • Asserts: use serverAssert for invariants and serverAssertWithInfo(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) — must RedisModule_ThreadSafeContextLock before 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:

  1. Call signalModifiedKey(c, db, key) to invalidate client-side cache trackers and inform WATCH.
  2. Call notifyKeyspaceEvent(...) if the operation has a documented keyspace-notification class (see redis.conf notify-keyspace-events).
  3. Call server.dirty++ so persistence and replication know there's pending work.
  4. Optionally call propagate(...) if the command needs to be rewritten for the AOF/replicas (e.g. EXPIRE becomes PEXPIREAT).

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/_set accessor).
  • Default value.
  • Hidden flag (some options are diagnostic-only).
  • An apply callback 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 slow if they exceed ~1 second. The CI fast lane skips slow tests; the daily lane runs them.
  • assert_equal, assert_error, wait_for_condition are the helper macros most tests use. See tests/support/util.tcl.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Redis wiki | Factory