Skip to content
Merged
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
16 changes: 8 additions & 8 deletions pod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ spec:
- name: CLICKHOUSE_USER
value: clickhouse
- name: CLICKHOUSE_PASSWORD
value: CLICKHOUSE_PASSWORD_PLACEHOLDER
value: clickhouse
image: docker.io/clickhouse/clickhouse-server:26.5.1
livenessProbe:
exec:
Expand All @@ -196,7 +196,7 @@ spec:
- --user
- clickhouse
- --password
- CLICKHOUSE_PASSWORD_PLACEHOLDER
- clickhouse
- --query
- SELECT 1
initialDelaySeconds: 20
Expand All @@ -222,7 +222,7 @@ spec:
- --port
- '6380'
- --requirepass
- REDIS_AUTH_PLACEHOLDER
- langfuse-redis-2026
- --maxmemory-policy
- noeviction
- --loglevel
Expand All @@ -242,7 +242,7 @@ spec:
- -p
- "6380"
- -a
- REDIS_AUTH_PLACEHOLDER
- langfuse-redis-2026
- ping
initialDelaySeconds: 2
periodSeconds: 5
Expand Down Expand Up @@ -287,15 +287,15 @@ spec:
- name: CLICKHOUSE_USER
value: clickhouse
- name: CLICKHOUSE_PASSWORD
value: CLICKHOUSE_PASSWORD_PLACEHOLDER
value: clickhouse
- name: CLICKHOUSE_CLUSTER_ENABLED
value: 'false'
- name: REDIS_HOST
value: 127.0.0.1
- name: REDIS_PORT
value: '6380'
- name: REDIS_AUTH
value: REDIS_AUTH_PLACEHOLDER
value: langfuse-redis-2026
- name: LANGFUSE_INIT_ORG_ID
value: org-local-dev-id
- name: LANGFUSE_INIT_ORG_NAME
Expand Down Expand Up @@ -343,15 +343,15 @@ spec:
- name: CLICKHOUSE_USER
value: clickhouse
- name: CLICKHOUSE_PASSWORD
value: CLICKHOUSE_PASSWORD_PLACEHOLDER
value: clickhouse
- name: CLICKHOUSE_CLUSTER_ENABLED
value: 'false'
- name: REDIS_HOST
value: 127.0.0.1
- name: REDIS_PORT
value: '6380'
- name: REDIS_AUTH
value: REDIS_AUTH_PLACEHOLDER
value: langfuse-redis-2026
- name: HOSTNAME
value: 0.0.0.0
- name: PORT
Expand Down
13 changes: 9 additions & 4 deletions router/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,15 @@ def get_redis():
return None
_redis_last_init_attempt = now
try:
host = os.getenv("VALKEY_HOST", "127.0.0.1")
port = int(os.getenv("VALKEY_PORT", "6379"))
_redis_client = aioredis.Redis(host=host, port=port, decode_responses=True, socket_timeout=1.0)
logger.info(f"Valkey client initialized at {host}:{port}")
url = os.getenv("VALKEY_URL")
if url:
_redis_client = aioredis.Redis.from_url(url, decode_responses=True, socket_timeout=1.0)
logger.info(f"Valkey client initialized from URL")
Comment on lines +39 to +42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The redis-py library (imported here as aioredis) does not natively support valkey:// or valkeys:// URL schemes. If a user configures VALKEY_URL using one of these schemes (which is highly likely given the variable name), aioredis.Redis.from_url will raise a ValueError: Unknown URL scheme.

To ensure compatibility, we should sanitize the URL by converting valkey:// and valkeys:// to redis:// and rediss:// respectively before passing it to from_url.

Suggested change
url = os.getenv("VALKEY_URL")
if url:
_redis_client = aioredis.Redis.from_url(url, decode_responses=True, socket_timeout=1.0)
logger.info(f"Valkey client initialized from URL")
url = os.getenv("VALKEY_URL")
if url:
if url.startswith("valkey://"):
url = "redis://" + url[9:]
elif url.startswith("valkeys://"):
url = "rediss://" + url[10:]
_redis_client = aioredis.Redis.from_url(url, decode_responses=True, socket_timeout=1.0)
logger.info(f"Valkey client initialized from URL")

