Project 2: Key-Value Store Client Library

Build a robust C client library for a key-value server with explicit ownership and a consistent error model.

Quick Reference

Attribute Value
Difficulty Intermediate
Time Estimate 1-2 weeks
Main Programming Language C (Alternatives: Rust, Zig)
Alternative Programming Languages Rust, Zig
Coolness Level Level 6 - real client API boundary
Business Potential Level 6 - reusable network client foundation
Prerequisites Sockets, memory ownership, error handling
Key Topics TCP I/O, ownership contracts, error models

1. Learning Objectives

By completing this project, you will:

  1. Design a stable, misuse-resistant C API for a network client.
  2. Implement a robust TCP client with partial read/write handling.
  3. Define ownership rules for returned data and enforce them.
  4. Build a consistent error model that distinguishes “not found” vs “error.”
  5. Create tests that validate correct behavior under failure conditions.

2. All Theory Needed (Per-Concept Breakdown)

2.1 TCP Socket Lifecycle & Framing

Fundamentals

A TCP client is a state machine: create socket -> connect -> send request -> receive response -> close. Unlike file I/O, TCP is a stream with no message boundaries. This means you must design framing: a way to delimit where a response begins and ends. If you assume recv returns a full response, your client will break under load or on slower networks. You also must handle partial writes (send returns fewer bytes than requested) and partial reads (recv returns partial data). A correct client loops until the entire message is sent and the entire response is read. This is the core boundary between your library and the network.

Deep Dive into the Concept

TCP is a byte stream, not a packet system. The server might send your response in two separate network packets or coalesce multiple responses into one. Your client can’t rely on any single recv call to return the complete message. Thus, you need a protocol that defines message boundaries. A common approach is line-based framing (\n terminated commands and responses) or length-prefixed messages (send LEN:<n>\n followed by n bytes). The choice affects your parser and your API design. Line-based is easy but fragile for binary data; length-prefixed is more robust and handles arbitrary bytes.

Partial writes are just as important. send(fd, buf, len, 0) may return k < len. Your client must retry sending the remaining bytes, otherwise the server sees a truncated command. Likewise, recv may return less than you expect, or it may return 0, which indicates EOF (server closed the connection). You must treat EOF as a disconnection and propagate a meaningful error. Timeouts are another boundary. Without timeouts, your library can block forever. You should use setsockopt with SO_RCVTIMEO/SO_SNDTIMEO or use poll/select to enforce a deadline.

Framing and buffering interact with ownership. If you parse a response line by line, you’ll likely buffer data internally. That buffer may be reused across calls, which affects whether you can return borrowed pointers. If you want to return char* values to the caller, you must either allocate a new buffer per response (caller owns) or require the caller to copy data before the next call. The safest contract is: “values returned by kv_get are owned by the caller and must be freed with kv_free().” This avoids reentrancy problems and makes the API safe across calls.

Error handling in network I/O is subtle. EINTR means the system call was interrupted; you should retry. EAGAIN/EWOULDBLOCK means a non-blocking socket would block; you can retry or return a timeout error. ECONNRESET indicates the peer reset the connection. These errors must map into your library’s error model. A well-designed client never exposes raw errno values directly; it translates them into stable error codes and optionally provides kv_last_error() for debugging.

Finally, consider protocol consistency. If you design a simple protocol for your key-value server, define it precisely: allowed characters in keys, maximum key/value lengths, and response formats. A client library is not just “send strings”; it must enforce protocol invariants to prevent injection and to detect server bugs. For example, if the server says VALUE 5 but sends only 3 bytes, your client must treat that as a protocol error. The network boundary is adversarial by default; robust clients validate everything.

How This Fits in This Project

Your client’s wire protocol, buffer management, and retry logic are built on this concept. You’ll implement framing in Sec. 3.5, handle partial I/O in Sec. 5.10 Phase 2, and test protocol errors in Sec. 6.2. This concept also feeds into Project 5’s HTTP parsing. Also used in: Project 5.

Definitions & Key Terms

  • TCP stream -> Continuous byte stream with no message boundaries.
  • Framing -> Protocol rules for message boundaries.
  • Partial write/read -> send/recv transfers fewer bytes than requested.
  • EOF -> recv returns 0; peer closed connection.
  • Timeout -> Upper bound on wait time for I/O.

