Skip to content

Commit 32ffcd3

Browse files
CopilotMte90
andcommitted
Add production-ready error handling and retry logic
Co-authored-by: Mte90 <403283+Mte90@users.noreply.github.com>
1 parent dd27993 commit 32ffcd3

File tree

2 files changed

+154
-56
lines changed

2 files changed

+154
-56
lines changed

main.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,25 @@ class CodeCompletionRequest(BaseModel):
6161
@app.post("/api/projects")
6262
def api_create_project(request: CreateProjectRequest):
6363
"""Create or get a project with per-project database."""
64+
import logging
65+
logger = logging.getLogger(__name__)
66+
6467
try:
68+
# Validate input
69+
if not request.path:
70+
return JSONResponse({"error": "Project path is required"}, status_code=400)
71+
6572
project = get_or_create_project(request.path, request.name)
6673
return JSONResponse(project)
67-
except Exception as e:
74+
except ValueError as e:
75+
logger.warning(f"Validation error creating project: {e}")
6876
return JSONResponse({"error": str(e)}, status_code=400)
77+
except RuntimeError as e:
78+
logger.error(f"Runtime error creating project: {e}")
79+
return JSONResponse({"error": str(e)}, status_code=500)
80+
except Exception as e:
81+
logger.exception(f"Unexpected error creating project: {e}")
82+
return JSONResponse({"error": "Internal server error"}, status_code=500)
6983

7084

7185
@app.get("/api/projects")

projects.py

Lines changed: 139 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,51 @@
66
import json
77
import sqlite3
88
import hashlib
9+
import logging
910
from pathlib import Path
1011
from typing import Dict, List, Optional, Any
1112
from datetime import datetime
1213

14+
# Configure logging
15+
logging.basicConfig(level=logging.INFO)
16+
logger = logging.getLogger(__name__)
17+
1318
# Default projects directory
1419
PROJECTS_DIR = os.path.expanduser("~/.picocode/projects")
1520

21+
# Retry configuration for database operations
22+
DB_RETRY_COUNT = 3
23+
DB_RETRY_DELAY = 0.1 # seconds
24+
1625

1726
def _ensure_projects_dir():
1827
"""Ensure projects directory exists."""
19-
os.makedirs(PROJECTS_DIR, exist_ok=True)
28+
try:
29+
os.makedirs(PROJECTS_DIR, exist_ok=True)
30+
except Exception as e:
31+
logger.error(f"Failed to create projects directory {PROJECTS_DIR}: {e}")
32+
raise
33+
34+
35+
def _retry_on_db_locked(func, *args, max_retries=DB_RETRY_COUNT, **kwargs):
36+
"""Retry a database operation if it's locked."""
37+
import time
38+
last_error = None
39+
40+
for attempt in range(max_retries):
41+
try:
42+
return func(*args, **kwargs)
43+
except sqlite3.OperationalError as e:
44+
if "database is locked" in str(e).lower() and attempt < max_retries - 1:
45+
last_error = e
46+
time.sleep(DB_RETRY_DELAY * (2 ** attempt)) # Exponential backoff
47+
continue
48+
raise
49+
except Exception as e:
50+
raise
51+
52+
if last_error:
53+
raise last_error
2054

2155

2256
def _get_project_id(project_path: str) -> str:
@@ -37,29 +71,44 @@ def _get_projects_registry_path() -> str:
3771

3872

3973
def _init_registry_db():
40-
"""Initialize the projects registry database."""
74+
"""Initialize the projects registry database with proper configuration."""
4175
registry_path = _get_projects_registry_path()
42-
conn = sqlite3.connect(registry_path)
43-
conn.row_factory = sqlite3.Row
44-
try:
45-
cur = conn.cursor()
46-
cur.execute(
47-
"""
48-
CREATE TABLE IF NOT EXISTS projects (
49-
id TEXT PRIMARY KEY,
50-
name TEXT NOT NULL,
51-
path TEXT NOT NULL UNIQUE,
52-
database_path TEXT NOT NULL,
53-
created_at TEXT DEFAULT (datetime('now')),
54-
last_indexed_at TEXT,
55-
status TEXT DEFAULT 'created',
56-
settings TEXT
76+
77+
def _init():
78+
conn = sqlite3.connect(registry_path, timeout=10.0)
79+
conn.row_factory = sqlite3.Row
80+
try:
81+
# Enable WAL mode for better concurrency
82+
conn.execute("PRAGMA journal_mode=WAL;")
83+
conn.execute("PRAGMA synchronous=NORMAL;")
84+
85+
cur = conn.cursor()
86+
cur.execute(
87+
"""
88+
CREATE TABLE IF NOT EXISTS projects (
89+
id TEXT PRIMARY KEY,
90+
name TEXT NOT NULL,
91+
path TEXT NOT NULL UNIQUE,
92+
database_path TEXT NOT NULL,
93+
created_at TEXT DEFAULT (datetime('now')),
94+
last_indexed_at TEXT,
95+
status TEXT DEFAULT 'created',
96+
settings TEXT
97+
)
98+
"""
5799
)
58-
"""
59-
)
60-
conn.commit()
61-
finally:
62-
conn.close()
100+
conn.commit()
101+
except Exception as e:
102+
logger.error(f"Failed to initialize registry database: {e}")
103+
raise
104+
finally:
105+
conn.close()
106+
107+
try:
108+
_retry_on_db_locked(_init)
109+
except Exception as e:
110+
logger.error(f"Failed to initialize registry after retries: {e}")
111+
raise
63112

