Skip to content

Commit e07d182

Browse files
Adopt orjson (#76)
A good chunk of the compute done by the Edge Proxy is JSON marshalling and unmarshalling, and having a more performant library has an impact. Todoist's production data shows an approximate: - Reduction in average CPU usage of 1% - Reduction in p99 latency of 5% - Increased throughput by ~8% With recent improvements, an ECS instance with 4 vCPU is able to handle 3000 requests per minute without noticeable variation to p99 latency.
1 parent 7f86fb5 commit e07d182

File tree

5 files changed

+27
-16
lines changed

5 files changed

+27
-16
lines changed

requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ flagsmith-flag-engine
66
python-decouple
77
python-dotenv
88
pydantic
9+
orjson
910
# sse-stuff
1011
sse-starlette
1112
asyncio

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ idna==3.4
2828
# requests
2929
marshmallow==3.20.1
3030
# via -r requirements.in
31+
orjson==3.9.7
32+
# via -r requirements.in
3133
packaging==23.1
3234
# via marshmallow
3335
pydantic==1.10.12

src/cache.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from datetime import datetime
33

4+
import orjson
45
import requests
56

67
from .exceptions import FlagsmithUnknownKeyError
@@ -22,7 +23,7 @@ def fetch_document(self, server_side_key):
2223
url, headers={"X-Environment-Key": server_side_key}
2324
)
2425
response.raise_for_status()
25-
return response.json()
26+
return orjson.loads(response.text)
2627

2728
def refresh(self):
2829
received_error = False
@@ -31,10 +32,10 @@ def refresh(self):
3132
self._cache[key_pair.client_side_key] = self.fetch_document(
3233
key_pair.server_side_key
3334
)
34-
except requests.exceptions.HTTPError:
35+
except (requests.exceptions.HTTPError, orjson.JSONDecodeError):
3536
received_error = True
3637
logger.exception(
37-
f"Received non 200 response for {key_pair.client_side_key}"
38+
f"Failed to fetch document for {key_pair.client_side_key}"
3839
)
3940
if not received_error:
4041
self.last_updated_at = datetime.now()

src/main.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from fastapi import FastAPI, Header
55
from fastapi.middleware.cors import CORSMiddleware
6-
from fastapi.responses import JSONResponse
6+
from fastapi.responses import ORJSONResponse
77
from flag_engine.engine import (
88
get_environment_feature_state,
99
get_environment_feature_states,
@@ -33,7 +33,7 @@
3333

3434
@app.exception_handler(FlagsmithUnknownKeyError)
3535
async def unknown_key_error(request, exc):
36-
return JSONResponse(
36+
return ORJSONResponse(
3737
status_code=401,
3838
content={
3939
"status": "unauthorized",
@@ -42,19 +42,19 @@ async def unknown_key_error(request, exc):
4242
)
4343

4444

45-
@app.get("/health", deprecated=True)
46-
@app.get("/proxy/health")
45+
@app.get("/health", response_class=ORJSONResponse, deprecated=True)
46+
@app.get("/proxy/health", response_class=ORJSONResponse)
4747
def health_check():
4848
with suppress(TypeError):
4949
last_updated = datetime.now() - cache_service.last_updated_at
5050
buffer = 30 * len(settings.environment_key_pairs) # 30s per environment
5151
if last_updated.total_seconds() <= settings.api_poll_frequency + buffer:
52-
return JSONResponse(status_code=200, content={"status": "ok"})
52+
return ORJSONResponse(status_code=200, content={"status": "ok"})
5353

54-
return JSONResponse(status_code=500, content={"status": "error"})
54+
return ORJSONResponse(status_code=500, content={"status": "error"})
5555

5656

57-
@app.get("/api/v1/flags/")
57+
@app.get("/api/v1/flags/", response_class=ORJSONResponse)
5858
def flags(feature: str = None, x_environment_key: str = Header(None)):
5959
environment_document = cache_service.get_environment(x_environment_key)
6060
environment = build_environment_model(environment_document)
@@ -66,7 +66,7 @@ def flags(feature: str = None, x_environment_key: str = Header(None)):
6666
feature_states=[feature_state],
6767
environment=environment,
6868
):
69-
return JSONResponse(
69+
return ORJSONResponse(
7070
status_code=404,
7171
content={
7272
"status": "not_found",
@@ -83,10 +83,10 @@ def flags(feature: str = None, x_environment_key: str = Header(None)):
8383
)
8484
data = map_feature_states_to_response_data(feature_states)
8585

86-
return JSONResponse(data)
86+
return ORJSONResponse(data)
8787

8888

89-
@app.post("/api/v1/identities/")
89+
@app.post("/api/v1/identities/", response_class=ORJSONResponse)
9090
def identity(
9191
input_data: IdentityWithTraits,
9292
x_environment_key: str = Header(None),
@@ -112,7 +112,7 @@ def identity(
112112
identity_hash_key=identity.composite_key,
113113
),
114114
}
115-
return JSONResponse(data)
115+
return ORJSONResponse(data)
116116

117117

118118
@app.on_event("startup")

tests/test_cache.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import unittest.mock
2+
13
import pytest
24
import requests
35

@@ -16,14 +18,19 @@
1618

1719
def test_refresh_makes_correct_http_call(mocker):
1820
# Given
19-
mocked_session = mocker.patch("src.cache.requests.Session")
21+
mocked_get = mocker.patch("src.cache.requests.Session.get")
22+
mocked_get.side_effect = [
23+
unittest.mock.AsyncMock(text='{"key1": "value1"}'),
24+
unittest.mock.AsyncMock(text='{"key2": "value2"}'),
25+
]
2026
mocked_datetime = mocker.patch("src.cache.datetime")
2127
cache_service = CacheService(settings)
2228

2329
# When
2430
cache_service.refresh()
31+
2532
# Then
26-
mocked_session.return_value.get.assert_has_calls(
33+
mocked_get.assert_has_calls(
2734
[
2835
mocker.call(
2936
f"{settings.api_url}/environment-document/",

0 commit comments

Comments
 (0)