kedis-python 0.1.0__tar.gz → 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {kedis_python-0.1.0 → kedis_python-0.1.1}/PKG-INFO +4 -2
  2. kedis_python-0.1.1/kedis_python/__init__.py +0 -0
  3. kedis_python-0.1.1/kedis_python/benchmark.py +65 -0
  4. kedis_python-0.1.1/kedis_python/benchmark_asyncio.py +33 -0
  5. kedis_python-0.1.1/kedis_python/commands.py +512 -0
  6. kedis_python-0.1.1/kedis_python/kesp_cli.py +140 -0
  7. kedis_python-0.1.1/kedis_python/main.py +751 -0
  8. kedis_python-0.1.1/kedis_python/network.py +60 -0
  9. kedis_python-0.1.1/kedis_python/parser.py +132 -0
  10. kedis_python-0.1.1/kedis_python/replica.py +454 -0
  11. kedis_python-0.1.1/kedis_python/server.py +514 -0
  12. kedis_python-0.1.1/kedis_python/simulator.py +46 -0
  13. kedis_python-0.1.1/kedis_python/skiplist.py +226 -0
  14. kedis_python-0.1.1/kedis_python/store.py +873 -0
  15. kedis_python-0.1.1/kedis_python/ui.py +189 -0
  16. {kedis_python-0.1.0 → kedis_python-0.1.1}/kedis_python.egg-info/PKG-INFO +4 -2
  17. kedis_python-0.1.1/kedis_python.egg-info/SOURCES.txt +28 -0
  18. kedis_python-0.1.1/kedis_python.egg-info/entry_points.txt +2 -0
  19. kedis_python-0.1.1/kedis_python.egg-info/top_level.txt +1 -0
  20. kedis_python-0.1.1/pyproject.toml +29 -0
  21. kedis_python-0.1.0/kedis_python.egg-info/SOURCES.txt +0 -13
  22. kedis_python-0.1.0/kedis_python.egg-info/top_level.txt +0 -1
  23. kedis_python-0.1.0/pyproject.toml +0 -16
  24. {kedis_python-0.1.0 → kedis_python-0.1.1}/LICENSE +0 -0
  25. {kedis_python-0.1.0 → kedis_python-0.1.1}/README.md +0 -0
  26. {kedis_python-0.1.0 → kedis_python-0.1.1}/kedis_python.egg-info/dependency_links.txt +0 -0
  27. {kedis_python-0.1.0 → kedis_python-0.1.1}/setup.cfg +0 -0
  28. {kedis_python-0.1.0 → kedis_python-0.1.1}/tests/test_bench.py +0 -0
  29. {kedis_python-0.1.0 → kedis_python-0.1.1}/tests/test_kesp.py +0 -0
  30. {kedis_python-0.1.0 → kedis_python-0.1.1}/tests/test_kesp2.py +0 -0
  31. {kedis_python-0.1.0 → kedis_python-0.1.1}/tests/test_lists.py +0 -0
  32. {kedis_python-0.1.0 → kedis_python-0.1.1}/tests/test_lru.py +0 -0
  33. {kedis_python-0.1.0 → kedis_python-0.1.1}/tests/test_replica.py +0 -0
@@ -1,8 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kedis-python
3
- Version: 0.1.0
4
- Summary: A Redis-inspired in-memory store in Python
3
+ Version: 0.1.1
4
+ Summary: A Redis-inspired in-memory store in Python.
5
+ Author: V SS Karthik
5
6
  License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Hogwarts-coder10/kedis-python
6
8
  Requires-Python: >=3.9
7
9
  Description-Content-Type: text/markdown
8
10
  License-File: LICENSE