Mental Model Diagram (ASCII)

Client buffer -> send() -> [TCP stream] -> recv() -> parse frame
      ^                                           |
      |                                           v
  retry loop                                length/line parser

How It Works (Step-by-Step)

  1. Create socket and connect to server.
  2. Serialize request into buffer with a clear delimiter or length prefix.
  3. Send buffer with a loop until all bytes are written.
  4. Read response in chunks until framing rules indicate complete message.
  5. Parse response; validate format and lengths.
  6. Translate errors into library status codes.

Minimal Concrete Example

// Send all bytes
size_t off = 0;
while (off < len) {
    ssize_t n = send(fd, buf + off, len - off, 0);
    if (n < 0 && errno == EINTR) continue;
    if (n <= 0) return KV_ERR_IO;
    off += (size_t)n;
}

Common Misconceptions

  • recv returns a whole message.” -> It returns any available bytes.
  • “Line-based protocols are always safe.” -> They break on embedded newlines or binary values.
  • “Timeouts are optional.” -> They prevent permanent hangs.

Check-Your-Understanding Questions

  1. Why must you loop on send and recv?
  2. What is the difference between EOF and timeout?
  3. Why is framing essential for a TCP protocol?

Check-Your-Understanding Answers

  1. Because TCP is a stream and I/O can be partial.
  2. EOF means peer closed; timeout means no data within deadline.
  3. Without framing, you cannot detect message boundaries.

Real-World Applications

  • Redis and Memcached clients.
  • HTTP client libraries.
  • Database wire protocols.

Where You’ll Apply It

  • In this project: Sec. 3.5 (protocol), Sec. 5.10 Phase 2 (I/O loops), Sec. 6.2 (tests).
  • Also used in: Project 5.

References

  • “The Linux Programming Interface” - Ch. 56-59 (Sockets)
  • “UNIX Network Programming” by W. Richard Stevens - Vol. 1
  • man send, man recv, man setsockopt

Key Insight

TCP gives you bytes, not messages; your protocol and loops create correctness.

Summary

Correct socket clients require framing, partial I/O handling, and timeouts. These are the core techniques that make network boundaries predictable.

Homework/Exercises to Practice the Concept

  1. Write a client that sends a long message and handles partial writes.
  2. Simulate server delays and verify timeouts trigger cleanly.
  3. Implement both line-based and length-prefixed responses.

Solutions to the Homework/Exercises

  1. Use a loop around send until all bytes are written.
  2. Set SO_RCVTIMEO and return a timeout error.
  3. Parse \n for line-based; parse LEN header for length-prefixed.

2.2 Ownership & API Contracts in C

Fundamentals

Ownership is about who allocates and who frees memory. In a client library, ownership decisions must be unambiguous: if kv_get() returns a pointer, does the caller own it, or is it borrowed from an internal buffer? Ambiguity leads to leaks and use-after-free. A good contract uses explicit naming (kv_get_owned, kv_free_string) and stable types (opaque kv_handle). Const correctness communicates whether the caller can modify data. Together, these rules form the boundary contract between your library and its users.

Deep Dive into the Concept

C has no garbage collector or automatic ownership tracking. That means your API must make ownership explicit. The most robust pattern for a client library is to return owned data and provide a library-specific free function. Why not free() directly? Because your library might allocate from a custom allocator or attach metadata. Even if you use malloc now, providing kv_free_string() future-proofs the API and makes ownership obvious in code reviews.

Opaque handles are essential. If you expose struct kv_handle in your public header, any change to that struct breaks ABI. Instead, declare typedef struct kv_handle kv_handle; in the header and define the struct in the .c file. Users can only treat it as an opaque pointer, which stabilizes your ABI and lets you evolve internals freely. This also encodes ownership: the handle is created by kv_connect and destroyed by kv_disconnect. All functions that take a handle can assume it is valid only between those calls.

Const correctness further clarifies contracts. For input parameters like keys and values, use const char* to signal that the library will not modify them. For returned data, use const char* only if it is borrowed; otherwise return char* and document that the caller owns it. You should also define what happens on error: for example, kv_get() could return NULL and set an error code. But NULL could also represent “key not found.” To avoid ambiguity, return a status code and write the value via an output parameter. This pattern makes ownership and errors explicit and testable.

