Skip to content

Commit 9b1012b

Browse files
anshul23102claude
andcommitted
fix: implement thread-safe in-memory cache in data_loader.py
_projects_cache was declared but load_all_projects() never read or wrote it, causing a redundant disk read on every request. Added double-checked locking with threading.Lock so the JSON file is read once and reused safely across concurrent requests. clear_cache() now acquires the same lock before resetting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 47ae1b6 commit 9b1012b

1 file changed

Lines changed: 29 additions & 29 deletions

File tree

utils/data_loader.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,58 @@
1-
# utils/data_loader.py
2-
# Handles all reading and lookup of project data from the JSON file.
3-
41
import json
52
import os
3+
import threading
64

7-
# Build the path to projects.json relative to this file's location
85
DATA_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "projects.json")
96

7+
_projects_cache = None
8+
_cache_lock = threading.Lock()
9+
1010

1111
def load_all_projects():
12-
"""Read and return the full list of projects from the JSON file."""
13-
with open(DATA_FILE, "r", encoding="utf-8") as f:
14-
return json.load(f)
12+
"""Read and return the full list of projects from the JSON file.
13+
14+
Results are cached in memory after the first read so subsequent calls
15+
do not hit the filesystem.
16+
"""
17+
global _projects_cache
18+
if _projects_cache is not None:
19+
return _projects_cache
20+
with _cache_lock:
21+
if _projects_cache is None:
22+
with open(DATA_FILE, "r", encoding="utf-8") as f:
23+
_projects_cache = json.load(f)
24+
return _projects_cache
1525

1626

1727
def find_project_by_id(project_id):
18-
"""
19-
Return the project dict whose 'id' matches the given integer.
20-
Returns None if no match is found.
21-
"""
28+
"""Return the project whose 'id' matches project_id, or None."""
2229
for project in load_all_projects():
2330
if project.get("id") == project_id:
2431
return project
2532
return None
2633

34+
2735
def get_project_stats():
28-
"""
29-
Calculate and return statistics about the projects.
30-
Returns: { total_projects, unique_skills, beginner_friendly }
31-
"""
36+
"""Return total_projects, unique_skills, and beginner_friendly counts."""
3237
projects = load_all_projects()
33-
total_projects = len(projects)
3438

35-
# Collect all unique skills
3639
all_skills = set()
40+
beginner_friendly = 0
3741
for p in projects:
3842
for s in p.get("skills", []):
3943
all_skills.add(s)
40-
unique_skills = len(all_skills)
41-
42-
# Count beginner projects
43-
beginner_friendly = len([p for p in projects if p.get("level") == "Beginner"])
44+
if p.get("level") == "Beginner":
45+
beginner_friendly += 1
4446

4547
return {
46-
"total_projects": total_projects,
47-
"unique_skills": unique_skills,
48-
"beginner_friendly": beginner_friendly
48+
"total_projects": len(projects),
49+
"unique_skills": len(all_skills),
50+
"beginner_friendly": beginner_friendly,
4951
}
5052

51-
# Cache for loaded projects
52-
_projects_cache = None
53-
5453

5554
def clear_cache():
56-
"""Reset the in-memory project cache."""
55+
"""Reset the in-memory project cache (used in tests)."""
5756
global _projects_cache
58-
_projects_cache = None
57+
with _cache_lock:
58+
_projects_cache = None

0 commit comments

Comments
 (0)