else:
host = os.getenv("VALKEY_HOST", "127.0.0.1")
port = int(os.getenv("VALKEY_PORT", "6379"))
_redis_client = aioredis.Redis(host=host, port=port, decode_responses=True, socket_timeout=1.0)
logger.info(f"Valkey client initialized at {host}:{port}")
except Exception as e:
logger.warning(f"Failed to initialize Valkey client: {e} — falling back to local memory")
_redis_client = None
Expand Down
46 changes: 46 additions & 0 deletions router/tests/test_get_goose_sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import pytest
import sys
import os
from pathlib import Path
from unittest.mock import patch, MagicMock

os.environ["CONFIG_PATH"] = str(Path(__file__).resolve().parent.parent / "config.yaml")
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from main import get_goose_sessions

def test_get_goose_sessions_no_db():
with patch('os.path.exists', return_value=False):
assert get_goose_sessions() == []

def test_get_goose_sessions_success():
mock_sqlite3 = MagicMock()
mock_conn = MagicMock()
mock_cursor = MagicMock()

mock_sqlite3.connect.return_value = mock_conn
mock_conn.cursor.return_value = mock_cursor

mock_cursor.fetchall.return_value = [
{"id": 1, "name": "s1"},
{"id": 2, "name": "s2"}
]

with patch('os.path.exists', return_value=True):
with patch.dict(sys.modules, {'sqlite3': mock_sqlite3}):
result = get_goose_sessions()

assert len(result) == 2
assert result[0] == {"id": 1, "name": "s1"}
mock_sqlite3.connect.assert_called_once_with("/config/goose_sessions/sessions/sessions.db", timeout=1.0)
mock_cursor.execute.assert_called_once()
mock_conn.close.assert_called_once()

def test_get_goose_sessions_exception():
mock_sqlite3 = MagicMock()
mock_sqlite3.connect.side_effect = Exception("DB error")