Another boundary is lifetime of returned data. If your library returns a pointer to an internal buffer that is reused on the next call, callers will experience data corruption. The safe policy is: each successful kv_get allocates a new buffer; the caller frees it with kv_free_string. This also makes the API thread-safe because concurrent calls do not share buffers. The cost is allocation overhead, but this is acceptable for correctness-first design. If you later want a zero-copy API, you can add a new function that returns a slice with documented lifetime constraints.

Finally, design for misuse resistance. Use names like kv_get_copy or kv_free_string to remind callers of ownership. Accept NULL pointers only where you can handle them safely, otherwise return KV_ERR_INVALID_ARG. Validate arguments aggressively at the API boundary. Defensive programming is part of the contract.

How This Fits in This Project

This concept drives your public header (kv.h), your handle design (Sec. 3.5), and your error model in Sec. 3.2. You will also test ownership contracts in Sec. 6.2. Ownership discipline here mirrors Project 3’s JSON tree and Project 4’s logging buffers. Also used in: Project 3, Project 4.

Definitions & Key Terms

  • Ownership -> Responsibility for freeing memory.
  • Opaque handle -> Type declared but not defined in header.
  • Borrowed pointer -> Memory owned elsewhere; must not be freed.
  • Const correctness -> Use const to signal immutability.

Mental Model Diagram (ASCII)

caller allocates key/value (borrowed)
    |
    v
kv_get(handle, key, &out_value)
    |
    v
library allocates new string -> caller owns -> kv_free_string()

How It Works (Step-by-Step)

  1. kv_connect allocates kv_handle and returns it to caller.
  2. kv_set borrows key/value and sends request.
  3. kv_get allocates a new buffer for the value.
  4. Caller frees with kv_free_string.
  5. kv_disconnect frees the handle and closes socket.

Minimal Concrete Example

kv_status kv_get(kv_handle *h, const char *key, char **out_value);
void kv_free_string(char *s);

Common Misconceptions

  • “Returning const char* means owned data.” -> It means borrowed.
  • free() is always fine.” -> API-specific free preserves ABI.
  • “Opaque types are overkill.” -> They protect ABI stability.

Check-Your-Understanding Questions

  1. Why should kv_get allocate a new buffer?
  2. What is the risk of exposing struct kv_handle in the header?
  3. How does const correctness prevent misuse?

Check-Your-Understanding Answers

  1. It avoids lifetime ambiguity and makes the API thread-safe.
  2. Any internal change breaks ABI and user code.
  3. It prevents accidental modification of borrowed inputs.

Real-World Applications

  • Database client libraries (PostgreSQL, MySQL).
  • HTTP libraries returning response bodies.

Where You’ll Apply It

  • In this project: Sec. 3.2 (API), Sec. 5.10 Phase 1 (handle design).
  • Also used in: Project 3, Project 4.

References

  • “Effective C” - Ch. 6 (Memory and Ownership)
  • “C Interfaces and Implementations” - Ch. 1

Key Insight

Clear ownership rules eliminate an entire class of API misuse bugs.

Summary

Ownership is a boundary contract. Opaque handles, explicit free functions, and const-correct signatures make your client library safe and stable.

Homework/Exercises to Practice the Concept

  1. Sketch an API that returns borrowed data and list potential bugs.
  2. Refactor it into an owned-data API.
  3. Write unit tests that assert ownership by freeing returned values.

Solutions to the Homework/Exercises

  1. Borrowed buffers can be invalidated on the next call.
  2. Allocate per call and return via output parameter.
  3. Verify valgrind reports no leaks.

2.3 Error Models & Resilient Client Behavior

Fundamentals

An error model defines how failures are reported, how detailed errors are retrieved, and what state remains after failure. For a client library, you must distinguish between “key not found,” “network error,” “protocol error,” and “invalid arguments.” Using a single error code or returning NULL for all failures is ambiguous. A good model uses a status enum plus a per-handle error string. It also guarantees that on failure, output parameters are set to safe values. This predictability makes the boundary reliable.

Deep Dive into the Concept

