Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
491 changes: 491 additions & 0 deletions basic_impl/plan.md

Large diffs are not rendered by default.

433 changes: 433 additions & 0 deletions basic_impl/prompt.md

Large diffs are not rendered by default.

File renamed without changes.
13 changes: 13 additions & 0 deletions basic_impl/run_with_session_snapshots.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# docker run --runtime=runsc --rm -it \
# -v "$(pwd)/forevervm_minimal:/app" \
# -v "$(pwd)/snapshots:/var/forevervm/snapshots" \
# -p 8000:8000 \
# python:3.9.21-alpine3.21 \
# sh -c "cd /app && python main.py"

docker run --runtime=runsc --rm -it \
-v "$(pwd)/forevervm_minimal:/app/forevervm_minimal" \
-v "$(pwd)/snapshots:/var/forevervm/snapshots" \
-p 8000:8000 \
python:3.9.21-alpine3.21 \
sh -c "cd /app/forevervm_minimal && pip install flask && python main.py"
85 changes: 85 additions & 0 deletions basic_impl/scratchpad.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Scratchpad

## Current Task
Debugging and fixing the TCP client test script for the Python REPL server, creating a concurrent clients test, and enhancing the output with Rich library.

## Progress
[X] Identified the issue: The client was not properly receiving the initial greeting using the length-prefixed protocol
[X] Fixed the client code by adding a `receive_response` function that follows the same protocol as the server
[X] Successfully tested the client against the server
[X] Created a new test script to simulate concurrent clients with different sessions
[X] Improved the concurrent clients test to clearly demonstrate session isolation
[X] Enhanced the test script with Rich library for better console output
[X] Added a common variable name test to verify that both clients can set independent values to the same variable name

## Lessons
- The server uses a length-prefixed protocol for all communication (both sending and receiving)
- Each message is prefixed with a 4-byte integer (big-endian) indicating the length of the following JSON message
- All messages are JSON-encoded and UTF-8 encoded
- The initial greeting from the server follows the same protocol as all other messages
- When working with custom TCP protocols, it's important to ensure both client and server follow the same message format for all communications
- The server maintains isolated session environments for different clients, ensuring that variables defined in one session are not accessible from another
- When testing session isolation, it's important to use distinct variable names to clearly demonstrate that variables from one session cannot be accessed from another
- The Rich library provides excellent tools for creating visually appealing and more readable console output in Python applications
- Variables with the same name can have different values in different sessions, demonstrating complete session isolation

## Technical Details
- The server implements a Python REPL that maintains separate session environments
- Each session is identified by a UUID
- The server accepts JSON requests with the following fields:
- `code`: Python code to execute
- `session_id` (optional): ID of an existing session
- The server responds with JSON containing:
- `status`: "ok" or "error"
- `output` or `error`: The output of the code execution or error message
- `session_id`: The ID of the session used

## Concurrent Clients Test
I created a new test script `test_concurrent_clients.py` that simulates two clients connecting to the server concurrently, each with their own session. The test does the following:

1. Creates two client threads (Client A and Client B) that connect to the server simultaneously
2. Each client:
- Creates a new session
- Defines a function in its session
- Creates a unique variable with a client-specific name (e.g., `unique_value_A` for Client A)
- Creates a variable with the same name (`common_variable`) but different values in each session
- Verifies the variables are still correct after a delay
3. After both client threads complete, a third connection:
- Attempts to access Client A's variable from Client B's session (should fail)
- Verifies that `common_variable` in Client B's session has Client B's value
- Verifies that `common_variable` in Client A's session has Client A's value
- Attempts to access Client B's variable from Client B's session (should succeed)
4. The test verifies that session isolation is maintained by confirming that:
- Client B cannot access Client A's variables (NameError is raised)
- Client B can access its own variables
- The `common_variable` in each session has a different value, specific to that session

This test helps verify:
- The server can handle multiple concurrent clients
- Each client maintains its own isolated session state
- Variables defined in one session are not accessible from another session
- Variables with the same name can have different values in different sessions
- The server correctly manages session IDs and their associated environments

## Rich Library Enhancements
I enhanced the test script with the Rich library to provide a more visually appealing and readable console output. The enhancements include:

1. Progress tracking with spinners for each client thread
2. Colorful panels to separate different sections of the test
3. Tables to display test results in a structured format
4. Color-coded status messages and results
5. Clear visual indication of test success/failure

These enhancements make it easier to:
- Monitor the progress of concurrent client threads
- Understand the relationships between sessions and variables
- Quickly identify whether session isolation is properly maintained
- View the test results in a well-organized format

To run the test:
```bash
python test_concurrent_clients.py
```


https://claude.ai/chat/ad32c8cf-d589-4844-a85f-1084139269ea
File renamed without changes.
File renamed without changes.
File renamed without changes.
177 changes: 177 additions & 0 deletions basic_impl/test_timeouts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
import socket
import json
import sys
import threading
import time
import uuid
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich import box
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn

console = Console()

def send_receive(sock, request_dict):
"""Send a request to the server and receive the response (length-prefixed JSON)."""
request_json = json.dumps(request_dict)
request_bytes = request_json.encode('utf-8')

