Skip to content

Commit f2df1aa

Browse files
authored
Add files via upload
1 parent aa37177 commit f2df1aa

File tree

5 files changed

+153
-26
lines changed

5 files changed

+153
-26
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea
2+
app/test.py
3+
__pycache__

README.md

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
2-
# Python Redis Server
1+
# Redis Clone
32

43
This is a simple implementation of a Redis clone written in Python.
54
It is not meant to be a fully-featured Redis server, but rather a demonstration of how the Redis protocol works.
5+
I developed it following the challenge on the wonderful website [CodeCrafters](https://codecrafters.io/).
66

77
## Features
88

@@ -39,27 +39,4 @@ This Redis clone has several limitations compared to a real Redis server:
3939

4040
- Only a limited set of commands are implemented.
4141
- The database is not persisted to disk, so all data is lost when the server is stopped.
42-
- There is no support for multiple databases or authentication.
43-
44-
## Common Bug
45-
46-
When running your server locally, you might see an error like this:
47-
48-
```
49-
Traceback (most recent call last):
50-
File "/.../python3.7/runpy.py", line 193, in _run_module_as_main
51-
"__main__", mod_spec)
52-
File "/.../python3.7/runpy.py", line 85, in _run_code
53-
exec(code, run_globals)
54-
File "/app/app/main.py", line 11, in <module>
55-
main()
56-
File "/app/app/main.py", line 6, in main
57-
s = socket.create_server(("localhost", 6379), reuse_port=True)
58-
AttributeError: module 'socket' has no attribute 'create_server'
59-
```
60-
61-
This is because `socket.create_server` was introduced in Python 3.8, and you
62-
might be running an older version.
63-
64-
You can fix this by installing Python 3.8 locally and using that.
65-
42+
- There is no support for multiple databases or authentication.

app/main.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import socket
2+
import threading
3+
import time
4+
5+
from .resp_decoder import RESPDecoder
6+
7+
database = {}
8+
9+
10+
def handle_ping(client_connection):
11+
client_connection.send(b"+PONG\r\n")
12+
13+
14+
def handle_echo(client_connection, args):
15+
client_connection.send(b"$%d\r\n%b\r\n" % (len(args[0]), args[0]))
16+
17+
18+
def handle_set(client_connection, args):
19+
expiry = None
20+
# Check for "px" argument and extract expiry value
21+
if b"px" in args:
22+
expiry_index = args.index(b"px") + 1
23+
expiry = int(args[expiry_index])
24+
expiry = int(time.time() * 1000) + expiry
25+
args = args[: expiry_index - 1] + args[expiry_index + 1 :]
26+
27+
database[args[0]] = (args[1], expiry)
28+
client_connection.send(b"+OK\r\n")
29+
30+
31+
def handle_get(client_connection, args):
32+
key = args[0]
33+
entry = database.get(key)
34+
35+
if entry is None:
36+
client_connection.send(b"$-1\r\n")
37+
return
38+
39+
value, expiry = entry
40+
if expiry is not None and expiry <= int(time.time() * 1000):
41+
del database[key]
42+
client_connection.send(b"$-1\r\n")
43+
else:
44+
client_connection.send(b"$%d\r\n%b\r\n" % (len(value), value))
45+
46+
47+
def handle_connection(client_connection):
48+
while True:
49+
try:
50+
command, *args = RESPDecoder(client_connection).decode()
51+
command = command.decode("ascii").lower()
52+
if command == "ping":
53+
handle_ping(client_connection)
54+
elif command == "echo":
55+
handle_echo(client_connection, args)
56+
elif command == "set":
57+
handle_set(client_connection, args)
58+
elif command == "get":
59+
handle_get(client_connection, args)
60+
else:
61+
client_connection.send(b"-ERR unknown command\r\n")
62+
except ConnectionError:
63+
break # Stop serving if the client connection is closed
64+
65+
66+
def main():
67+
server_socket = socket.create_server(("localhost", 6379), reuse_port=True)
68+
69+
while True:
70+
client_connection, _ = server_socket.accept() # wait for client
71+
threading.Thread(target=handle_connection, args=(client_connection,)).start()
72+
73+
74+
if __name__ == "__main__":
75+
main()

app/resp_decoder.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# ConnectionBuffer wraps socket.Socket and adds support for reading until a delimiter.
2+
class ConnectionBuffer:
3+
def __init__(self, connection):
4+
self.connection = connection
5+
self.buffer = b''
6+
7+
def read_until_delimiter(self, delimiter):
8+
while delimiter not in self.buffer:
9+
data = self.connection.recv(1024)
10+
11+
if not data: # socket closed
12+
return None
13+
14+
self.buffer += data
15+
16+
data_before_delimiter, delimiter, self.buffer = self.buffer.partition(delimiter)
17+
return data_before_delimiter
18+
19+
def read(self, bufsize):
20+
if len(self.buffer) < bufsize:
21+
data = self.connection.recv(1024)
22+
23+
if not data: # socket closed
24+
return None
25+
26+
self.buffer += data
27+
28+
data, self.buffer = self.buffer[:bufsize], self.buffer[bufsize:]
29+
return data
30+
31+
32+
class RESPDecoder:
33+
def __init__(self, connection):
34+
self.connection = ConnectionBuffer(connection)
35+
36+
def decode(self):
37+
data_type_byte = self.connection.read(1)
38+
39+
if data_type_byte == b"+":
40+
return self.decode_simple_string()
41+
elif data_type_byte == b"$":
42+
return self.decode_bulk_string()
43+
elif data_type_byte == b"*":
44+
return self.decode_array()
45+
else:
46+
raise Exception(f"Unknown data type byte: {data_type_byte}")
47+
48+
def decode_simple_string(self):
49+
return self.connection.read_until_delimiter(b"\r\n")
50+
51+
def decode_bulk_string(self):
52+
bulk_string_length = int(self.connection.read_until_delimiter(b"\r\n"))
53+
data = self.connection.read(bulk_string_length)
54+
assert self.connection.read_until_delimiter(b"\r\n") == b"" # delimiter should be immediately after string
55+
return data
56+
57+
def decode_array(self):
58+
result = []
59+
array_length = int(self.connection.read_until_delimiter(b"\r\n"))
60+
61+
for _ in range(array_length):
62+
result.append(self.decode())
63+
64+
return result

redis-clone

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
#
3+
# DON'T EDIT THIS!
4+
#
5+
# CodeCrafters uses this file to test your code. Don't make any changes here!
6+
#
7+
# DON'T EDIT THIS!
8+
exec python3 -m app.main

0 commit comments

Comments
 (0)