In C, the common error mechanisms are return codes, errno, and out-parameters. For a library, a return code enum is the clearest because it is stable and under your control. Define a kv_status enum with values like KV_OK, KV_ERR_CONN, KV_ERR_TIMEOUT, KV_ERR_PROTO, KV_ERR_NOT_FOUND, KV_ERR_INVALID_ARG. This makes failure modes explicit. Then define const char* kv_last_error(kv_handle*) to provide human-readable context. This string can be stored inside the handle, so it is thread-safe per handle.

Error models also define state transitions. If a network error occurs mid-request, is the connection still usable? A safe default is: if the protocol framing is compromised or the server closes the connection, mark the handle as “dirty” and require reconnect. This prevents subtle bugs where the client reads leftover bytes from a failed request. You can implement this by tracking a connected flag in the handle and returning KV_ERR_CONN on subsequent calls until the user reconnects.

You must also handle invalid input. If key is NULL or too long, return KV_ERR_INVALID_ARG immediately and do not touch network. This isolates boundary failures and improves debuggability. For “not found,” do not return NULL without status. Instead, return KV_ERR_NOT_FOUND and set *out_value = NULL. This avoids ambiguity and makes it easy to handle.

Mapping system errors to your enum requires care. For example, ECONNREFUSED maps to KV_ERR_CONN, ETIMEDOUT maps to KV_ERR_TIMEOUT, EPIPE (broken pipe) maps to KV_ERR_CONN. EINVAL might mean you used an invalid socket option, which is a programmer error; you can map it to KV_ERR_INTERNAL and include details in kv_last_error.

Finally, consider future extensibility. If you add new error codes later, old callers should still work. That means you should define a default case and avoid assuming the enum is exhaustive. Provide a stable numeric range and document it. This is how you preserve ABI and API contracts across versions.

How This Fits in This Project

Your error enum and kv_last_error function are central to Sec. 3.2 and Sec. 3.7. You’ll test error handling in Sec. 6.2 and debug error propagation in Sec. 7.1. The same approach is reused in Project 3’s parser errors and Project 5’s HTTP error responses. Also used in: Project 3, Project 5.

Definitions & Key Terms

  • Error model -> Rules for detecting and reporting failures.
  • Status code -> Enum representing specific failure types.
  • Error string -> Human-readable detail, often per-handle.
  • Fail-closed -> On error, invalidate state to prevent reuse.

Mental Model Diagram (ASCII)

kv_get() -> status code
          -> if error: last_error(handle)
          -> output pointer = NULL

How It Works (Step-by-Step)

  1. Validate inputs; return KV_ERR_INVALID_ARG on failure.
  2. Perform I/O; map system errors to kv_status.
  3. On success, set *out_value to newly allocated data.
  4. On failure, set *out_value = NULL and store error string.
  5. If protocol corruption occurs, mark handle disconnected.

Minimal Concrete Example

typedef enum {
    KV_OK = 0,
    KV_ERR_NOT_FOUND = 1,
    KV_ERR_CONN = -1,
    KV_ERR_TIMEOUT = -2,
    KV_ERR_PROTO = -3,
    KV_ERR_INVALID_ARG = -4
} kv_status;

Common Misconceptions

  • “NULL return is enough.” -> It hides “not found” vs “error.”
  • errno is a good API.” -> It is global and easy to misuse.
  • “Errors don’t affect state.” -> Protocol errors can corrupt the stream.

Check-Your-Understanding Questions

  1. Why distinguish NOT_FOUND from CONN errors?
  2. When should a handle be marked unusable after error?
  3. Why store error strings per handle?

Check-Your-Understanding Answers

  1. They require different caller actions (retry vs report missing key).
  2. When the protocol stream is corrupted or the peer disconnects.
  3. It’s thread-safe and avoids global state.

Real-World Applications

  • Redis client libraries returning REDIS_ERR vs REDIS_NIL.
  • HTTP libraries returning status codes plus error messages.

Where You’ll Apply It

  • In this project: Sec. 3.2 (API), Sec. 3.7 (outcome), Sec. 6.2 (tests).
  • Also used in: Project 3, Project 5.

References

  • “Fluent C” - Ch. 5 (Error handling)
  • “The Linux Programming Interface” - Ch. 6 (Errors)

Key Insight

A clear error model is a usability feature and a safety guarantee.

Summary

Return codes plus per-handle error strings create a predictable boundary. They allow callers to respond correctly without guesswork.

