From 291ad6ad0b3dab667f5c93c7ad4d90b44feee28e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 20:23:50 +0000 Subject: [PATCH 1/5] Add busy loop detector to prevent infinite loops during pagination Introduces a BusyLoopDetector class that monitors call frequency within a time window and raises an error if a threshold is exceeded. Integrated into ProjectDatabase.get_items(). Also updated README to point candidates to api/pagination.py. https://claude.ai/code/session_01MMKGE3aKehnxGfV8H6uvNt --- README.md | 4 ++++ api/busy_loop_detector.py | 28 ++++++++++++++++++++++++++++ api/database.py | 4 ++++ 3 files changed, 36 insertions(+) create mode 100644 api/busy_loop_detector.py diff --git a/README.md b/README.md index 9f23ae7..8f1400d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ Welcome to the Hex Web Development Interview! During the interview session, you'll use this boilerplate repository to build out a frontend application with your interviewer. Before the session starts, please clone this repository and run `npm install` and `uv sync` in the root to install dependencies. You can also add any other dependencies that you think that you may want to use, but there is no need to write any code or do anything else ahead of time to prepare for the session. +## Interview + +During the interview session, you'll implement two functions in this repository to fix an application. All changes will be made to [`api/pagination.py`](api/pagination.py). + ## Running the app 1) Install JS dependencies with `npm install`. diff --git a/api/busy_loop_detector.py b/api/busy_loop_detector.py new file mode 100644 index 0000000..29a0c05 --- /dev/null +++ b/api/busy_loop_detector.py @@ -0,0 +1,28 @@ +import time + + +class BusyLoopDetector: + """ + Detects busy loops by tracking the number of calls within a time window. + If the number of calls exceeds the threshold within the window, it raises + an error to prevent infinite loops from consuming resources. + """ + + def __init__(self, threshold: int = 1000, time_window: float = 1.0): + self._counter = 0 + self._threshold = threshold + self._time_window = time_window + self._start_time = time.monotonic() + + def _reset_timer(self) -> None: + self._start_time = time.monotonic() + self._counter = 0 + + def check(self) -> None: + self._counter += 1 + current_time = time.monotonic() + + if current_time - self._start_time > self._time_window: + self._reset_timer() + elif self._counter > self._threshold: + raise RuntimeError("Busy loop detected") diff --git a/api/database.py b/api/database.py index 9afcb62..55cfd0c 100644 --- a/api/database.py +++ b/api/database.py @@ -2,6 +2,7 @@ import json from typing import List, Optional from models import Project +from busy_loop_detector import BusyLoopDetector class ProjectDatabase: @@ -11,6 +12,7 @@ def __init__(self): raw_data = json.load(f) # Convert raw dictionaries to Project objects self._items = [Project(**item) for item in raw_data] + self._detector = BusyLoopDetector() def get_items( self, page_size: int = 10, start_after: Optional[Project] = None @@ -26,6 +28,8 @@ def get_items( Returns: List of Project objects for the requested page """ + self._detector.check() + if start_after is None: return self._items[:page_size] From 7e12e2bd974df18443f1913db107ad965a094c96 Mon Sep 17 00:00:00 2001 From: Sheng Wu Date: Fri, 13 Mar 2026 16:30:12 -0400 Subject: [PATCH 2/5] fix --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 0a6005d..044b23e 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,6 @@ Welcome to the Hex Coding Interview! During the interview session, you'll implem Before the session starts, please clone this repository and run `npm install` and `uv sync` in the root to install dependencies. You can also add any other dependencies that you think that you may want to use, but there is no need to write any code or do anything else ahead of time to prepare for the session. -## Interview - -During the interview session, you'll implement two functions in this repository to fix an application. All changes will be made to [`api/pagination.py`](api/pagination.py). - ## Running the app 1) Install JS dependencies with `npm install`. From b58cc4534a22349d3d326cd8adaec1f1c5e1ef18 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 20:32:34 +0000 Subject: [PATCH 3/5] Fix busy loop detector: use module-level singleton and correct counter reset - Move detector from per-instance to module-level so it persists across ProjectDatabase instantiations. Previously, get_page() creating a new ProjectDatabase each call meant candidates looping over get_page() would get a fresh detector every iteration, defeating the protection. - Reset counter to 1 (not 0) in _reset_timer so the call that triggers the window reset is properly counted in the new window. https://claude.ai/code/session_01MMKGE3aKehnxGfV8H6uvNt --- api/busy_loop_detector.py | 2 +- api/database.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/busy_loop_detector.py b/api/busy_loop_detector.py index 29a0c05..8101079 100644 --- a/api/busy_loop_detector.py +++ b/api/busy_loop_detector.py @@ -16,7 +16,7 @@ def __init__(self, threshold: int = 1000, time_window: float = 1.0): def _reset_timer(self) -> None: self._start_time = time.monotonic() - self._counter = 0 + self._counter = 1 def check(self) -> None: self._counter += 1 diff --git a/api/database.py b/api/database.py index 55cfd0c..88b6f8d 100644 --- a/api/database.py +++ b/api/database.py @@ -5,6 +5,9 @@ from busy_loop_detector import BusyLoopDetector +_detector = BusyLoopDetector() + + class ProjectDatabase: def __init__(self): data_dir = Path(__file__).parent.parent / "src" / "api" / "data" @@ -12,7 +15,6 @@ def __init__(self): raw_data = json.load(f) # Convert raw dictionaries to Project objects self._items = [Project(**item) for item in raw_data] - self._detector = BusyLoopDetector() def get_items( self, page_size: int = 10, start_after: Optional[Project] = None @@ -28,7 +30,7 @@ def get_items( Returns: List of Project objects for the requested page """ - self._detector.check() + _detector.check() if start_after is None: return self._items[:page_size] From 5dd9735bbb79ec12594c157a09094e068e2d880b Mon Sep 17 00:00:00 2001 From: Sheng Wu Date: Fri, 13 Mar 2026 16:56:10 -0400 Subject: [PATCH 4/5] real fix --- src/Projects.tsx | 10 +++++++--- src/api/apiImpl.ts | 37 +++++++++++++++++++++++++++++++++++-- src/index.tsx | 36 +++++++++++++++++++++++------------- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/Projects.tsx b/src/Projects.tsx index 9d6fdc6..d278687 100644 --- a/src/Projects.tsx +++ b/src/Projects.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { ProjectData, SERVER } from "./api"; +import { ProjectData, ProjectsRequestTimeoutError, SERVER } from "./api"; export interface NameById { [key: number]: string; @@ -22,7 +22,11 @@ export default function Projects({ selectedUser, nameById }: ProjectsProps) { setProjects(projects => [...(projects ?? []), ...page.projects]); } setHasMoreResults(page.hasMoreResults); - }).catch(() => { + }).catch((error) => { + if (error instanceof ProjectsRequestTimeoutError) { + alert(error.message); + return; + } alert("Something went wrong..."); }); }, [selectedUser]); @@ -47,4 +51,4 @@ export default function Projects({ selectedUser, nameById }: ProjectsProps) { ); -} \ No newline at end of file +} diff --git a/src/api/apiImpl.ts b/src/api/apiImpl.ts index 41a1a55..6e172df 100644 --- a/src/api/apiImpl.ts +++ b/src/api/apiImpl.ts @@ -5,6 +5,27 @@ export interface ProjectsResponse { hasMoreResults: boolean; } +const PROJECTS_REQUEST_TIMEOUT_MS = 1000; + +export const PROJECTS_REQUEST_TIMEOUT_MESSAGE = + "Request timed out. Possible infinite loop in api/pagination.py."; + +export class ProjectsRequestTimeoutError extends Error { + constructor(message: string = PROJECTS_REQUEST_TIMEOUT_MESSAGE) { + super(message); + this.name = "ProjectsRequestTimeoutError"; + } +} + +function isAbortError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "name" in error && + error.name === "AbortError" + ); +} + class DefaultServer { async getUsers(): Promise { const response = await fetch('http://127.0.0.1:5000/api/users'); @@ -28,8 +49,20 @@ class DefaultServer { url.searchParams.append('pageSize', options.pageSize.toString()); } - const response = await fetch(url); - return response.json(); + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), PROJECTS_REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(url, { signal: controller.signal }); + return response.json(); + } catch (error) { + if (isAbortError(error)) { + throw new ProjectsRequestTimeoutError(); + } + throw error; + } finally { + window.clearTimeout(timeoutId); + } } } diff --git a/src/index.tsx b/src/index.tsx index 06bf5aa..7fc7a2b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,7 @@ import { render } from "react-dom"; import App from "./App"; import { PROJECTS, USERS } from "./api/data"; -import { ProjectData, ProjectsResponse, SERVER, UserData } from "./api"; +import { ProjectData, ProjectsRequestTimeoutError, ProjectsResponse, SERVER, UserData } from "./api"; const rootElement = document.getElementById("root"); render(, rootElement); @@ -16,18 +16,26 @@ render(, rootElement); let hasMoreResults = true; let lastProject: ProjectData | undefined = undefined; - while (hasMoreResults) { - const page: ProjectsResponse = await SERVER.getProjects({ pageSize, startAfter: lastProject, userId: user?.id?.toString() }); - if (page.hasMoreResults && page.projects.length < pageSize) { - console.log( - `❌ ${userString} // Improperly sized page - hasMoreResults: true but results.length < pageSize` - ); - console.groupEnd(); + try { + while (hasMoreResults) { + const page: ProjectsResponse = await SERVER.getProjects({ pageSize, startAfter: lastProject, userId: user?.id?.toString() }); + if (page.hasMoreResults && page.projects.length < pageSize) { + console.log( + `❌ ${userString} // Improperly sized page - hasMoreResults: true but results.length < pageSize` + ); + console.groupEnd(); + return; + } + totalCountFromApi += page.projects.length; + hasMoreResults = page.hasMoreResults; + lastProject = page.projects[page.projects.length - 1]; + } + } catch (error) { + if (error instanceof ProjectsRequestTimeoutError) { + console.log(`❌ ${userString} // ${error.message}`); return; } - totalCountFromApi += page.projects.length; - hasMoreResults = page.hasMoreResults; - lastProject = page.projects[page.projects.length - 1]; + throw error; } const filterCallback = user ? (project: ProjectData) => project.creatorId === user.id : undefined; @@ -39,5 +47,7 @@ render(, rootElement); console.log(`✅ ${userString} // Counts match: ${totalCountFromApi}`); } } - Promise.all([...USERS.map(testUser), testUser()]); -} \ No newline at end of file + Promise.all([...USERS.map(testUser), testUser()]).catch((error) => { + console.error(error); + }); +} From 927e496f284a61228d8c799f9e8e8b8831b89cef Mon Sep 17 00:00:00 2001 From: Sheng Wu Date: Fri, 13 Mar 2026 16:56:24 -0400 Subject: [PATCH 5/5] revert --- api/busy_loop_detector.py | 28 ---------------------------- api/database.py | 6 ------ 2 files changed, 34 deletions(-) delete mode 100644 api/busy_loop_detector.py diff --git a/api/busy_loop_detector.py b/api/busy_loop_detector.py deleted file mode 100644 index 8101079..0000000 --- a/api/busy_loop_detector.py +++ /dev/null @@ -1,28 +0,0 @@ -import time - - -class BusyLoopDetector: - """ - Detects busy loops by tracking the number of calls within a time window. - If the number of calls exceeds the threshold within the window, it raises - an error to prevent infinite loops from consuming resources. - """ - - def __init__(self, threshold: int = 1000, time_window: float = 1.0): - self._counter = 0 - self._threshold = threshold - self._time_window = time_window - self._start_time = time.monotonic() - - def _reset_timer(self) -> None: - self._start_time = time.monotonic() - self._counter = 1 - - def check(self) -> None: - self._counter += 1 - current_time = time.monotonic() - - if current_time - self._start_time > self._time_window: - self._reset_timer() - elif self._counter > self._threshold: - raise RuntimeError("Busy loop detected") diff --git a/api/database.py b/api/database.py index 88b6f8d..9afcb62 100644 --- a/api/database.py +++ b/api/database.py @@ -2,10 +2,6 @@ import json from typing import List, Optional from models import Project -from busy_loop_detector import BusyLoopDetector - - -_detector = BusyLoopDetector() class ProjectDatabase: @@ -30,8 +26,6 @@ def get_items( Returns: List of Project objects for the requested page """ - _detector.check() - if start_after is None: return self._items[:page_size]