# Send the length first (4 bytes, big-endian)
length = len(request_bytes)
sock.sendall(length.to_bytes(4, 'big'))
# Then send the actual data
sock.sendall(request_bytes)

# Read response length
length_bytes = sock.recv(4)
if not length_bytes:
return {}
response_length = int.from_bytes(length_bytes, 'big')

# Read response data
chunks = []
bytes_received = 0
while bytes_received < response_length:
chunk = sock.recv(min(4096, response_length - bytes_received))
if not chunk:
break
chunks.append(chunk)
bytes_received += len(chunk)

response_str = b''.join(chunks).decode('utf-8')
return json.loads(response_str)

def receive_greeting(sock):
"""Receive a single greeting message from the server (length-prefixed JSON)."""
length_bytes = sock.recv(4)
if not length_bytes:
return None
message_length = int.from_bytes(length_bytes, 'big')
chunks = []
bytes_received = 0
while bytes_received < message_length:
chunk = sock.recv(min(4096, message_length - bytes_received))
if not chunk:
break
chunks.append(chunk)
bytes_received += len(chunk)
data = b''.join(chunks).decode('utf-8')
return json.loads(data)

def client_thread(name, results, progress):
"""
Each client:
1. Connects to the server, receives greeting.
2. Creates a session by sending code with no session_id (server should create new).
3. Defines a variable, prints it.
4. Sleeps long enough to trigger session timeout (6s).
5. Executes code again to see if the session was snapshotted+restored.
"""
host = "localhost"
port = 8000

task_id = progress.add_task(f"[green]{name} Starting", total=5)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
session_id = None
try:
sock.connect((host, port))
progress.update(task_id, description=f"[green]{name}: Connected", advance=1)

# 1) Receive initial greeting
greeting = receive_greeting(sock)
results[name]["greeting"] = greeting

# 2) Create a new session by sending some code (omitting session_id => server should create it)
progress.update(task_id, description=f"[green]{name}: Creating session", advance=1)
resp_create = send_receive(sock, {
"code": f"client_name = '{name}'\nprint('Hello from {name}!')"
})
# The server should respond with something like: {"session_id": "...", "output": "..."}
session_id = resp_create.get("session_id")
results[name]["session_id"] = session_id
results[name]["outputs"].append(resp_create.get("output", ""))

# 3) Define a variable and print it
progress.update(task_id, description=f"[green]{name}: Defining variable", advance=1)
myvar = f"value_{uuid.uuid4().hex[:6]}"
code_str = f"""myvar_{name} = '{myvar}'\nprint('Set myvar_{name} = ' + myvar_{name})"""
resp_define = send_receive(sock, {
"session_id": session_id,
"code": code_str
})
results[name]["outputs"].append(resp_define.get("output", ""))

# 4) Sleep 7s to exceed the 6s inactivity timeout
progress.update(task_id, description=f"[yellow]{name}: Sleeping 7s (timeout test)", advance=1)
time.sleep(7)

# 5) Execute new code => triggers restore if session was snapshotted
progress.update(task_id, description=f"[green]{name}: Checking session restore", advance=1)
resp_after_sleep = send_receive(sock, {
"session_id": session_id,
"code": f"print('After timeout, myvar_{name} is ' + myvar_{name})"
})
results[name]["outputs"].append(resp_after_sleep.get("output", ""))

progress.update(task_id, description=f"[green]{name}: Finished", completed=True)

except Exception as e:
results[name]["error"] = str(e)
finally:
sock.close()

def main():
console.clear()
console.print(Panel.fit("[bold cyan]Timeout & Concurrency Test (6s Inactivity)[/bold cyan]", border_style="cyan"))

# We'll track results from multiple clients
# For demonstration, let's spin up two concurrent clients
results = {
"ClientA": {"session_id": None, "greeting": None, "outputs": [], "error": None},
"ClientB": {"session_id": None, "greeting": None, "outputs": [], "error": None}
}

with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
TimeElapsedColumn(),
console=console
) as progress:
console.print("[bold]Starting two client threads...[/bold]\n")

tA = threading.Thread(target=client_thread, args=("ClientA", results, progress))
tB = threading.Thread(target=client_thread, args=("ClientB", results, progress))

tA.start()
tB.start()

tA.join()
tB.join()

console.print("\n[bold green]All client threads completed![/bold green]\n")

# Present a brief results summary table
table = Table(title="Results Summary", box=box.ROUNDED, show_header=True, header_style="bold magenta")
table.add_column("Client")
table.add_column("Session ID")
table.add_column("Error")
table.add_column("Outputs")

for client_name, data in results.items():
session_id = data["session_id"] or "[None]"
error = data["error"] or "[None]"
outputs_joined = "\n---\n".join(data["outputs"]) if data["outputs"] else "[No outputs]"
table.add_row(client_name, session_id, error, outputs_joined)

console.print(table)

console.print(Panel.fit(
"[bold green]✓ Test completed. Check the logs above or your server logs to confirm sessions got snapshotted/restored after 6s of inactivity.[/bold green]",
border_style="green"
))

if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions forevervm_minimal/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
__pycache__/

.venv/
data_dir/
1 change: 1 addition & 0 deletions forevervm_minimal/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
Loading