Skip to content

Commit b630c26

Browse files
authored
Merge pull request #151 from Flagsmith/feat/liveness-readiness
feat: Add liveness and readiness check endpoints. Return 503 instead of 500 when checks fail
2 parents 9b67c93 + 91eaa4b commit b630c26

File tree

3 files changed

+100
-71
lines changed

3 files changed

+100
-71
lines changed

src/edge_proxy/server.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from fastapi import FastAPI, Header
66
from fastapi.middleware.cors import CORSMiddleware
77
from fastapi.middleware.gzip import GZipMiddleware
8-
from fastapi.responses import ORJSONResponse
8+
from fastapi.responses import ORJSONResponse, Response
99

1010
from edge_proxy.health_check.responses import HealthCheckResponse
1111
from fastapi_utils.tasks import repeat_every
@@ -40,11 +40,12 @@ async def unknown_key_error(request, exc):
4040

4141
@app.get("/health", response_class=ORJSONResponse, deprecated=True)
4242
@app.get("/proxy/health", response_class=ORJSONResponse)
43+
@app.get("/proxy/health/readiness", response_class=ORJSONResponse)
4344
async def health_check():
4445
last_updated_at = environment_service.last_updated_at
4546
if not last_updated_at:
4647
return HealthCheckResponse(
47-
status_code=500,
48+
status_code=503,
4849
status="error",
4950
reason="environment document(s) not updated.",
5051
last_successful_update=None,
@@ -58,7 +59,7 @@ async def health_check():
5859
)
5960
if last_updated_at < threshold:
6061
return HealthCheckResponse(
61-
status_code=500,
62+
status_code=503,
6263
status="error",
6364
reason="environment document(s) stale.",
6465
last_successful_update=last_updated_at,
@@ -67,6 +68,11 @@ async def health_check():
6768
return HealthCheckResponse(last_successful_update=last_updated_at)
6869

6970

71+
@app.get("/proxy/health/liveness")
72+
async def liveness_check():
73+
return Response(status_code=200)
74+
75+
7076
@app.get("/api/v1/flags/", response_class=ORJSONResponse)
7177
async def flags(feature: str = None, x_environment_key: str = Header(None)):
7278
try:

tests/test_health.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from datetime import datetime, timedelta
2+
3+
import pytest
4+
from fastapi.testclient import TestClient
5+
from pytest_mock import MockerFixture
6+
7+
from edge_proxy.settings import HealthCheckSettings
8+
9+
READINESS_ENDPOINTS = [
10+
"/proxy/health/readiness",
11+
"/proxy/health",
12+
"/health",
13+
]
14+
15+
16+
def test_liveness_check(client: TestClient) -> None:
17+
response = client.get("/proxy/health/liveness")
18+
assert response.status_code == 200
19+
20+
21+
@pytest.mark.parametrize("endpoint", READINESS_ENDPOINTS)
22+
def test_health_check_returns_200_if_cache_was_updated_recently(
23+
mocker: MockerFixture,
24+
client: TestClient,
25+
endpoint: str,
26+
) -> None:
27+
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
28+
mocked_environment_service.last_updated_at = datetime.now()
29+
30+
response = client.get(endpoint)
31+
assert response.status_code == 200
32+
33+
34+
@pytest.mark.parametrize("endpoint", READINESS_ENDPOINTS)
35+
def test_health_check_returns_503_if_cache_was_not_updated(
36+
client: TestClient,
37+
endpoint: str,
38+
) -> None:
39+
response = client.get(endpoint)
40+
assert response.status_code == 503
41+
assert response.json() == {
42+
"status": "error",
43+
"reason": "environment document(s) not updated.",
44+
"last_successful_update": None,
45+
}
46+
47+
48+
@pytest.mark.parametrize("endpoint", READINESS_ENDPOINTS)
49+
def test_health_check_returns_503_if_cache_is_stale(
50+
mocker: MockerFixture,
51+
client: TestClient,
52+
endpoint: str,
53+
) -> None:
54+
last_updated_at = datetime.now() - timedelta(days=10)
55+
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
56+
mocked_environment_service.last_updated_at = last_updated_at
57+
58+
response = client.get(endpoint)
59+
60+
assert response.status_code == 503
61+
assert response.json() == {
62+
"status": "error",
63+
"reason": "environment document(s) stale.",
64+
"last_successful_update": last_updated_at.isoformat(),
65+
}
66+
67+
68+
@pytest.mark.parametrize("endpoint", READINESS_ENDPOINTS)
69+
def test_health_check_returns_200_if_cache_is_never_stale(
70+
mocker: MockerFixture,
71+
client: TestClient,
72+
endpoint: str,
73+
) -> None:
74+
# Given
75+
health_check = HealthCheckSettings(environment_update_grace_period_seconds=None)
76+
mocker.patch("edge_proxy.server.settings.health_check", health_check)
77+
78+
last_updated_at = datetime.now() - timedelta(days=10)
79+
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
80+
mocked_environment_service.last_updated_at = last_updated_at
81+
82+
# When
83+
response = client.get(endpoint)
84+
85+
# Then
86+
assert response.status_code == 200
87+
assert response.json() == {
88+
"status": "ok",
89+
"reason": None,
90+
"last_successful_update": last_updated_at.isoformat(),
91+
}

tests/test_server.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,15 @@
1-
from datetime import datetime, timedelta
21
import typing
32

43
import orjson
5-
import pytest
64
from fastapi.testclient import TestClient
75
from pytest_mock import MockerFixture
86

9-
from edge_proxy.settings import HealthCheckSettings
107
from tests.fixtures.response_data import environment_1
118

129
if typing.TYPE_CHECKING:
1310
from edge_proxy.environments import EnvironmentService
1411

1512

16-
@pytest.mark.parametrize("endpoint", ["/proxy/health", "/health"])
17-
def test_health_check_returns_200_if_cache_was_updated_recently(
18-
mocker: MockerFixture,
19-
endpoint: str,
20-
client: TestClient,
21-
) -> None:
22-
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
23-
mocked_environment_service.last_updated_at = datetime.now()
24-
25-
response = client.get(endpoint)
26-
assert response.status_code == 200
27-
28-
29-
def test_health_check_returns_500_if_cache_was_not_updated(
30-
client: TestClient,
31-
) -> None:
32-
response = client.get("/proxy/health")
33-
assert response.status_code == 500
34-
assert response.json() == {
35-
"status": "error",
36-
"reason": "environment document(s) not updated.",
37-
"last_successful_update": None,
38-
}
39-
40-
41-
def test_health_check_returns_500_if_cache_is_stale(
42-
mocker: MockerFixture,
43-
client: TestClient,
44-
) -> None:
45-
last_updated_at = datetime.now() - timedelta(days=10)
46-
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
47-
mocked_environment_service.last_updated_at = last_updated_at
48-
response = client.get("/proxy/health")
49-
assert response.status_code == 500
50-
assert response.json() == {
51-
"status": "error",
52-
"reason": "environment document(s) stale.",
53-
"last_successful_update": last_updated_at.isoformat(),
54-
}
55-
56-
57-
def test_health_check_returns_200_if_cache_is_stale_and_health_check_configured_correctly(
58-
mocker: MockerFixture,
59-
client: TestClient,
60-
) -> None:
61-
# Given
62-
health_check = HealthCheckSettings(environment_update_grace_period_seconds=None)
63-
mocker.patch("edge_proxy.server.settings.health_check", health_check)
64-
65-
last_updated_at = datetime.now() - timedelta(days=10)
66-
mocked_environment_service = mocker.patch("edge_proxy.server.environment_service")
67-
mocked_environment_service.last_updated_at = last_updated_at
68-
69-
# When
70-
response = client.get("/proxy/health")
71-
72-
# Then
73-
assert response.status_code == 200
74-
assert response.json() == {
75-
"status": "ok",
76-
"reason": None,
77-
"last_successful_update": last_updated_at.isoformat(),
78-
}
79-
80-
8113
def test_get_flags(
8214
mocker: MockerFixture,
8315
environment_1_feature_states_response_list: list[dict],

0 commit comments

Comments
 (0)