Skip to content

Commit ea0a335

Browse files
committed
Fix load shedding error handling and cleanup
1 parent 9612589 commit ea0a335

File tree

3 files changed

+64
-16
lines changed

3 files changed

+64
-16
lines changed

app/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def authentication_error(error: AuthError) -> ResponseReturnValue:
5656

5757
@blueprint.errorhandler(ServiceUnavailableError)
5858
def service_unavailable_error(error: ServiceUnavailableError) -> ResponseReturnValue:
59-
response = jsonify(error.to_dict_v2())
59+
response = jsonify(result="error", message=error.message)
6060
response.status_code = 429
6161
response.headers["Retry-After"] = str(error.retry_after)
6262
current_app.logger.info(error)

app/load_shedding.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def __init__(self, window_seconds: float = 60.0) -> None:
5252
"""
5353
self._requests: dict[str, deque[float]] = {}
5454
self._window = window_seconds
55+
self._last_cleanup = 0.0
56+
self._cleanup_interval = window_seconds
5557

5658
def track_request(self, service_id: str) -> None:
5759
"""Record a request from this service.
@@ -68,6 +70,11 @@ def track_request(self, service_id: str) -> None:
6870
# Lazy cleanup of old entries to keep memory bounded
6971
self._cleanup_old_entries(service_id, now)
7072

73+
# Periodic cleanup across all services to avoid unbounded growth
74+
if now - self._last_cleanup >= self._cleanup_interval:
75+
self._cleanup_all(now)
76+
self._last_cleanup = now
77+
7178
def _cleanup_old_entries(self, service_id: str, now: float) -> None:
7279
"""Remove entries older than the window.
7380
@@ -84,6 +91,22 @@ def _cleanup_old_entries(self, service_id: str, now: float) -> None:
8491
if not queue:
8592
del self._requests[service_id]
8693

94+
def _cleanup_all(self, now: float) -> None:
95+
"""Remove expired entries for all services.
96+
97+
Args:
98+
now: Current timestamp
99+
"""
100+
cutoff = now - self._window
101+
102+
for service_id in list(self._requests.keys()):
103+
queue = self._requests[service_id]
104+
while queue and queue[0] < cutoff:
105+
queue.popleft()
106+
107+
if not queue:
108+
del self._requests[service_id]
109+
87110
def get_volumes(self) -> dict[str, int]:
88111
"""Get current volumes for all services that have recent requests.
89112

app/v2/errors.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from app.authentication.auth import AuthError
1111
from app.clients.document_download import DocumentDownloadError
1212
from app.errors import InvalidRequest
13+
from app.load_shedding import ServiceUnavailableError
1314

1415

1516
class TooManyRequestsError(InvalidRequest):
@@ -84,7 +85,23 @@ def to_dict_v2(self):
8485
}
8586

8687

87-
def register_errors(blueprint):
88+
def _register_rate_limit_errors(blueprint):
89+
@blueprint.errorhandler(ServiceUnavailableError)
90+
def service_unavailable_error(error: ServiceUnavailableError):
91+
current_app.logger.info(error)
92+
response = jsonify(error.to_dict_v2())
93+
response.status_code = 429
94+
response.headers["Retry-After"] = str(error.retry_after)
95+
return response
96+
97+
@blueprint.errorhandler(InvalidRequest)
98+
def invalid_data(error):
99+
current_app.logger.info(error)
100+
response = jsonify(error.to_dict_v2()), error.status_code
101+
return response
102+
103+
104+
def _register_validation_errors(blueprint):
88105
@blueprint.errorhandler(InvalidRecipientError)
89106
def invalid_format(error):
90107
current_app.logger.info(error)
@@ -109,36 +126,36 @@ def invalid_phone(error):
109126
400,
110127
)
111128

112-
@blueprint.errorhandler(InvalidRequest)
113-
def invalid_data(error):
114-
current_app.logger.info(error)
115-
response = jsonify(error.to_dict_v2()), error.status_code
116-
return response
117-
118-
@blueprint.errorhandler(DocumentDownloadError)
119-
def document_download_error(error: DocumentDownloadError):
120-
current_app.logger.info(error)
121-
# cast it to a BadRequestError to ensure we format the error well
122-
bad_request_exc = BadRequestError(message=error.message)
123-
response = jsonify(bad_request_exc.to_dict_v2()), error.status_code
124-
return response
125-
126129
@blueprint.errorhandler(JsonSchemaValidationError)
127130
def validation_error(error):
128131
current_app.logger.info(error)
129132
return jsonify(json.loads(error.message)), 400
130133

134+
135+
def _register_data_errors(blueprint):
131136
@blueprint.errorhandler(NoResultFound)
132137
@blueprint.errorhandler(DataError)
133138
def no_result_found(e):
134139
current_app.logger.info(e, exc_info=True)
135140
return jsonify(status_code=404, errors=[{"error": e.__class__.__name__, "message": "No result found"}]), 404
136141

142+
143+
def _register_auth_errors(blueprint):
137144
@blueprint.errorhandler(AuthError)
138145
def auth_error(error):
139146
current_app.logger.info("API AuthError, client: %s error: %s", request.headers.get("User-Agent"), error)
140147
return jsonify(error.to_dict_v2()), error.code
141148

149+
150+
def _register_misc_errors(blueprint):
151+
@blueprint.errorhandler(DocumentDownloadError)
152+
def document_download_error(error: DocumentDownloadError):
153+
current_app.logger.info(error)
154+
# cast it to a BadRequestError to ensure we format the error well
155+
bad_request_exc = BadRequestError(message=error.message)
156+
response = jsonify(bad_request_exc.to_dict_v2()), error.status_code
157+
return response
158+
142159
@blueprint.errorhandler(EventletTimeout)
143160
def eventlet_timeout(error):
144161
current_app.logger.exception(error)
@@ -156,3 +173,11 @@ def internal_server_error(error):
156173
jsonify(status_code=500, errors=[{"error": error.__class__.__name__, "message": "Internal server error"}]),
157174
500,
158175
)
176+
177+
178+
def register_errors(blueprint):
179+
_register_rate_limit_errors(blueprint)
180+
_register_validation_errors(blueprint)
181+
_register_data_errors(blueprint)
182+
_register_auth_errors(blueprint)
183+
_register_misc_errors(blueprint)

0 commit comments

Comments
 (0)