File without changes
@@ -0,0 +1,65 @@
1
+ import socket
2
+ import threading
3
+ import time
4
+
5
+ from rich.console import Console
6
+
7
+ console = Console()
8
+
9
+ HOST = "127.0.0.1"
10
+ PORT = 6379
11
+ THREADS = 10
12
+ REQUESTS_PER_THREAD = 5000
13
+
14
+
15
+ def hammer_server(thread_id):
16
+ """Simulates a ruthless client hammering the database."""
17
+ try:
18
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
19
+ s.connect((HOST, PORT))
20
+
21
+ for i in range(REQUESTS_PER_THREAD):
22
+ # Write phase
23
+ cmd_set = f"SET stress_key_{thread_id}_{i} payload_data_{i}"
24
+ s.sendall(cmd_set.encode("utf-8"))
25
+ s.recv(1024) # Wait for +OK
26
+
27
+ # Read phase
28
+ cmd_get = f"GET stress_key_{thread_id}_{i}"
29
+ s.sendall(cmd_get.encode("utf-8"))
30
+ s.recv(1024)
31
+
32
+ s.close()
33
+ except Exception as e:
34
+ console.print(f"[red]Thread {thread_id} crashed: {e}[/red]")
35
+
36
+
37
+ def run_stress_test():
38
+ console.print(f"[bold cyan]🏎️ KEDIS WIND TUNNEL ONLINE[/bold cyan]")
39
+ console.print(f"Target: {HOST}:{PORT}")
40
+ console.print(f"Threads: {THREADS}")
41
+ console.print(f"Total Operations: {THREADS * REQUESTS_PER_THREAD * 2:,}\n")
42
+
43
+ threads = []
44
+ start_time = time.time()
45
+
46
+ # Unleash the swarm
47
+ for i in range(THREADS):
48
+ t = threading.Thread(target=hammer_server, args=(i,))
49
+ threads.append(t)
50
+ t.start()
51
+
52
+ for t in threads:
53
+ t.join()
54
+
55
+ duration = time.time() - start_time
56
+ total_ops = THREADS * REQUESTS_PER_THREAD * 2
57
+ rps = total_ops / duration
58
+
59
+ console.print("[bold green]🏁 STRESS TEST COMPLETE[/bold green]")
60
+ console.print(f"Time Elapsed: [yellow]{duration:.2f} seconds[/yellow]")
61
+ console.print(f"Throughput: [bold blue]{rps:,.0f} Requests/Second[/bold blue]")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ run_stress_test()
@@ -0,0 +1,33 @@
1
+ import asyncio
2
+ import time
3
+
4
+ from parser import KESPEncoder # Reusing your KESP Exhaust!
5
+
6
+
7
+ async def fire_commands():
8
+ reader, writer = await asyncio.open_connection("127.0.0.1", 6379)
9
+
10
+ # 1. Test the Surge Tank with a massive 50KB payload
11
+ massive_string = "V" * 50000
12
+ payload = KESPEncoder.encode(["SET", "load_test", massive_string])
13
+ writer.write(payload)
14
+ await writer.drain()
15
+ await reader.read(1024)
16
+ print("✓ 50KB Surge Tank payload swallowed successfully.")
17
+
18
+ # 2. Fire 10,000 rapid commands
19
+ start = time.time()
20
+ for i in range(10000):
21
+ cmd = KESPEncoder.encode(["SET", f"key_{i}", str(i)])
22
+ writer.write(cmd)
23
+ await writer.drain()
24
+ await reader.read(1024)
25
+
26
+ end = time.time()
27
+ print(f"✓ 10,000 KESP commands executed in {end - start:.2f} seconds.")
28
+
29
+ writer.close()
30
+ await writer.wait_closed()
31
+
32
+
33
+ asyncio.run(fire_commands())
@@ -0,0 +1,512 @@
1
+ import threading
2
+ import time
3
+ from typing import Any
4
+
5
+ from .store import KedisStore
6
+
7
+
8
+ class CommandHandler:
9
+ def __init__(self, store: KedisStore):
10
+ self.store = store
11
+
12
+ # Global Engine lock
13
+ self._engine_lock = threading.Lock()
14
+
15
+ # Pub / Sub SwitchBoard
16
+ self._channels = {}
17
+
18
+ # The O(1) Dispatch Table
19
+ self._commands = {
20
+ "SET": self._handle_set,
21
+ "GET": self._handle_get,
22
+ "DEL": self._handle_del,
23
+ "EXISTS": self._handle_exists,
24
+ "EXPIRE": self._handle_expire,
25
+ "KEYS": self._handle_keys,
26
+ "TTL": self._handle_ttl,
27
+ "FLUSHALL": self._handle_flushall,
28
+ "SAVE": self._handle_save,
29
+ "COMPACT": self._handle_compact,
30
+ "LPUSH": self._handle_lpush,
31
+ "LRANGE": self._handle_lrange,
32
+ "RPUSH": self._handle_rpush,
33
+ "LPOP": self._handle_lpop,
34
+ "RPOP": self._handle_rpop,
35
+ "SADD": self._handle_sadd,
36
+ "SMEMBERS": self._handle_smembers,
37
+ "SREM": self._handle_srem,
38
+ "HSET": self._handle_hset,
39
+ "HGET": self._handle_hget,
40
+ "HGETALL": self._handle_hgetall,
41
+ "ZADD": self._handle_zadd,
42
+ "ZRANGE": self._handle_zrange,
43
+ "TYPE": self._handle_type,
44
+ "STATS": self._handle_stats,
45
+ "CONFIG": self._handle_config,
46
+ "SUBSCRIBE": self._handle_subscribe,
47
+ "PUBLISH": self._handle_publish,
48
+ "SLOWLOG": self.cmd_slowlog,
49
+ "LATENCY": self.cmd_latency,
50
+ }
51
+
52
+ @property
53
+ def WRITE_COMMANDS(self) -> set[str]:
54
+ """
55
+ The definitive list of all engine commands that mutate database state.
56
+ Used by the replication slipstream to broadcast changes and enforce read-only firewalls.
57
+ """
58
+ return {
59
+ "SET",
60
+ "DEL",
61
+ "EXPIREAT",
62
+ "FLUSHALL",
63
+ "LPUSH",
64
+ "RPUSH",
65
+ "LPOP",
66
+ "RPOP",
67
+ "SADD",
68
+ "SREM",
69
+ "HSET",
70
+ "ZADD",
71
+ }
72
+
73
+ def execute(self, tokens: list[str], client_socket=None):
74
+ """
75
+ Routes parsed tokens to the correct storage operations in O(1) time.
76
+ Returns raw Python types (int, list, str, None) for the KESP Encoder.
77
+ """
78
+ if not tokens:
79
+ return "-ERR empty command or syntax error"
80
+
81
+ cmd = tokens[0].upper()
82
+ args = tokens[
83
+ 1:
84
+ ] # (Though your current code passes tokens, let's keep your routing exact)
85
+
86
+ # 🚀 TRACK A: Start the high-precision telemetry stopwatch (in nanoseconds)
87
+ start_time = time.perf_counter_ns()
88
+ result = None
89
+
90
+ try:
91
+ with self._engine_lock:
92
+ if cmd in self._commands:
93
+ if cmd in ["SUBSCRIBE", "PUBLISH"]:
94
+ handler_function = self._commands[cmd]
95
+ result = handler_function(tokens, client_socket)
96
+ else:
97
+ handler_function = self._commands[cmd]
98
+ # Note: Depending on how your handlers are registered,
99
+ # they might expect 'tokens' or 'args'. Keeping your existing call:
100
+ result = handler_function(tokens)
101
+ return result
102
+ else:
103
+ result = f"-ERR unknown command '{cmd}'"
104
+ return result
105
+
106
+ except TypeError as e:
107
+ result = f"-ERR wrong number of arguments for '{cmd}' command"
108
+ return result
109
+ except Exception as e:
110
+ result = f"-ERR {str(e)}"
111
+ return result
112
+
113
+ finally:
114
+ # 🚀 TRACK A: Stop the stopwatch, convert nanoseconds to microseconds
115
+ end_time = time.perf_counter_ns()
116
+ duration_us = (end_time - start_time) // 1000
117
+
118
+ # Feed the metrics to the store's slowlog ring buffer if available
119
+ if (
120
+ hasattr(self, "store")
121
+ and self.store
122
+ and hasattr(self.store, "_log_slow_command")
123
+ ):
124
+ self.store._log_slow_command(tokens, duration_us)
125
+
126
+ # ---------------------------------------------------------
127
+ # COMMAND HANDLERS (The isolated engine components)
128
+ # ---------------------------------------------------------
129
+
130
+ def _handle_config(self, tokens: list[str]):
131
+ if len(tokens) < 3:
132
+ return "-ERR wrong number of arguments for 'CONFIG' command"
133
+
134
+ sub_cmd = tokens[1].upper()
135
+ param = tokens[2].lower()
136
+
137
+ if param != "appendfsync":
138
+ return f"-ERR unsupported CONFIG parameter '{param}'"
139
+
140
+ if sub_cmd == "SET":
141
+ if len(tokens) != 4:
142
+ return "-ERR wrong number of arguments for 'CONFIG SET'"
143
+ new_mode = tokens[3].lower()
144
+ return self.store.set_appendfsync(new_mode)
145
+
146
+ elif sub_cmd == "GET":
147
+ if len(tokens) != 3:
148
+ return "-ERR wrong number of arguments for 'CONFIG GET'"
149
+ return [param, self.store.appendfsync]
150
+
151
+ else:
152
+ return f"-ERR unknown CONFIG subcommand '{sub_cmd}'"
153
+
154
+ def _handle_set(self, tokens: list[str]):
155
+ if len(tokens) != 3:
156
+ return "-ERR wrong number of arguments for 'SET' command"
157
+ self.store.set(tokens[1], tokens[2])
158
+ return "OK"
159
+
160
+ def _handle_get(self, tokens: list[str]):
161
+ if len(tokens) != 2:
162
+ return "-ERR wrong number of arguments for 'GET' command"
163
+ return self.store.get(tokens[1])
164
+
165
+ def _handle_del(self, tokens: list[str]):
166
+ if len(tokens) != 2:
167
+ return "-ERR wrong number of arguments for 'DEL' command"
168
+ return self.store.delete(tokens[1])
169
+
170
+ def _handle_exists(self, tokens: list[str]):
171
+ if len(tokens) != 2:
172
+ return "-ERR wrong number of arguments for 'EXISTS' command"
173
+ return self.store.exists(tokens[1])
174
+
175
+ def _handle_expire(self, tokens: list[str]):
176
+ if len(tokens) != 3:
177
+ return "-ERR wrong number of arguments for 'EXPIRE' command"
178
+ try:
179
+ seconds = int(tokens[2])
180
+ except ValueError:
181
+ return "-ERR value is not an integer or out of range"
182
+ return self.store.set_expire(tokens[1], seconds)
183
+
184
+ def _handle_keys(self, tokens: list[str]):
185
+ if len(tokens) != 1:
186
+ return "-ERR wrong number of arguments for 'KEYS' command"
187
+ all_keys = self.store.keys()
188
+ if not all_keys:
189
+ return []
190
+
191
+ return [
192
+ f"{k} | {data['type']} | {data['ttl']} | {data['length']}"
193
+ for k, data in all_keys.items()
194
+ ]
195
+
196
+ def _handle_ttl(self, tokens: list[str]):
197
+ if len(tokens) != 2:
198
+ return "-ERR wrong number of arguments for 'TTL' command"
199
+ return self.store.ttl(tokens[1])
200
+
201
+ def _handle_flushall(self, tokens: list[str]):
202
+ if len(tokens) != 1:
203
+ return "-ERR wrong number of arguments for 'FLUSHALL' command"
204
+ self.store.flushall()
205
+ return "OK"
206
+
207
+ def _handle_save(self, tokens: list[str]):
208
+ if len(tokens) != 1:
209
+ return "-ERR wrong number of arguments for 'SAVE' command"
210
+ success = self.store.save()
211
+ return "OK" if success else "-ERR failed to save data"
212
+
213
+ def _handle_compact(self, tokens: list[str]):
214
+ if len(tokens) > 1:
215
+ return "-ERR wrong number of arguments for 'compact' command"
216
+ success = self.store.compact_aof()
217
+ return (
218
+ "+OK AOF log compacted successfully"
219
+ if success
220
+ else "-ERR Failed to compact AOF log"
221
+ )
222
+
223
+ def _handle_lpush(self, tokens: list[str]):
224
+ if len(tokens) < 3:
225
+ return "-ERR wrong number of arguments for 'lpush' command"
226
+ try:
227
+ return self.store.lpush(tokens[1], *tokens[2:])
228
+ except TypeError as e:
229
+ return f"-ERR {str(e)}"
230
+
231
+ def _handle_lrange(self, tokens: list[str]):
232
+ if len(tokens) != 4:
233
+ return "-ERR wrong number of arguments for 'lrange' command"
234
+ try:
235
+ result = self.store.lrange(tokens[1], int(tokens[2]), int(tokens[3]))
236
+ return result if result else []
237
+ except (TypeError, ValueError) as e:
238
+ return f"-ERR {str(e)}"
239
+
240
+ def _handle_rpush(self, tokens: list[str]):
241
+ if len(tokens) < 3:
242
+ return "-ERR wrong number of arguments for 'rpush' command"
243
+ try:
244
+ return self.store.rpush(tokens[1], *tokens[2:])
245
+ except TypeError as e:
246
+ return f"-ERR {str(e)}"
247
+
248
+ def _handle_lpop(self, tokens: list[str]):
249
+ if len(tokens) != 2:
250
+ return "-ERR wrong number of arguments for 'lpop' command"
251
+ try:
252
+ return self.store.lpop(tokens[1])
253
+ except TypeError as e:
254
+ return f"-ERR {str(e)}"
255
+
256
+ def _handle_rpop(self, tokens: list[str]):
257
+ if len(tokens) != 2:
258
+ return "-ERR wrong number of arguments for 'rpop' command"
259
+ try:
260
+ return self.store.rpop(tokens[1])
261
+ except TypeError as e:
262
+ return f"-ERR {str(e)}"
263
+
264
+ def _handle_sadd(self, tokens: list[str]):
265
+ if len(tokens) < 3:
266
+ return "-ERR wrong number of arguments for 'sadd' command"
267
+ try:
268
+ return self.store.sadd(tokens[1], *tokens[2:])
269
+ except TypeError as e:
270
+ return f"-ERR {str(e)}"
271
+
272
+ def _handle_smembers(self, tokens: list[str]):
273
+ if len(tokens) != 2:
274
+ return "-ERR wrong number of arguments for 'smembers' command"
275
+ try:
276
+ members = self.store.smembers(tokens[1])
277
+ return members if members else []
278
+ except TypeError as e:
279
+ return f"-ERR {str(e)}"
280
+
281
+ def _handle_srem(self, tokens: list[str]):
282
+ if len(tokens) < 3:
283
+ return "-ERR wrong number of arguments for 'srem' command"
284
+ try:
285
+ return self.store.srem(tokens[1], *tokens[2:])
286
+ except TypeError as e:
287
+ return f"-ERR {str(e)}"
288
+
289
+ def _handle_hset(self, tokens: list[str]):
290
+ if len(tokens) < 4:
291
+ return "-ERR wrong number of arguments for 'hset' command"
292
+ try:
293
+ val = " ".join(tokens[3:])
294
+ return self.store.hset(tokens[1], tokens[2], val)
295
+ except TypeError as e:
296
+ return f"-ERR {str(e)}"
297
+
298
+ def _handle_hget(self, tokens: list[str]):
299
+ if len(tokens) != 3:
300
+ return "-ERR wrong number of arguments for 'hget' command"
301
+ try:
302
+ return self.store.hget(tokens[1], tokens[2])
303
+ except TypeError as e:
304
+ return f"-ERR {str(e)}"
305
+
306
+ def _handle_hgetall(self, tokens: list[str]):
307
+ if len(tokens) != 2:
308
+ return "-ERR wrong number of arguments for 'hgetall' command"
309
+ try:
310
+ data = self.store.hgetall(tokens[1])
311
+ if not data:
312
+ return []
313
+
314
+ # Flattens dict into [key1, val1, key2, val2]
315
+ flat_list = []
316
+ for k, v in data.items():
317
+ flat_list.extend([k, v])
318
+ return flat_list
319
+ except TypeError as e:
320
+ return f"-ERR {str(e)}"
321
+
322
+ def _handle_zadd(self, tokens: list[str]):
323
+ if len(tokens) < 4 or len(tokens) % 2 != 0:
324
+ return "-ERR wrong number of arguments for 'zadd' command"
325
+ key = tokens[1]
326
+ added = 0
327
+ try:
328
+ for i in range(2, len(tokens), 2):
329
+ score = float(tokens[i])
330
+ member = tokens[i + 1]
331
+ added += self.store.zadd(key, score, member)
332
+ return added
333
+ except ValueError:
334
+ return "-ERR value is not a valid float"
335
+ except TypeError as e:
336
+ return f"-ERR {str(e)}"
337
+
338
+ def _handle_zrange(self, tokens: list[str]):
339
+ if len(tokens) < 4 or len(tokens) > 5:
340
+ return "-ERR wrong number of arguments for 'zrange' command"
341
+
342
+ key = tokens[1]
343
+ withscores = False
344
+
345
+ if len(tokens) == 5:
346
+ if tokens[4].upper() == "WITHSCORES":
347
+ withscores = True
348
+ else:
349
+ return "-ERR syntax error"
350
+
351
+ try:
352
+ start = int(tokens[2])
353
+ stop = int(tokens[3])
354
+ result = self.store.zrange(key, start, stop, withscores)
355
+ return result if result else []
356
+ except ValueError:
357
+ return "-ERR value is not an integer or out of range"
358
+ except TypeError as e:
359
+ return f"-ERR {str(e)}"
360
+
361
+ def _handle_type(self, tokens: list[str]):
362
+ if len(tokens) != 2:
363
+ return "-ERR wrong number of arguments for 'type' command"
364
+ return self.store.type_of(tokens[1])
365
+
366
+ def _handle_stats(self, tokens: list[str]):
367
+ stats = self.store.get_engine_stats()
368
+ lru = stats.get("lru_cache", {})
369
+
370
+ return (
371
+ f"Total Keys:{stats.get('total_keys', 0)}\n"
372
+ f"String Chars:{stats.get('string_chars', 0)}\n"
373
+ f"List Items:{stats.get('list_items', 0)}\n"
374
+ f"Set Members:{stats.get('set_members', 0)}\n"
375
+ f"Hash Fields:{stats.get('hash_fields', 0)}\n"
376
+ f"ZSet Nodes:{stats.get('zset_nodes', 0)}\n"
377
+ "---\n"
378
+ f"LRU Hits:{lru.get('hits', 0)}\n"
379
+ f"LRU Misses:{lru.get('misses', 0)}\n"
380
+ f"Hit Rate:{lru.get('hit_rate_pct', 0.0)}%\n"
381
+ f"LRU Tracked:{lru.get('tracked_keys', 0)} / {lru.get('max_size', 128)}"
382
+ )
383
+
384
+ def _handle_subscribe(self, tokens: list[str], client_socket):
385
+ if client_socket is None:
386
+ return "-ERR network socket not found"
387
+ if len(tokens) < 2:
388
+ return "-ERR wrong number of arguments for 'subscribe' command"
389
+
390
+ channel = tokens[1]
391
+
392
+ if channel not in self._channels:
393
+ self._channels[channel] = []
394
+
395
+ if client_socket not in self._channels[channel]:
396
+ self._channels[channel].append(client_socket)
397
+
398
+ return ["subscribe", channel, 1]
399
+
400
+ def _handle_publish(self, tokens: list[str], client_socket):
401
+ if len(tokens) < 3:
402
+ return "-ERR wrong number of arguments for 'publish' command"
403
+
404
+ channel = tokens[1]
405
+ message = " ".join(tokens[2:])
406
+
407
+ if channel not in self._channels:
408
+ return 0
409
+
410
+ subscribers = self._channels[channel]
411
+ receivers = 0
412
+
413
+ # Construct raw KESP bytes for the broadcast push
414
+ kesp_payload = (
415
+ f"A3\n"
416
+ f"S7\nmessage\n"
417
+ f"S{len(channel.encode('utf-8'))}\n{channel}\n"
418
+ f"S{len(message.encode('utf-8'))}\n{message}\n"
419
+ ).encode("utf-8")
420
+
421
+ dead_sockets = []
422
+ for sock in subscribers:
423
+ try:
424
+ sock.sendall(kesp_payload)
425
+ receivers += 1
426
+ except Exception:
427
+ dead_sockets.append(sock)
428
+
429
+ for dead in dead_sockets:
430
+ subscribers.remove(dead)
431
+
432
+ return
433
+
434
+ def cmd_slowlog(self, tokens: list[str]) -> Any:
435
+ if len(tokens) < 2:
436
+ return "-ERR wrong number of arguments for 'slowlog' command"
437
+
438
+ subcmd = tokens[1].upper()
439
+
440
+ if subcmd == "GET":
441
+ count = None
442
+ if len(tokens) > 2:
443
+ try:
444
+ count = int(tokens[2])
445
+ except ValueError:
446
+ return "-ERR value is not an integer or out of range"
447
+
448
+ # Fetch raw logs
449
+ raw_logs = self.store.slowlog_get(count)
450
+ if not raw_logs:
451
+ return []
452
+
453
+ # Formatting raw logs for client UI
454
+ formatted_logs = []
455
+
456
+ for entry in raw_logs:
457
+ log_id = entry[0]
458
+ timestamp = entry[1]
459
+ duration_us = entry[2]
460
+ cmd_string = " ".join(entry[3])
461
+
462
+ formatted_logs.append(
463
+ f"ID: {log_id} | Time: {timestamp} | {duration_us}µs | Cmd: {cmd_string}"
464
+ )
465
+
466
+ return formatted_logs
467
+
468
+ elif subcmd == "LEN":
469
+ return self.store.slowlog_len()
470
+
471
+ elif subcmd == "RESET":
472
+ self.store.slowlog_reset()
473
+ return "+OK"
474
+
475
+ else:
476
+ return f"-ERR Unknown subcommand '{subcmd}'. Try SLOWLOG <GET|LEN|RESET>"
477
+
478
+ def cmd_latency(self, tokens: list[str]) -> Any:
479
+ if len(tokens) < 2:
480
+ return "-ERR wrong number of arguments for 'latency' command"
481
+
482
+ subcmd = tokens[1].upper()
483
+ # Safely fetch the lag, defaulting to 0.0 if in Local Mode without a heartbeat
484
+ lag_ms = getattr(self.store, "current_lag_ms", 0.0)
485
+
486
+ if subcmd == "LAG":
487
+ return f"{lag_ms}ms"
488
+
489
+ elif subcmd == "DOCTOR":
490
+ # The Engine Health Diagnostic Report
491
+ drivetrain = self.store.appendfsync.upper()
492
+ keys = len(getattr(self.store, "_data", {}))
493
+
494
+ # Determine health status based on lag severity
495
+ if lag_ms < 5.0:
496
+ health = "[green]EXCELLENT[/green]"
497
+ elif lag_ms < 20.0:
498
+ health = "[yellow]WARNING (Mild Loop Delay)[/yellow]"
499
+ else:
500
+ health = "[red]CRITICAL (Heavy Blocking)[/red]"
501
+
502
+ report = (
503
+ f"--- KEDIS ENGINE HEALTH ---\n"
504
+ f"Event Loop Lag : {lag_ms}ms\n"
505
+ f"Status : {health}\n"
506
+ f"I/O Drivetrain : {drivetrain}\n"
507
+ f"Active Keys : {keys}\n"
508
+ )
509
+ return report
510
+
511
+ else:
512
+ return f"-ERR Unknown subcommand '{subcmd}'. Try LATENCY <LAG|DOCTOR>"