Skip to content

Commit f5b8816

Browse files
committed
feature: implement XREAD command
1 parent 8a5c6e8 commit f5b8816

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

app/main.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import threading
33
import time
44

5-
from app.resp_parser import bulk_array, bulk_int, bulk_stream_entries, bulk_string, decode_resp
5+
from app.resp_parser import bulk_array, bulk_int, bulk_stream_entries, bulk_string, bulk_xread_response, decode_resp
66

77
HOST = "localhost"
88
PORT = 6379
@@ -16,6 +16,7 @@
1616

1717
# Each stream entry: (id, {field: value, ...})
1818
stream_store: dict[str, list[tuple[str, dict[str, str]]]] = {}
19+
stream_condition = threading.Condition()
1920

2021

2122
_XADD_ID_ERROR = b"-ERR The ID specified in XADD is equal or smaller than the target stream top item\r\n"
@@ -141,8 +142,67 @@ def handle_connection(conn: socket.socket) -> None:
141142
conn.sendall(err)
142143
else:
143144
fields = dict(zip(args[3::2], args[4::2]))
144-
stream_store.setdefault(key, []).append((entry_id, fields))
145+
with stream_condition:
146+
stream_store.setdefault(key, []).append((entry_id, fields))
147+
stream_condition.notify_all()
145148
conn.sendall(bulk_string(entry_id))
149+
case "XREAD":
150+
i = 1
151+
count = None
152+
if args[i].upper() == "COUNT":
153+
count = int(args[i + 1])
154+
i += 2
155+
block_ms = None
156+
if args[i].upper() == "BLOCK":
157+
block_ms = int(args[i + 1])
158+
i += 2
159+
i += 1 # skip STREAMS
160+
remaining = args[i:]
161+
mid = len(remaining) // 2
162+
keys, start_ids = remaining[:mid], remaining[mid:]
163+
164+
def _parse_exclusive_id(id_str: str, key: str) -> tuple[int, int]:
165+
if id_str == "$":
166+
entries = stream_store.get(key, [])
167+
if entries:
168+
ms, seq = entries[-1][0].split("-")
169+
return (int(ms), int(seq))
170+
return (0, 0)
171+
ms, seq = id_str.split("-")
172+
return (int(ms), int(seq))
173+
174+
def _read_streams() -> list | None:
175+
result = []
176+
for key, start_id_str in zip(keys, start_ids):
177+
after = _parse_exclusive_id(start_id_str, key)
178+
entries = [
179+
e for e in stream_store.get(key, [])
180+
if (lambda p: (int(p[0]), int(p[1])))(e[0].split("-")) > after
181+
]
182+
if count is not None:
183+
entries = entries[:count]
184+
if entries:
185+
result.append((key, entries))
186+
return result if result else None
187+
188+
deadline = time.time() + block_ms / 1000 if block_ms is not None and block_ms > 0 else None
189+
with stream_condition:
190+
# Resolve $ IDs before blocking so they capture the current last ID
191+
resolved_ids = [_parse_exclusive_id(sid, k) for k, sid in zip(keys, start_ids)]
192+
start_ids = [f"{ms}-{seq}" for ms, seq in resolved_ids]
193+
194+
response = _read_streams()
195+
while response is None and block_ms is not None:
196+
remaining_time = max(0.0, deadline - time.time()) if deadline else None
197+
if remaining_time == 0.0:
198+
break
199+
stream_condition.wait(timeout=remaining_time)
200+
response = _read_streams()
201+
202+
if response:
203+
conn.sendall(bulk_xread_response(response))
204+
else:
205+
conn.sendall(b"$-1\r\n")
146206
case "XRANGE":
147207
def _parse_id(id_str: str, end: bool = False) -> tuple[float, float]:
148208
if id_str == "-":
@@ -159,7 +219,7 @@ def _parse_id(id_str: str, end: bool = False) -> tuple[float, float]:
159219
count = int(args[5]) if len(args) >= 6 and args[4].upper() == "COUNT" else None
160220
result = [
161221
e for e in entries
162-
if start <= tuple(int(x) for x in e[0].split("-")) <= end
222+
if start <= (lambda p: (int(p[0]), int(p[1])))(e[0].split("-")) <= end
163223
]
164224
if count is not None:
165225
result = result[:count]

app/resp_parser.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ def decode_resp(data: str) -> List[str]:
2525
return result
2626

2727

28+
def bulk_xread_response(streams: list[tuple[str, list[tuple[str, dict[str, str]]]]]) -> bytes:
29+
"""Encode XREAD response: array of [key, entries] pairs."""
30+
parts = [b"*" + str(len(streams)).encode() + b"\r\n"]
31+
for key, entries in streams:
32+
parts.append(b"*2\r\n")
33+
parts.append(bulk_string(key))
34+
parts.append(bulk_stream_entries(entries))
35+
return b"".join(parts)
36+
37+
2838
def bulk_stream_entries(entries: list[tuple[str, dict[str, str]]]) -> bytes:
2939
"""Encode a list of stream entries as a RESP array of [id, [field, value, ...]] pairs."""
3040
parts = [b"*" + str(len(entries)).encode() + b"\r\n"]

0 commit comments

Comments
 (0)