Homework/Exercises to Practice the Concept

  1. Design an enum that distinguishes four error types and NOT_FOUND.
  2. Write a test that forces a timeout and verifies error code.
  3. Simulate a protocol error and ensure the handle becomes unusable.

Solutions to the Homework/Exercises

  1. Use KV_ERR_* values and a separate KV_ERR_NOT_FOUND.
  2. Set short receive timeout and connect to a silent server.
  3. Inject a malformed response and mark handle disconnected.

3. Project Specification

3.1 What You Will Build

A C client library libkv that communicates with a simple key-value server using a line-based or length-prefixed protocol. The library exposes a stable API with explicit ownership rules and clear error handling, plus a demo CLI tool.

3.2 Functional Requirements

  1. Connect/Disconnect: Create and destroy a connection handle.
  2. SET/GET/DEL: Support basic operations.
  3. Timeouts: Configurable socket timeouts.
  4. Error Model: Return kv_status and kv_last_error.
  5. Ownership: Returned values are owned by caller and freed with kv_free_string.

3.3 Non-Functional Requirements

  • Performance: Handle 1,000 ops/sec on localhost.
  • Reliability: No leaks under valgrind.
  • Usability: Clear and stable API with documentation.

3.4 Example Usage / Output

Connected to 127.0.0.1:6379
SET user:1 -> OK
GET user:1 -> "Alice"
GET missing -> (not found)

3.5 Data Formats / Schemas / Protocols

Request format (line-based):

SET <key> <len>\n<value bytes>\n
GET <key>\n
DEL <key>\n

Response format:

OK\n
NOT_FOUND\n
VALUE <len>\n<value bytes>\n
ERR <code> <msg>\n

3.6 Edge Cases

  • Key contains whitespace or newline.
  • Server closes connection mid-response.
  • Response length mismatch.
  • Timeout during read or write.

3.7 Real World Outcome

A user can call kv_set and kv_get from C code and receive reliable status codes and owned data.

3.7.1 How to Run (Copy/Paste)

make
./demo --host 127.0.0.1 --port 6379

3.7.2 Golden Path Demo (Deterministic)

Use a test server that returns fixed values for user:1 and user:2.

3.7.3 If CLI: Exact Terminal Transcript

$ ./demo --host 127.0.0.1 --port 6379
Connected to 127.0.0.1:6379
SET user:1 -> OK
GET user:1 -> "Alice"
GET missing -> (not found)
$ echo $?
0

Failure demo (connection refused):

$ ./demo --host 127.0.0.1 --port 9999
Error [KV_ERR_CONN]: Connection refused
$ echo $?
2

Exit Codes:

  • 0 success
  • 2 connection error
  • 3 protocol error
  • 4 invalid arguments

4. Solution Architecture

4.1 High-Level Design

+--------------+    TCP    +--------------+
| libkv client |<--------->| kv server    |
| handle + API |           | simple text  |
+--------------+           +--------------+

4.2 Key Components

| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Connection handle | Socket + error state | Opaque struct, per-handle errors | | Protocol encoder | Serialize commands | Length-prefixed values | | Protocol decoder | Parse responses | Strict length validation |

4.3 Data Structures (No Full Code)

typedef struct {
    int fd;
    int connected;
    char last_error[256];
} kv_handle;

4.4 Algorithm Overview

Key Algorithm: kv_get

  1. Validate inputs.
  2. Send GET request with send-all loop.
  3. Read response header line.
  4. If VALUE, read length and value bytes.
  5. Allocate buffer and return owned string.

Complexity Analysis:

  • Time: O(n) for value length n.
  • Space: O(n) for returned buffer.

5. Implementation Guide

5.1 Development Environment Setup

cc --version
make --version

5.2 Project Structure

libkv/
|-- include/
|   `-- kv.h
|-- src/
|   |-- kv.c
|   |-- net.c
|   `-- protocol.c
|-- demo/
|   `-- demo.c
`-- Makefile

5.3 The Core Question You’re Answering

“When a function returns a pointer over a network boundary, how do you make ownership unambiguous?”

5.4 Concepts You Must Understand First

  1. TCP streams and framing.
  2. Ownership and opaque handles.
  3. Consistent error models.

