Data Contracts
Radish uses a layered system of structs to represent commands, their execution, and results. Understanding these contracts is crucial to understanding how data flows through the system.
The Four Core Structs
1. Command — The Client Request
struct Command
name::String # Command name (e.g., "S_GET", "L_POP")
key::Union{Nothing, String} # Target key, or nothing for keyless commands
args::Vector{String} # Additional arguments
end
Purpose: Represents a parsed client request after RESP decoding.
Created by: The RESP protocol layer (read_resp_command in resp.jl)
Examples:
Command("S_GET", "mykey", []) # S_GET mykey
Command("S_SET", "mykey", ["hello", "60"]) # S_SET mykey hello 60
Command("L_RANGE", "mylist", ["1", "10"]) # L_RANGE mylist 1 10
Command("PING", nothing, []) # PING (no key)
Flow: Client → RESP decoder → Command → Dispatcher
2. CommandResult — Type Command Output
struct CommandResult
success::Bool # Did the operation succeed?
value::Any # Result value (for reads/modifications)
error::Union{Nothing, String} # Error message if success=false
element::Union{RadishElement, Nothing} # New element (for creators only)
end
Purpose: Returned by type commands (like sget, sincr!, lpop!) to indicate success or failure at the data structure level.
Created by: Type commands in rstrings.jl, rlinkedlists.jl
Convenience constructors:
CommandSuccess(value) # Success with a return value
CommandError(msg::String) # Failure with error message
CommandCreate(elem::RadishElement) # Success, created new element
Examples:
# String get operation
CommandSuccess("hello")
# Increment operation
CommandSuccess(true)
# Create new string
CommandCreate(RadishElement("world", nothing, now(), :string))
# Error: value is not an integer
CommandError("Value 'abc' is not an integer")
Flow: Type command → CommandResult → Hypercommand
3. ExecuteResult — Hypercommand Output
struct ExecuteResult
status::ExecutionStatus # SUCCESS, KEY_NOT_FOUND, or ERROR
value::Any # Result to return to client
error::Union{Nothing, String} # Error message (only for ERROR status)
end
Purpose: Returned by hypercommands (like rget_or_expire!, radd!) after handling TTL checks, key lookup, and delegation to type commands.
Created by: Hypercommands in radishelem.jl
Status enum:
@enum ExecutionStatus begin
SUCCESS # Command executed successfully
KEY_NOT_FOUND # Key doesn't exist or expired
ERROR # Wrong command, wrong type, bad arguments
end
Examples:
# Successful read
ExecuteResult(SUCCESS, "hello", nothing)
# Key not found or expired
ExecuteResult(KEY_NOT_FOUND, nothing, nothing)
# Type mismatch error
ExecuteResult(ERROR, nothing, "WRONGTYPE: Key 'mykey' holds a list, not a string")
Flow: Hypercommand → ExecuteResult → Dispatcher → RESP encoder
4. ExecutionStatus — The Final Verdict
@enum ExecutionStatus begin
SUCCESS # Command executed, value returned
KEY_NOT_FOUND # Key doesn't exist or expired
ERROR # Wrong type, bad arguments, etc.
end
Purpose: A simple enum to categorize the outcome of a command execution.
Used by: ExecuteResult to indicate the high-level status
Mapping to RESP responses:
| ExecutionStatus | RESP Response | Example |
|---|---|---|
SUCCESS | +OK, :integer, $bulk, *array | +OK\r\n, :42\r\n, $5\r\nhello\r\n |
KEY_NOT_FOUND | $-1\r\n (nil) | Client sees (nil) |
ERROR | -ERR message\r\n | -ERR WRONGTYPE ...\r\n |
The Complete Flow
Here’s how these structs interact during a command execution: (I know this chart is a bit hard to see, please zoom in, this is not a static image I promise you will see the details after zooming)
sequenceDiagram
participant Client
participant RESP as RESP Layer
participant Dispatcher
participant Palette as Palette (S_PALETTE)
participant Hypercommand as Hypercommand (rget_or_expire!)
participant TypeCommand as Type Command (sget)
participant Context as RadishContext
Client->>RESP: Raw bytes — "S_GET mykey\r\n"
Note over RESP: Parses RESP protocol<br/>Builds Command struct
RESP->>Dispatcher: Command("S_GET", "mykey", [])
Note over Dispatcher: name = "S_GET"<br/>key = "mykey"<br/>args = []
Dispatcher->>Palette: Lookup S_PALETTE["S_GET"]
Palette-->>Dispatcher: (sget, rget_or_expire!)
Note over Dispatcher: Acquires read lock<br/>on mykey's shard
Dispatcher->>Hypercommand: rget_or_expire!(ctx, "mykey", sget)
Hypercommand->>Context: haskey(ctx, "mykey") ?
Note over Hypercommand,Context: Also checks TTL:<br/>is tinit + ttl > now() ?
alt Key exists and TTL valid
Hypercommand->>TypeCommand: sget(element)
Note over TypeCommand: element.value = "hello"<br/>element.datatype = :string
TypeCommand-->>Hypercommand: CommandResult(success=true, value="hello", error=nothing, element=nothing)
Hypercommand-->>Dispatcher: ExecuteResult(SUCCESS, "hello", nothing)
else Key missing or expired
Note over Hypercommand: Deletes key if expired
Hypercommand-->>Dispatcher: ExecuteResult(KEY_NOT_FOUND, nothing, nothing)
end
Note over Dispatcher: Releases read lock<br/>on mykey's shard
Dispatcher->>RESP: ExecuteResult(SUCCESS, "hello", nothing)
Note over RESP: status = SUCCESS<br/>→ encode as Bulk String
RESP->>Client: "$5\r\nhello\r\n"
Conversion Rules
CommandResult → ExecuteResult
Hypercommands convert CommandResult to ExecuteResult:
cmd_result = command(element, args...)
if !cmd_result.success
return ExecuteResult(ERROR, nothing, cmd_result.error)
end
return ExecuteResult(SUCCESS, cmd_result.value, nothing)
Key insight: CommandResult.success=false always becomes ExecuteResult(ERROR, ...)
ExecuteResult → RESP
The RESP encoder (write_resp_response in resp.jl) converts ExecuteResult to wire format:
if result.status == ERROR
write(sock, "-ERR $(result.error)\r\n")
elseif result.status == KEY_NOT_FOUND
write(sock, "$-1\r\n") # nil
elseif result.status == SUCCESS
# Format based on value type (string, integer, array, etc.)
end
Why This Layered Design?
- Separation of concerns:
- Type commands focus on data structure logic
- Hypercommands handle cross-cutting concerns (TTL, key lookup)
- Dispatcher handles routing and locking
- RESP layer handles wire protocol
- Type safety:
CommandResultis internal (type command ↔ hypercommand)ExecuteResultis the public contract (hypercommand ↔ dispatcher ↔ client)
- Extensibility:
- Adding a new data type only requires implementing type commands that return
CommandResult - The rest of the system (hypercommands, dispatcher, RESP) remains unchanged
- Adding a new data type only requires implementing type commands that return
- Error propagation:
- Type-level errors (e.g., “value is not an integer”) flow through
CommandResult.error - System-level errors (e.g., “key not found”) are handled by hypercommands
- Both eventually become
ExecuteResultfor the client
- Type-level errors (e.g., “value is not an integer”) flow through
Common Patterns
Pattern 1: Read Operation
# Type command (rstrings.jl)
function sget(elem::RadishElement)
return CommandSuccess(elem.value)
end
# Hypercommand (radishelem.jl)
function rget_or_expire!(context, key, command, args...)
if haskey(context, key)
element = context[key]
# TTL check...
cmd_result = command(element, args...)
return ExecuteResult(SUCCESS, cmd_result.value, nothing)
end
return ExecuteResult(KEY_NOT_FOUND, nothing, nothing)
end
Pattern 2: Write Operation with Validation
# Type command (rstrings.jl)
function sincr!(elem::RadishElement)
elem_n = tryparse(Int, string(elem.value))
if elem_n === nothing
return CommandError("Value '$(elem.value)' is not an integer")
end
elem_n += 1
elem.value = string(elem_n)
return CommandSuccess(true)
end
# Hypercommand (radishelem.jl)
function rmodify!(context, key, command, args...)
if haskey(context, key)
cmd_result = command(context[key], args...)
if !cmd_result.success
return ExecuteResult(ERROR, nothing, cmd_result.error)
end
return ExecuteResult(SUCCESS, cmd_result.value, nothing)
end
return ExecuteResult(KEY_NOT_FOUND, nothing, nothing)
end
Pattern 3: Creator Operation
# Type command (rstrings.jl)
function sadd(value::String, ttl::String)
ttl_p = tryparse(Int, ttl)
if ttl_p === nothing
return CommandError("TTL must be a valid integer")
end
elem = RadishElement(value, ttl_p, now(), :string)
return CommandCreate(elem)
end
# Hypercommand (radishelem.jl)
function radd!(context, key, command, args...)
if haskey(context, key)
return ExecuteResult(ERROR, nothing, "Key '$key' already exists")
end
cmd_result = command(args...)
if !cmd_result.success
return ExecuteResult(ERROR, nothing, cmd_result.error)
end
context[key] = cmd_result.element
return ExecuteResult(SUCCESS, true, nothing)
end
Summary Table
| Struct | Created By | Used By | Purpose |
|---|---|---|---|
Command | RESP decoder | Dispatcher | Parsed client request |
CommandResult | Type commands | Hypercommands | Type-level operation result |
ExecuteResult | Hypercommands | Dispatcher, RESP encoder | System-level execution result |
ExecutionStatus | Hypercommands | RESP encoder | High-level status category |
The golden rule: Type commands return CommandResult, hypercommands return ExecuteResult, and the dispatcher routes everything.