with patch('os.path.exists', return_value=True):
with patch.dict(sys.modules, {'sqlite3': mock_sqlite3}):
result = get_goose_sessions()
assert result == []
35 changes: 35 additions & 0 deletions router/tests/test_get_redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,38 @@ def test_get_redis_initialization_exception(mock_logger_warning, mock_redis, moc
assert main._redis_last_init_attempt == 110.0
mock_logger_warning.assert_called_once()
assert "Test Exception" in mock_logger_warning.call_args[0][0]

@patch("router.main.time.monotonic")
@patch("router.main.aioredis.Redis.from_url")
@patch.dict(os.environ, {"VALKEY_URL": "redis://my-url:1234"})
def test_get_redis_simulation_flow_url(mock_from_url, mock_monotonic):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Assert that Redis.from_url is called with the expected URL and options

This test only checks that Redis.from_url is called once, but not that it’s called with the expected VALKEY_URL and options. Please assert the exact call, including URL and keyword arguments (e.g. decode_responses and socket_timeout), to verify the new configuration path and prevent regressions if defaults change.

Suggested implementation:

    # 1. First attempt fails
    mock_monotonic.return_value = 10.0
    mock_from_url.side_effect = Exception("Connection error")
    assert main.get_redis() is None
    assert main._redis_last_init_attempt == 10.0

    # Redis.from_url should have been called once with the VALKEY_URL and expected options
    mock_from_url.assert_called_once()
    called_args, called_kwargs = mock_from_url.call_args

    # First positional argument is the VALKEY_URL
    assert called_args[0] == os.environ["VALKEY_URL"]

    # Assert important keyword arguments to guard configuration regressions
    assert "decode_responses" in called_kwargs
    assert "socket_timeout" in called_kwargs

    # 2. Second attempt during cooldown (e.g. 12.0s)
    mock_monotonic.return_value = 12.0
    mock_from_url.reset_mock()

To fully meet your comment about asserting the exact options, you should:

  1. Replace the generic presence checks with value assertions that match get_redis:
    • assert called_kwargs["decode_responses"] is True
    • assert called_kwargs["socket_timeout"] == <EXPECTED_TIMEOUT>
  2. If get_redis passes additional keyword arguments (e.g. max_connections, health_check_interval), add explicit assertions for them as well.

"""Simulate the full flow for from_url: failure -> cooldown -> success -> cached."""
# State is reset by the autouse fixture reset_redis_globals

# 1. First attempt fails
mock_monotonic.return_value = 10.0
mock_from_url.side_effect = Exception("Connection error")
assert main.get_redis() is None
assert main._redis_last_init_attempt == 10.0

# 2. Second attempt during cooldown (e.g. 12.0s)
mock_monotonic.return_value = 12.0
mock_from_url.reset_mock()
assert main.get_redis() is None
mock_from_url.assert_not_called()

# 3. Third attempt after cooldown (e.g. 16.0s) succeeds
mock_monotonic.return_value = 16.0
mock_redis_instance = MagicMock()
mock_from_url.side_effect = None
mock_from_url.return_value = mock_redis_instance
client = main.get_redis()
assert client is mock_redis_instance
assert main._redis_client is mock_redis_instance
mock_from_url.assert_called_once()

# 4. Fourth attempt returns cached instance
mock_monotonic.return_value = 18.0
mock_from_url.reset_mock()
assert main.get_redis() is mock_redis_instance
mock_from_url.assert_not_called()
22 changes: 3 additions & 19 deletions start-stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,6 @@ if [ -z "$LANGFUSE_INIT_USER_PASSWORD" ]; then
echo "✓ Generated new LANGFUSE_INIT_USER_PASSWORD and saved to $ENV_FILE"
fi

if [ -z "$REDIS_AUTH" ]; then
REDIS_AUTH="$(openssl rand -hex 16)"
echo "REDIS_AUTH=\"$REDIS_AUTH\"" >> "$ENV_FILE"
echo "✓ Generated new REDIS_AUTH and saved to $ENV_FILE"
fi

if [ -z "$CLICKHOUSE_PASSWORD" ]; then
CLICKHOUSE_PASSWORD="$(openssl rand -hex 16)"
echo "CLICKHOUSE_PASSWORD=\"$CLICKHOUSE_PASSWORD\"" >> "$ENV_FILE"
echo "✓ Generated new CLICKHOUSE_PASSWORD and saved to $ENV_FILE"
fi


if [ -z "$ROUTER_API_KEY" ]; then
ROUTER_API_KEY="$(openssl rand -hex 32)"
Expand Down Expand Up @@ -382,7 +370,7 @@ if podman pod exists agent-router-pod 2>/dev/null; then
fi

render_pod_yaml() {
export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD REDIS_AUTH CLICKHOUSE_PASSWORD
export WORKDIR HOME LITELLM_MASTER_KEY POSTGRES_PASSWORD NEXTAUTH_SECRET SALT ENCRYPTION_KEY LANGFUSE_INIT_USER_PASSWORD MINIO_ROOT_USER MINIO_ROOT_PASSWORD
python3 - "$WORKDIR/pod.yaml" <<'PY'
import os, sys, urllib.parse
uid = os.getuid()
Expand All @@ -399,10 +387,8 @@ placeholders = [
"ENCRYPTION_KEY_PLACEHOLDER",
"postgres-password-***",
"MINIO_USER_PLACEHOLDER",
"MINIO_PASSWORD_PLACEHOLDER",
"LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER",
"REDIS_AUTH_PLACEHOLDER",
"CLICKHOUSE_PASSWORD_PLACEHOLDER"
"MINIO_PASSWORD_PLACEHOLDER"
"LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER"
]
for ph in placeholders:
if ph not in text:
Expand All @@ -422,8 +408,6 @@ text = text.replace("ENCRYPTION_KEY_PLACEHOLDER", os.environ["ENCRYPTION_KEY"])
text = text.replace("MINIO_USER_PLACEHOLDER", os.environ["MINIO_ROOT_USER"])
text = text.replace("MINIO_PASSWORD_PLACEHOLDER", os.environ["MINIO_ROOT_PASSWORD"])
text = text.replace("LANGFUSE_INIT_USER_PASSWORD_PLACEHOLDER", os.environ["LANGFUSE_INIT_USER_PASSWORD"])
text = text.replace("REDIS_AUTH_PLACEHOLDER", os.environ["REDIS_AUTH"])
text = text.replace("CLICKHOUSE_PASSWORD_PLACEHOLDER", os.environ["CLICKHOUSE_PASSWORD"])
sys.stdout.write(text)
PY
}
Expand Down