5.5 Questions to Guide Your Design

  1. How will you distinguish “not found” from “error”?
  2. Who owns returned strings and how are they freed?
  3. What happens after a protocol error?

5.6 Thinking Exercise

Simulate a response with an incorrect length. What should the client do? Should it disconnect immediately?

5.7 The Interview Questions They’ll Ask

  1. How do you handle partial reads in TCP?
  2. Why use an opaque handle?
  3. How do you prevent ownership confusion?

5.8 Hints in Layers

Hint 1: Use output parameters

kv_status kv_get(kv_handle *h, const char *key, char **out);

Hint 2: Provide a library free

void kv_free_string(char *s);

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Sockets | “The Linux Programming Interface” | Ch. 56-59 | | Ownership | “Effective C” | Ch. 6 | | Error handling | “Fluent C” | Ch. 5 |

5.10 Implementation Phases

Phase 1: API and Handle (2-3 days)

Goals: define public API and error model. Tasks: write kv.h, define enums, implement kv_connect and kv_disconnect. Checkpoint: connect/disconnect without leaks.

Phase 2: Protocol and I/O (3-5 days)

Goals: implement send/recv loops and response parsing. Tasks: add kv_set, kv_get, kv_del and strict parsing. Checkpoint: demo works with a local server.

Phase 3: Hardening (2-3 days)

Goals: edge cases, timeouts, and testing. Tasks: add timeout config, simulate errors, add tests. Checkpoint: all tests pass; valgrind clean.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Response framing | line-based vs length-prefixed | length-prefixed | Handles arbitrary data safely | | Ownership model | borrowed vs owned | owned | Avoids lifetime ambiguity | | Error reporting | errno vs status enum | status enum | Clear, stable API |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | Protocol parsing | parse VALUE lines | | Integration Tests | Server interaction | set/get/del | | Failure Tests | Timeouts, disconnects | forced close |

6.2 Critical Test Cases

  1. Partial write: simulate short write; ensure full command sent.
  2. Malformed response: length mismatch -> KV_ERR_PROTO.
  3. Timeout: server delays -> KV_ERR_TIMEOUT.

6.3 Test Data

GET user:1 -> VALUE 5\nAlice\n
GET missing -> NOT_FOUND\n
ERR 100 invalid\n

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |——–|———|———-| | Reusing internal buffers | Corrupted results | Allocate per call | | No framing | Stuck reads | Define strict protocol | | Mixing errno and status codes | Confusing errors | Centralize error mapping |

7.2 Debugging Strategies

  • Use tcpdump or strace to see wire data.
  • Add a debug log of raw responses.

7.3 Performance Traps

  • Reallocating buffers on every chunk; use a growable buffer strategy.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add PING command.
  • Add a kv_last_status() function.

8.2 Intermediate Extensions

  • Add automatic reconnect.
  • Add configurable retry policy.

8.3 Advanced Extensions

  • Add async API with callbacks.
  • Add TLS support via OpenSSL.

9. Real-World Connections

9.1 Industry Applications

  • Cache clients for Redis/Memcached.
  • Service discovery clients.
  • hiredis - Redis C client with status codes.
  • libmemcached - Memcached client library.

9.3 Interview Relevance

  • Network I/O handling under load.
  • Ownership and error model design.

10. Resources

10.1 Essential Reading

  • “The Linux Programming Interface” - Ch. 56-59
  • “Effective C” - Ch. 6

10.2 Video Resources

  • “TCP Explained” - networking lecture series (searchable title)

10.3 Tools & Documentation

  • man socket, man connect, man send, man recv

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain how TCP framing works.
  • I can explain ownership in kv_get.
  • I can distinguish NOT_FOUND from ERR.

11.2 Implementation

  • All API functions meet requirements.
  • Errors are consistent and documented.
  • No leaks in demo under valgrind.

11.3 Growth

  • I can explain protocol design in an interview.
  • I documented pitfalls and fixes.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • kv_connect, kv_set, kv_get, kv_disconnect implemented.
  • Clear ownership rules and free function.
  • Error model documented and tested.

Full Completion:

  • Timeouts and protocol validation.
  • Demo CLI with clear success/failure outputs.
  • Tests covering error paths.

Excellence (Going Above & Beyond):

  • Async client API.
  • TLS or authentication support.