Server Runtime
This chapter follows the executable entrypoint, the shared Server object, and the per-connection loop that every socket eventually enters.
File boundaries
src/main.rssrc/server.rssrc/options.rs
Runtime map
The runtime is intentionally compact. There is no separate acceptor service, scheduler, or replication supervisor.
The top-level flow is:
1
2
3
4
5
6
7
8
CLI args
-> DBOption / ReplicationOption
-> Server::new(...)
-> Server::init(...)
-> optional follower replication bootstrap
-> TcpListener accept loop
-> one Tokio task per socket
-> Server::handle(...)
That shape matters because almost every subsystem in the repo is reached from this one path.
Executable entrypoint
src/main.rs owns the only binary entrypoint.
It parses four CLI arguments:
--dir--dbfilename--port--replicaof
Those values are assembled into DBOption and ReplicationOption before any server state exists.
The process role is therefore fixed at startup:
- no
--replicaof-> master - with
--replicaof-> slave
There is no later promotion or role negotiation.
Configuration objects
src/options.rs keeps configuration deliberately small.
DBOption carries:
dirdb_file_namereplicationport
ReplicationOption carries:
rolemaster_replidmaster_repl_offsetreplica_of
Two details are worth keeping in mind:
master_replidis hard-coded inmain.rsinstead of being generated dynamically.master_repl_offsetis part of configuration, not the live shared counter used by command execution.
That second split explains why INFO replication and REPLCONF GETACK do not report the same source of truth later in the code.
What a Server clone actually shares
Server derives Clone, but it is not a deep independent copy.
The fields break down into two groups.
Shared across tasks through Arc:
storage: Arc<Mutex<Storage>>streams: Arc<Mutex<HashMap<String, Stream>>>offset: Arc<AtomicU64>master_repl_clients: Arc<Mutex<Option<MasterReplicationClient>>>stream_reader_blocker: Arc<Mutex<Vec<Sender<()>>>>
Copied by value into each task-local Server clone:
option: DBOptionmaster_addr: Option<String>
So every accepted connection gets its own lightweight orchestration shell, but all meaningful mutable runtime state is shared.
Server::new(...)
Server::new(...) in src/server.rs does three things:
- derive
master_addrfromreplica_ofwhen the role is slave - allocate the shared containers
- call
init().await
The master-only fan-out client list is created only when the role is master:
- master ->
Some(MasterReplicationClient::new()) - slave ->
None
This is one of the clearer ownership decisions in the repo. Follower nodes never carry unused downstream-replica machinery.
Server::init(...)
init(...) is the runtime bootstrap hook, but today it only performs master-side local persistence restore.
The control flow is:
- check
self.is_master() - build
dir/dbfilename - open the file with create-if-missing semantics
- inspect file length
- if non-zero, call
rdb::parse_rdb_file(...)
Slave nodes skip this whole branch. Their initial state is expected to come from the upstream master during replication bootstrap instead of local file restore.
That asymmetry is intentional and visible in code, not hidden behind one generic storage-init path.
Follower bootstrap stays in main.rs
Slave startup is not buried inside Server::new(...).
main.rs keeps the sequence explicit:
- build
Server - clone it into
sc - call
get_follower_repl_client(...) ping_master()report_port(server.option.port)report_sync_protocol()start_psync(&mut sc)- spawn
sc.handle(follower_repl_client.stream, true)
This is good for code reading because the role-dependent startup path is still visible at the top level.
get_follower_repl_client(...)
Server::get_follower_repl_client(...) is a small role gate.
- slave -> create
FollowerReplicationClient - master -> return
None
It does not cache the upstream connection. It creates it on demand during startup and hands the socket back to main.rs.
Listener and concurrency model
After startup, main.rs binds one Tokio TcpListener on 127.0.0.1:<port>.
For every accepted socket it:
- clones the shared
Server - spawns one Tokio task
- calls
Server::handle(stream, false)
The concurrency model is therefore:
- one listener
- one async task per socket
- no central request queue
- shared mutable state guarded by
Mutex
That keeps the runtime readable, but it also means lock boundaries directly shape behavior.
The per-connection loop in Server::handle(...)
Server::handle(...) is where the ordinary client path and replication-socket path finally converge.
The loop currently works like this:
- read into a fixed
512byte buffer - stop if
len == 0 - interpret the bytes as UTF-8 with
str::from_utf8(...) - parse one command with
Cmd::from(...) - run it with
cmd.run(...) - if this is not a replication connection, write the encoded response back
- if the server is master and the command was
PSYNC, switch this socket into downstream-replica mode
There is also a task-local transaction queue:
1
queued_cmd: Option<Vec<(Cmd, Protocol)>>
That queue is created and consumed entirely inside the connection loop, which is why transactions are per-client-session rather than globally visible.
Normal request data flow
For an ordinary client connection, the data path is:
1
2
3
4
5
6
socket bytes
-> Server::handle
-> Cmd::from
-> Cmd::run
-> Protocol response
-> stream.write(...)
Server mostly orchestrates. It does not implement command semantics itself.
Replication-socket mode switch
PSYNC is special because it is both:
- a command with a logical response
- a connection-state transition
The split is visible in handle(...):
cmd.run(...)returnsFULLRESYNC ...- because
is_rep_conn == false, that response is written to the socket handle(...)seesCmd::PsyncMasterReplicationClient::send_rdb_file(...)writes the snapshot payloadMasterReplicationClient::add_stream(...)stores the socket for later fan-out- the normal request loop breaks
After that point, the socket stops being a normal request/response connection and becomes a registered replica downstream.
Locking and ownership boundaries
The runtime uses a few coarse-grained locks:
- all string-key storage behind one
Mutex<Storage> - all streams behind one
Mutex<HashMap<...>> - all downstream replica sockets behind one
Mutex<Vec<TcpStream>> - all blocked stream readers behind one
Mutex<Vec<Sender<()>>>
This keeps the code straightforward, but the granularity is intentionally broad. The design optimizes for readability over parallel fine-grained coordination.
Current implementation limits
handle(...)assumes a full command fits in one read and one512byte buffer- there is no incremental framing across partial socket reads
- input is treated as UTF-8 text early, not as raw bytes all the way through
- there is no structured shutdown or task supervision tree
- follower bootstrap has no reconnect loop if the master later disappears
- connection mode is inferred from ad hoc branches rather than a dedicated state enum
The runtime is small enough to trace in one sitting, but the docs should be read as documentation of the current implementation, not of a fuller production Redis architecture.