Pair Programming With AI: Writing a Distributed, Fault-Tolerant Redis Client Using ChatGPT
Building software with ChatGPT prompts
Software 2.0 is upon us. The fundamental nature of how we work, play, and socialize is going to change. Unless you’ve been living under a rock, you’ve likely been seeing cool demos and screenshots floating around the internet of ChatGPT. A common theme is that it’s great for creative use cases like writing stories, making jokes, explaining how bubble sort works like a 1940s Gangster would, etc. I wanted to see if we could use it for something more useful in my domain: to write some good ol’ Software 1.0.
As a “Staff Engineer”, a part of your routine work is to find ways to make yourself and the team more productive. That includes implementing the right abstractions, adopting useful tools/frameworks and working with and training senior/junior engineers, and growing them to a point where they can function without handholding.
So how should one view ChatGPT? Should I think of it as a useful tool/framework? Or a smart junior engineer who just needs some direction? Let’s find out.
Project Goals
My goals for this project are to gain insights into the following:
- Can I prototype something much faster than I usually would?
- Similar to a junior engineer, can I teach ChatGPT enough to not handhold it too much later on? (a.k.a. can I grow ChatGPT enough to get it a promotion)
- Lastly, I wanted to hopefully figure out at what level of complexity does ChatGPT begin to struggle.
Since this was my first attempt at having ChatGPT do some coding for me, I wanted to make sure I took on a problem of reasonable scope:
- Do something it may have already had a decent amount of exposure to: Redis is a system that has a plethora of documentation, code samples, and open source clients, and I’m sure ChatGPT would have trained on a lot of it. In fact, if you just ask it to implement a Redis Client from scratch, it’ll likely spit one out from memory which is very similar to any one of the implementations online.
- Add a decent amount of complexity by introducing new concepts:
Since it may have seen many Redis clients, let’s see if we can start with a simple client and then add the notion of replication (for fault-tolerance) and sharding (for scalability).
It likely would have learned what replication and sharding is, but not in the context of building our custom client, so we’ll have to direct it.
The result? A working distributed, fault-tolerant Redis client in Go completely written by ChatGPT that it can easily reimplement in other languages!
Note that this is a POC implementation that has poor code abstractions and potential bugs. The goal of this project is to see how we can use ChatGPT for coding, not write the 200th Redis Client, so please don’t scrutinize the code quality; the poor thing’s doing their best.
What are we trying to build?
We’re trying to build a Redis client that has fault tolerance and scalable key space, one that is at a very high level, similar to the original DynamoDB architecture.
Here’s a Redis Node.

To achieve fault tolerance, we add replication.

To achieve a scalable address space, we shard the keyspace by choosing the number of servers based on the size of the data set we want to support,. Then we run every key through some consistent hash algorithm to find which shard it belongs to.

We will support the GET
, SET
, and DELETE
operations.
On a SET
and DELETE
, our client library will choose the right shard for the given key, and send the SET
/DELETE
to all 3 replicas.

On a GET, our client library will will choose the right shard for the given key and read from any one replica of that shard. If the first replica returns an error, it will try the next one until any one of them return a successful value.

