Skip to content

Commit 2a005cf

Browse files
committed
Merge branch 'refs/heads/main' into feat/serve-env-doc
2 parents 2a47f01 + f26b481 commit 2a005cf

11 files changed

Lines changed: 98 additions & 211 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: v0.11.2
3+
rev: v0.11.6
44
hooks:
55
- id: ruff
66
- id: ruff-format

Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.12-slim as application
1+
FROM python:3.12-slim AS application
22

33
WORKDIR /app
44

@@ -10,7 +10,6 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
1010
COPY src /app/src
1111
COPY requirements.lock pyproject.toml /app/
1212
RUN pip install --no-cache-dir -r requirements.lock
13-
RUN edge-proxy-render-config
1413

1514
EXPOSE 8000
1615

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ dev-dependencies = [
3636
[tool.ruff]
3737
line-length = 88
3838

39-
[tool.ruff.lint.isort]
40-
known-first-party = ['fastapi_utils']
41-
4239
[tool.ruff.lint.mccabe]
4340
max-complexity = 10
4441

src/edge_proxy/environments.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from typing import Any, Optional
22
from datetime import datetime
3+
from email.utils import formatdate
34
from functools import lru_cache
45

56
import httpx
7+
import starlette.status
68
import structlog
79
from flag_engine.engine import (
810
get_environment_feature_state,
@@ -22,7 +24,7 @@
2224
map_traits_to_response_data,
2325
)
2426
from edge_proxy.models import IdentityWithTraits
25-
from edge_proxy.settings import AppSettings
27+
from edge_proxy.settings import AppSettings, EnvironmentKeyPair
2628

2729
logger = structlog.get_logger(__name__)
2830

@@ -58,9 +60,7 @@ async def refresh_environment_caches(self):
5860
received_error = False
5961
for key_pair in self.settings.environment_key_pairs:
6062
try:
61-
environment_document = await self._fetch_document(
62-
key_pair.server_side_key
63-
)
63+
environment_document = await self._fetch_document(key_pair)
6464
if self.cache.put_environment(
6565
environment_api_key=key_pair.client_side_key,
6666
environment_document=environment_document,
@@ -156,11 +156,39 @@ def get_environment(
156156
return environment_document
157157
raise FlagsmithUnknownKeyError(server_side_key)
158158

159-
async def _fetch_document(self, server_side_key: str) -> dict[str, Any]:
159+
async def _fetch_document(self, key_pair: EnvironmentKeyPair) -> dict[str, Any]:
160+
headers = {
161+
"X-Environment-Key": key_pair.server_side_key,
162+
}
163+
environment_document = self.cache.get_environment(
164+
environment_api_key=key_pair.client_side_key
165+
)
166+
if environment_document:
167+
updated_at: str = environment_document.get("updated_at")
168+
if updated_at:
169+
try:
170+
epoch_seconds = datetime.fromisoformat(updated_at).timestamp()
171+
# Same implementation as https://docs.djangoproject.com/en/4.2/ref/utils/#django.utils.http.http_date
172+
headers["If-Modified-Since"] = formatdate(
173+
epoch_seconds, usegmt=True
174+
)
175+
except ValueError:
176+
logger.warning(
177+
f"failed to parse updated_at, environment={key_pair.client_side_key} updated_at={updated_at}"
178+
)
179+
else:
180+
logger.warning(
181+
f"received environment with no updated_at: {key_pair.client_side_key}"
182+
)
160183
response = await self._client.get(
161184
url=f"{self.settings.api_url}/environment-document/",
162-
headers={"X-Environment-Key": server_side_key},
185+
headers=headers,
163186
)
187+
if response.status_code == starlette.status.HTTP_304_NOT_MODIFIED:
188+
assert environment_document, (
189+
f"GET /environment-document returned 304 without a cached document. environment={key_pair.client_side_key}"
190+
)
191+
return environment_document
164192
response.raise_for_status()
165193
return orjson.loads(response.text)
166194

src/edge_proxy/server.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from datetime import datetime, timedelta
2+
import asyncio
23

34
import httpx
4-
import structlog
5+
from contextlib import asynccontextmanager
56
from fastapi import FastAPI, Header
67
from fastapi.middleware.cors import CORSMiddleware
78
from fastapi.middleware.gzip import GZipMiddleware
89
from fastapi.responses import ORJSONResponse, Response
910

1011
from edge_proxy.health_check.responses import HealthCheckResponse
11-
from fastapi_utils.tasks import repeat_every
1212

1313
from edge_proxy.cache import LocalMemEnvironmentsCache
1414
from edge_proxy.environments import EnvironmentService
@@ -24,7 +24,23 @@
2424
httpx.AsyncClient(timeout=settings.api_poll_timeout_seconds),
2525
settings,
2626
)
27-
app = FastAPI()
27+
28+
29+
async def poll_environments():
30+
while True:
31+
await environment_service.refresh_environment_caches()
32+
await asyncio.sleep(settings.api_poll_frequency_seconds)
33+
34+
35+
@asynccontextmanager
36+
async def lifespan(app: FastAPI):
37+
await environment_service.refresh_environment_caches()
38+
poll = asyncio.create_task(poll_environments())
39+
yield
40+
poll.cancel()
41+
42+
43+
app = FastAPI(lifespan=lifespan)
2844

2945

3046
@app.exception_handler(FlagsmithUnknownKeyError)
@@ -120,16 +136,6 @@ async def environment_document(
120136
return ORJSONResponse(status_code=401, content=None)
121137

122138

123-
@app.on_event("startup")
124-
@repeat_every(
125-
seconds=settings.api_poll_frequency_seconds,
126-
raise_exceptions=True,
127-
logger=structlog.get_logger(__name__),
128-
)
129-
async def refresh_cache():
130-
await environment_service.refresh_environment_caches()
131-
132-
133139
app.add_middleware(
134140
CORSMiddleware,
135141
allow_origins=settings.allow_origins,

src/edge_proxy/settings.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ def json_config_settings_source() -> dict[str, Any]:
5959
at the project's root.
6060
"""
6161
encoding = "utf-8"
62-
return json.loads(Path(CONFIG_PATH).read_text(encoding))
62+
try:
63+
config = json.loads(Path(CONFIG_PATH).read_text(encoding))
64+
logger.info(f"Loaded configuration from {CONFIG_PATH}")
65+
return config
66+
except FileNotFoundError:
67+
logger.info(f"Configuration file at {CONFIG_PATH} not found")
68+
return {}
6369

6470

6571
class EnvironmentKeyPair(BaseModel):

src/fastapi_utils/LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/fastapi_utils/tasks.py

Lines changed: 0 additions & 77 deletions
This file was deleted.

src/fastapi_utils/test_tasks.py

Lines changed: 0 additions & 87 deletions
This file was deleted.

tests/fixtures/response_data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191

9292

9393
environment_1 = {
94+
"updated_at": "1969-07-20T20:17:40Z",
9495
"feature_states": [
9596
_environment_feature_state_1,
9697
_environment_feature_state_2,

0 commit comments

Comments
 (0)