64113

65114
def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, Any]:
@@ -72,11 +121,25 @@ def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, A
72121
73122
Returns:
74123
Project metadata dictionary
124+
125+
Raises:
126+
ValueError: If project path is invalid
127+
RuntimeError: If database operations fail
75128
"""
76-
_init_registry_db()
129+
try:
130+
_init_registry_db()
131+
except Exception as e:
132+
logger.error(f"Failed to initialize registry: {e}")
133+
raise RuntimeError(f"Database initialization failed: {e}")
77134

78-
# Normalize path
79-
project_path = os.path.abspath(project_path)
135+
# Validate and normalize path
136+
if not project_path or not isinstance(project_path, str):
137+
raise ValueError("Project path must be a non-empty string")
138+
139+
try:
140+
project_path = os.path.abspath(project_path)
141+
except Exception as e:
142+
raise ValueError(f"Invalid project path: {e}")
80143

81144
if not os.path.exists(project_path):
82145
raise ValueError(f"Project path does not exist: {project_path}")
@@ -92,38 +155,59 @@ def create_project(project_path: str, name: Optional[str] = None) -> Dict[str, A
92155
if not name:
93156
name = os.path.basename(project_path)
94157

158+
# Sanitize project name
159+
if name and len(name) > 255:
160+
name = name[:255]
161+
95162
registry_path = _get_projects_registry_path()
96-
conn = sqlite3.connect(registry_path)
97-
conn.row_factory = sqlite3.Row
163+
164+
def _create():
165+
conn = sqlite3.connect(registry_path, timeout=10.0)
166+
conn.row_factory = sqlite3.Row
167+
try:
168+
cur = conn.cursor()
169+
170+
# Check if project already exists
171+
cur.execute("SELECT * FROM projects WHERE path = ?", (project_path,))
172+
existing = cur.fetchone()
173+
if existing:
174+
logger.info(f"Project already exists: {project_path}")
175+
return dict(existing)
176+
177+
# Insert new project
178+
cur.execute(
179+
"""
180+
INSERT INTO projects (id, name, path, database_path, status)
181+
VALUES (?, ?, ?, ?, 'created')
182+
""",
183+
(project_id, name, project_path, db_path)
184+
)
185+
conn.commit()
186+
187+
# Initialize project database
188+
try:
189+
from db import init_db
190+
init_db(db_path)
191+
logger.info(f"Created project {project_id} at {db_path}")
192+
except Exception as e:
193+
logger.error(f"Failed to initialize project database: {e}")
194+
# Rollback project creation
195+
cur.execute("DELETE FROM projects WHERE id = ?", (project_id,))
196+
conn.commit()
197+
raise RuntimeError(f"Failed to initialize project database: {e}")
198+
199+
# Return project metadata
200+
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
201+
row = cur.fetchone()
202+
return dict(row) if row else None
203+
finally:
204+
conn.close()
205+
98206
try:
99-
cur = conn.cursor()
100-
101-
# Check if project already exists
102-
cur.execute("SELECT * FROM projects WHERE path = ?", (project_path,))
103-
existing = cur.fetchone()
104-
if existing:
105-
return dict(existing)
106-
107-
# Insert new project
108-
cur.execute(
109-
"""
110-
INSERT INTO projects (id, name, path, database_path, status)
111-
VALUES (?, ?, ?, ?, 'created')
112-
""",
113-
(project_id, name, project_path, db_path)
114-
)
115-
conn.commit()
116-
117-
# Initialize project database
118-
from db import init_db
119-
init_db(db_path)
120-
121-
# Return project metadata
122-
cur.execute("SELECT * FROM projects WHERE id = ?", (project_id,))
123-
row = cur.fetchone()
124-
return dict(row)
125-
finally:
126-
conn.close()
207+
return _retry_on_db_locked(_create)
208+
except Exception as e:
209+
logger.error(f"Failed to create project: {e}")
210+
raise
127211

128212

129213
def get_project(project_path: str) -> Optional[Dict[str, Any]]:

0 commit comments

Comments
 (0)