I chose Go because it’s syntactically not as complex as something like C++ and not too simple like Python. I wanted a fair enough challenge for ChatGPT.
Limitations: It’s a single-threaded client and it doesn’t do any logging or metrics. It’s a pure POC.
Now that we understand what we’re building, let’s get started.
Setting up a basic single-node client
The core part of writing a client is to support the wire protocol between the client and server. Redis has a very well-documented and simple protocol called RESP (Redis Serialization Protocol) that ChatGPT would have surely trained on.
I initially wanted to try explaining a brand new protocol to it and see if it understands it and can write a client for it. That might be more complex, but it may be doable. However, to keep it simple, I went with a well-known protocol.
So first, let’s confirm that it already understands RESP.
Note: My questions are in bold.
Do you understand the Redis Serialization Protocol?
Yes, I am familiar with the Redis Serialization Protocol (RESP). It is a protocol that specifies how Redis will encode its data structures and send them over the network to clients or between instances in a Redis Cluster. RESP is used to encode data structures such as strings, lists, sets, and hashes in a way that is efficient and easy to parse. It allows Redis to communicate with clients and other Redis instances in a consistent and predictable way.
It does! So let’s initialize our client:
I want you to write a simple Redis client library in Go. It should support the following to begin with:
1. It should take an IP:Port pair and connect to that server.
2. It should be able to handle the entire RESP protocol.
3. It should support the GET, SET and DELETE commands and expose APIs for each.
4. It should handle errors appropriately.Here’s a simple Go Redis client library that supports the GET, SET, and DELETE commands and uses the RESP protocol. The library takes an IP:Port pair and uses it to connect to the specified Redis server. It also includes error handling.
package main
import (
"bufio"
"bytes"
"errors"
"fmt"
"net"
"strconv"
)
const (
GET byte = iota
SET
DELETE
)
type RedisClient struct {
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
}
func NewRedisClient(ip string, port int) (*RedisClient, error) {
address := fmt.Sprintf("%s:%d", ip, port)
conn, err := net.Dial("tcp", address)
if err != nil {
return nil, err
}
return &RedisClient{
conn: conn,
reader: bufio.NewReader(conn),
writer: bufio.NewWriter(conn),
}, nil
}
func (c *RedisClient) Get(key string) (string, error) {
err := c.sendCommand(GET, key)
if err != nil {
return "", err
}
return c.readBulkString()
}
func (c *RedisClient) Set(key, value string) error {
err := c.sendCommand(SET, key, value)
if err != nil {
return err
}
_, err = c.readSimpleString()
return err
}
func (c *RedisClient) Delete(key string) error {
err := c.sendCommand(DELETE, key)
if err != nil {
return err
}
_, err = c.readSimpleString()
return err
}
func (c *RedisClient) Close() error {
return c.conn.Close()
}
func (c *RedisClient) sendCommand(command byte, args ...string) error {
c.writer.WriteByte('*')
c.writer.WriteString(strconv.Itoa(len(args) + 1))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteByte('$')
c.writer.WriteString(strconv.Itoa(1))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteByte(command)
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
for _, arg := range args {
c.writer.WriteByte('$')
c.writer.WriteString(strconv.Itoa(len(arg)))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteString(arg)
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
}
Here was my first challenge with ChatGPT. It zones out in the middle if the response is too large — possibly due to some word limit. Notice how sendCommand()
is incomplete.
Could you complete the implementation of sendCommand() ?
Sure. Here’s the completed sendCommand() function:
func (c *RedisClient) sendCommand(command byte, args ...string) error {
c.writer.WriteByte('*')
c.writer.WriteString(strconv.Itoa(len(args) + 1))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteByte('$')
c.writer.WriteString(strconv.Itoa(1))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteByte(command)
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
for _, arg := range args {
c.writer.WriteByte('$')
c.writer.WriteString(strconv.Itoa(len(arg)))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteString(arg)
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
}
return c.writer.Flush()
}
It also showed a blurb of how sendCommand()
works, as it does with almost every code change I asked, but to keep this post from becoming too long, I’m trimming out responses that are redundant. The full transcript is here if you’re interested in seeing exactly what it said. It’s long but interesting.
Next, I had it implement the readSimpleString() and readBulkString() commands. It ended up implementing readSimpleString() with bugs, so I had to ask it to re-implement it. It still came back with bugs, so I had to tell it the exact bug to fix.
readSimpleString() and readBulkString() are undefined. Could you implement both those functions? Remember to support the entire RESP protocol.
…
func (c *RedisClient) readSimpleString() (string, error) {
prefix, err := c.reader.ReadByte()
if err != nil {
return "", err
}
if prefix != '+' {
return "", errors.New("unexpected prefix")
}
line, err := c.reader.ReadString('\n')
if err != nil {
return "", err
}
return line[:len(line)-2], nil
}
readSimpleString()
is actually incomplete here.
readSimpleString() only supports ‘+’ from RESP. Could you also support ‘-’ ?
…
func (c *RedisClient) readSimpleString() (string, error) {
prefix, err := c.reader.ReadByte()
if err != nil {
return "", err
}
if prefix != '+' && prefix != '-' {
return "", errors.New("unexpected prefix")
}
line, err := c.reader.ReadString('\n')
if err != nil {
return "", err
}
return line[:len(line)-2], nil
}
It ended up “supporting” ‘-’, but not in the right way. In RESP, ‘-’ indicates an error and ‘+’ indicates a successful response. So I had to tell it to do so:
In readSimpleString(), if the ‘prefix’ is ‘-’, return the remaining line as an error.
…
func (c *RedisClient) readSimpleString() (string, error) {
prefix, err := c.reader.ReadByte()
if err != nil {
return "", err
}
if prefix != '+' && prefix != '-' {
return "", errors.New("unexpected prefix")
}
line, err := c.reader.ReadString('\n')
if err != nil {
return "", err
}
if prefix == '-' {
return "", errors.New(line[:len(line)-2])
}
return line[:len(line)-2], nil
}
It still didn’t get it right. If you read the code, it always reinterprets a success string and an error string as an error after ReadString()
. So I asked it to redo the whole function, but it came back with the exact same function. Here, I realized that I have to be more prescriptive, like a code review; because it wasn’t getting it.
Move the prefix == ‘-’ check before ReadString().
…
func (c *RedisClient) readSimpleString() (string, error) {
prefix, err := c.reader.ReadByte()
if err != nil {
return "", err
}
if prefix == '-' {
line, err := c.reader.ReadString('\n')
if err != nil {
return "", err
}
return "", errors.New(line[:len(line)-2])
}
if prefix != '+' {
return "", errors.New("unexpected prefix")
}
line, err := c.reader.ReadString('\n')
if err != nil {
return "", err
}
return line[:len(line)-2], nil
}
That’s great, so that means even though I had to be prescriptive, I could review its code like I would to a person and it understood me. You can see how the code quality is poor as it has some duplicated code in there, but it works. In the future, I want to see if I can teach it to write cleaner code, but for now, I didn’t want to confuse with too much context this early.
Ok great, now we have the initial client, but it doesn’t compile. The bug is that sendCommand()
takes command a const byte, and writes it out to the network as such, whereas the RESP protocol expects it to be a string.
Could you reimplement sendCommand() to take ‘command’ as a string?
…
func (c *RedisClient) sendCommand(command string, args ...string) error {
c.writer.WriteByte('*')
c.writer.WriteString(strconv.Itoa(len(args) + 1))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteByte('$')
c.writer.WriteString(strconv.Itoa(len(command)))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteString(command)
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
for _, arg := range args {
c.writer.WriteByte('$')
c.writer.WriteString(strconv.Itoa(len(arg)))
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
c.writer.WriteString(arg)
c.writer.WriteByte('\r')
c.writer.WriteByte('\n')
}
return c.writer.Flush()
}
Now, we need to update the callers of this function.
Now reimplement Get(), Set() and Delete() to use the new implementation of sendCommand()
…
func (c *RedisClient) Get(key string) (string, error) {
if err := c.sendCommand("GET", key); err != nil {
return "", err
}
return c.readBulkString()
}
func (c *RedisClient) Set(key, value string) error {
if err := c.sendCommand("SET", key, value); err != nil {
return err
}
_, err := c.readSimpleString()
return err
}
func (c *RedisClient) Delete(key string) error {
if err := c.sendCommand("DELETE", key); err != nil {
return err
}
_, err := c.readSimpleString()
return err
}
I wanted the contract of the APIs to always return 2 values. The first value as the result and the second value as an error. Get()
already defines its contract this way (albeit with a minor bug in its last line which I fixed manually). So here I had to be prescriptive again:
In both Set() and Delete(), readSimpleString() returns 2 values. The first one is a successful return value, the second one is an error string. Capture both of them and if the error string is not nil, return the error string, otherwise return the successful return value.
…
func (c *RedisClient) Set(key, value string) (string, error) {
if err := c.sendCommand("SET", key, value); err != nil {
return "", err
}
res, err := c.readSimpleString()
if err != nil {
return "", err
}
return res, nil
}
func (c *RedisClient) Delete(key string) (string, error) {
if err := c.sendCommand("DELETE", key); err != nil {
return "", err
}
res, err := c.readSimpleString()
if err != nil {
return "", err
}
return res, nil
}
Great! Now running this works against a single Redis server. We have a working Redis Client that supports Get()
, Set()
, and Delete()
. The code up to this point is available here if you want to try it out.
Adding Replication
The smoothness of the experience led me to think that the more I speak to it like I would a person, the more “structure” it seemed to have in its thinking.
This is quite incredible and it’s the power of in-context learning, which shows these “emergent properties” when Large Language models are trained on large enough data sets. It’s creepy because we still don’t know how it exhibits these properties. So far, we’ve just observed that it does. If what I mean by “structure” doesn’t make sense to you yet, I’ll point to a few examples later of exactly what I mean.
Hence, I ramped up my compliments and spoke about things like “high availability”, a term I never mentioned to it yet, hoping it would reinforce this “structure”. Over time, I wanted to speak to it in more abstract terms and be less prescriptive because if that worked, it would save a lot more time for me.
Great job! This works for me. Now assume that there are multiple Redis servers and we want the users of this client library to achieve high availability. I want NewRedisClient() to take a list of servers and connect to all of them. Note that one RedisClient object should support all the servers. I will tell you what to do with the other functions later.
Sure. Here’s an updated implementation of NewRedisClient() that takes a list of servers and connects to all of them:
func NewRedisClient(servers []string) (*RedisClient, error) {
var conns []net.Conn
for _, server := range servers {
conn, err := net.Dial("tcp", server)
if err != nil {
return nil, err
}
conns = append(conns, conn)
}
c := &RedisClient{conns: conns}
for _, conn := range conns {
c.readers = append(c.readers, bufio.NewReader(conn))
c.writers = append(c.writers, bufio.NewWriter(conn))
}
return c, nil
}
The updated NewRedisClient() function takes a list of servers as an argument. For each server in the list, it creates a new connection to that server using the net.Dial() function. Then, it creates a new RedisClient object and sets the list of connections as the conns field. For each connection, it creates a new bufio.Reader and a new bufio.Writer and adds them to the readers and writers fields of the RedisClient object, respectively. Finally, it returns the RedisClient object.
Pretty cool, but it didn’t rewrite the RedisClient
struct. I didn’t explicitly tell it to, but I implied it. However, notice that it has some notion of an updated RedisClient
because the way it’s used in the above function is different than how it was used before. So maybe it knows what the updated implementation of RedisClient
should look like but just didn’t show me? Let’s ask it.
Can you also show an updated implementation of RedisClient?
Sure. Here’s an updated implementation of the RedisClient struct that supports multiple servers:
The updated RedisClient struct has the following fields:
type RedisClient struct {
conns []net.Conn
readers []*bufio.Reader
writers []*bufio.Writer
}
conns: a slice of connections to the Redis servers.
readers: a slice of bufio.Reader objects associated with each connection in conns.
writers: a slice of bufio.Writer objects associated with each connection in conns.With this new struct, a RedisClient object can support multiple connections to multiple Redis servers.
Notice how I just asked for an updated implementation in that question. Although I had told it about multiple Redis servers previously, I didn’t say anything about it in that question. That’s what I meant by adding “structure”. Next, we’ll update the Get()
, Set()
, and Delete()
APIs.
Good job. Now I want you to update the implementation of Set() to write to all the servers. Keep a track of all the responses and errors. At the end of the function, if there is at least one successful response, return it. Otherwise, return any one error.
func (c *RedisClient) Set(key, value string) (string, error) {
var res string
var err error
for i, conn := range c.conns {
writer := c.writers[i]
reader := c.readers[i]
if err := sendCommand(conn, writer, "SET", key, value); err != nil {
return "", err
}
r, e := readSimpleString(conn, reader)
if e != nil {
err = e
} else {
res = r
}
}
if res != "" {
return res, nil
}
return "", err
}
Here’s a place where ChatGPT optimized my query of sorts. I mentioned “Keep a track of all the responses and errors”, but I also mentioned “At the end of the function, if there is at least one successful response, return it. Otherwise, return any one error.”
Notice how it only kept a track of at least one response and at least one error instead of storing them all in some list and then returning only one. That’s pretty astounding.
Good job. Now update sendCommand() and readSimpleString() accordingly.
Sure. Here are updated implementations of sendCommand() and readSimpleString() that take the connection, writer, and reader objects as arguments:
func sendCommand(conn net.Conn, writer *bufio.Writer, command string, args ...string) error {
writer.WriteByte('*')
writer.WriteString(strconv.Itoa(len(args) + 1))
writer.WriteByte('\r')
writer.WriteByte('\n')
writer.WriteByte('$')
writer.WriteString(strconv.Itoa(len(command)))
writer.WriteByte('\r')
writer.WriteByte('\n')
writer.WriteString(command)
writer.WriteByte('\r')
writer.WriteByte('\n')
for _, arg := range args {
writer.WriteByte('$')
writer.WriteString(strconv.Itoa(len(arg)))
writer.WriteByte('\r')
writer.WriteByte('\n')
writer.WriteString(arg)
writer.WriteByte('\r')
writer.WriteByte('\n')
}
return writer.Flush()
}
func readSimpleString(conn net.Conn, reader *bufio.Reader) (string, error) {
prefix, err := reader.ReadByte()
if err != nil {
return "", err
}
if prefix == '-' {
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return "", errors.New(line[:len(line)-2])
}
if prefix != '+' {
return "", errors.New("unexpected prefix")
}
line, err := reader.ReadString('\n')
if err != nil {
return "", err
}
return line[:len(line)-2], nil
}
It changed the contract of both functions from being a struct-level function of RedisClient
to a top-level function. I didn’t challenge this and wanted to see the subsequent code choices it would make.
Great job! Now I want you to update Delete() as well accordingly. It should attempt to delete from all the servers.
<Code omitted> It wrote delete with the exact same logic as Set(), which is what I wanted.
Awesome! Let’s update the implementation of Get() to get the value from any one server at a time. If a server returns a successful value, return it to the caller immediately, otherwise, try getting the value from the remaining servers one by one until any one of them returns a successful value. If all of them return an error, then return that error back to the caller.
func (c *RedisClient) Get(key string) (string, error) {
for i, conn := range c.conns {
writer := c.writers[i]
reader := c.readers[i]
if err := sendCommand(conn, writer, "GET", key); err != nil {
return "", err
}
res, err := readBulkString(conn, reader)
if err != nil {
continue
}
return res, nil
}
return "", errors.New("failed to get value from all servers")
}
It made some creative choices, but it got it right on the first try.
I updated readBulkString()
and Close()
as well. Close()
needed to close all the connections which it did:
func (c *RedisClient) Close() error {
var err error
for _, conn := range c.conns {
if e := conn.Close(); e != nil {
err = e
}
}
return err
}
Things were moving much smoother by this point. There were fewer corrections I had to make. The code with replication is available here.
Adding Sharding
I started speaking more abstractly now and I kept introducing new terms like “fault tolerance” while referring to the benefits of replication and it understood what I meant.
Okay great! What we’ve basically done here is add fault tolerance by introducing multiple replicas. However, we’re still limited by one server per replica. We want our redis client to also support multiple shards per replica so that our system is much more scalable.
To do that, first let’s update the RedisClient structure to be aware of replicas. Each replica should support multiple connections, one to each server within that replica.
type RedisClient struct {
conns [][]net.Conn
readers [][]*bufio.Reader
writers [][]*bufio.Writer
}
The updated RedisClient struct has the following fields:
conns: a slice of connections to the Redis servers, organized by replica. The conns field is a slice of slices, where each inner slice represents a replica and contains connections to the servers within that replica.readers: a slice of bufio.Reader objects associated with each connection in conns, organized by replica. The readers field is a slice of slices, where each inner slice represents a replica and contains bufio.Reader objects for the connections within that replica.
writers: a slice of bufio.Writer objects associated with each connection in conns, organized by replica. The writers field is a slice of slices, where each inner slice represents a replica and contains bufio.Writer objects for the connections within that replica.
With this new struct, a RedisClient object can support multiple replicas, each containing multiple connections to multiple Redis servers.
Awesome, now let’s update NewRedisClient() to take multiple replicas and initialize connections to all of them.
func NewRedisClient(replicas [][]string) (*RedisClient, error) {
var conns [][]net.Conn
var readers [][]*bufio.Reader
var writers [][]*bufio.Writer
for _, replica := range replicas {
var replicaConns []net.Conn
var replicaReaders []*bufio.Reader
var replicaWriters []*bufio.Writer
for _, server := range replica {
conn, err := net.Dial("tcp", server)
if err != nil {
return nil, err
}
replicaConns = append(replicaConns, conn)
replicaReaders = append(replicaReaders, bufio.NewReader(conn))
replicaWriters = append(replicaWriters, bufio.NewWriter(conn))
}
conns = append(conns, replicaConns)
readers = append(readers, replicaReaders)
writers = append(writers, replicaWriters)
}
return &RedisClient{conns, readers, writers}, nil
}
Good job. Now, let’s introduce a helper function called FindShard() that takes 2 arguments:
1. The key as a string
2. Number of servers within a replicaRun the key through XXHash pass the result and the number of servers through Jump Hash and return the output to the caller.
func FindShard(key string, numServers int) int {
h := xxhash.New()
if _, err := h.Write([]byte(key)); err != nil {
return 0
}
hash := h.Sum64()
return int(jump.Hash(hash, numServers))
}
Everything looks pretty good so far.
Good, now let’s update the implementation of Set() to first find the shard the key belongs to, and then send the command to only that shard from each replica.
func (c *RedisClient) Set(key, value string) error {
shard := FindShard(key, int32(len(c.conns[0])))
var err error
for i, replicaConns := range c.conns {
conn := replicaConns[shard]
writer := c.writers[i][shard]
if err = sendCommand(conn, writer, "SET", key, value); err != nil {
break
}
_, err = readSimpleString(conn, c.readers[i][shard])
}
return err
}
Nice, it chose the right shard for a key. I’m omitting some minor debugging and Delete(). Here’s Get() and Close():
Good job! Now, let’s update Get() to send the command to the respective shard one replica at a time. As soon as we get a successful result, return. Otherwise, keep trying the remaining replicas until we find one. If all of them return an error, return the last error seen.
func (c *RedisClient) Get(key string) (string, error) {
shard := FindShard(key, int32(len(c.conns[0])))
var result string
var err error
for i, replicaConns := range c.conns {
conn := replicaConns[shard]
writer := c.writers[i][shard]
if err = sendCommand(conn, writer, "GET", key); err != nil {
continue
}
result, err = readBulkString(conn, c.readers[i][shard])
if err == nil {
break
}
}
return result, err
}
Update Close() to close every connection. If you encounter an error midway, ignore it and continue closing everything else. If all of them hit an error, return the last one.
func (c *RedisClient) Close() error {
var err error
for _, replicaConns := range c.conns {
for _, conn := range replicaConns {
if err1 := conn.Close(); err1 != nil {
err = err1
}
}
}
return err
}
Now we have a complete working distributed and fault-tolerant client POC. Here is the Github link to the entire code and transcript.
Can ChatGPT Polyglot?
One of the common challenges in Tech Product companies is Polyglot support of infrastructure components. Typically that means clients with the same logic but in multiple different languages. This of course is burdensome to maintain and hence service meshes like Envoy that speak a common GRPC protocol are preferred.
Nevertheless, I was curious to see if all this “structure” we’ve built so far could allow ChatGPT to translate all the work we’ve put in, into a different language.
Now we have a working distributed, fault-tolerant Redis Client written in Go. I want you to implement the entire client with the same logic in Python.
import xxhash
import jumphash
import socket
import struct
class RedisClient:
def __init__(self, replicas):
self.conns = [[] for _ in replicas]
self.writers = [[] for _ in replicas]
self.readers = [[] for _ in replicas]
for i, servers in enumerate(replicas):
for server in servers:
ip, port = server.split(":")
conn = socket.socket()
conn.connect((ip, int(port)))
self.conns[i].append(conn)
self.writers[i].append(conn.makefile("w"))
self.readers[i].append(conn.makefile("r"))
@staticmethod
def find_shard(key, num_servers):
h = xxhash.xxh64()
h.update(key.encode("utf-8"))
return jumphash.jump_consistent(h.intdigest(), num_servers)
def set(self, key, value):
shard = self.find_shard(key, len(self.conns[0]))
result, err = None, None
for i, replica_conns in enumerate(self.conns):
conn = replica_conns[shard]
writer = self.writers[i][shard]
self.send_command(writer, "SET", key, value)
result, err = self.read_simple_string(conn)
return result, err
def delete(self, key):
shard = self.find_shard(key, len(self.conns[0]))
result, err = None, None
for i, replica_conns in enumerate(self.conns):
conn = replica_conns[shard]
writer = self.writers[i][shard]
self.send_command(writer, "DELETE", key)
result, err = self.read_simple_string(conn)
return result, err
Not bad at all. It’s incomplete, but alas here’s where I, unfortunately, hit the “Back” shortcut for my browser on my keyboard and I lost all context with ChatGPT :(
But we don’t need to see the rest of the code to come to a conclusion. ChatGPT can rewrite it in a different language seamlessly in one prompt. I’ve uploaded this incomplete file to GitHub as well. I hope OpenAI soon supports saving chat context within ChatGPT to pick back up later.
Conclusion
Given my experience, I believe that this can be used to speed up parts of software development; prototyping of simple components, building parts of a project in smaller modules, etc. It took me a little over 2 hours to get this working; I spent longer writing this post.
Giving it more context as I would to a real person seemed to help.
However, given the code quality issues and the fact that ChatGPT makes “creative” choices, means you have less control over things like how well the code performs. Code maintenance may also be harder.
Over time, as LLMs improve and are the result of training on much larger data sets, it may be the case that these “emergent properties” are more effective. Then could we envision these LLM’s reviewing their own code and likely writing better and better software, and even explaining how it works to us.
Learning to prompt these types of LLMs better could also be a very useful skill to develop and that may soon be a new job function. I’ll write up a future post on the techniques I learned to prompt ChatGPT better.
- GitHub repo: https://github.com/smukil/chatgptredisclient
- Twitter: https://twitter.com/SaileshMukil