3838 DynamoEnvironmentV2Wrapper ,
3939 DynamoEnvironmentWrapper ,
4040)
41+ from environments .enums import EnvironmentDocumentCacheMode
4142from environments .exceptions import EnvironmentHeaderNotPresentError
4243from environments .managers import EnvironmentManager
44+ from environments .metrics import (
45+ CACHE_HIT ,
46+ CACHE_MISS ,
47+ flagsmith_environment_document_cache_queries_total ,
48+ )
4349from features .models import Feature , FeatureSegment , FeatureState
4450from features .multivariate .models import MultivariateFeatureStateValue
4551from metadata .models import Metadata
4652from projects .models import Project
4753from segments .models import Segment
48- from util .mappers import map_environment_to_sdk_document
54+ from util .mappers import (
55+ map_environment_to_environment_document ,
56+ map_environment_to_sdk_document ,
57+ )
4958from webhooks .models import AbstractBaseExportableWebhookModel
5059
5160logger = logging .getLogger (__name__ )
5261
5362environment_cache = caches [settings .ENVIRONMENT_CACHE_NAME ]
54- environment_document_cache = caches [settings .ENVIRONMENT_DOCUMENT_CACHE_LOCATION ]
63+ environment_document_cache = caches [settings .CACHE_ENVIRONMENT_DOCUMENT_LOCATION ]
5564environment_segments_cache = caches [settings .ENVIRONMENT_SEGMENTS_CACHE_NAME ]
5665bad_environments_cache = caches [settings .BAD_ENVIRONMENTS_CACHE_LOCATION ]
5766
@@ -149,22 +158,36 @@ class Environment(
149158 class Meta :
150159 ordering = ["id" ]
151160
152- @hook (AFTER_CREATE )
153- def create_feature_states (self ): # type: ignore[no-untyped-def]
161+ @hook (AFTER_CREATE ) # type: ignore[misc]
162+ def create_feature_states (self ) -> None :
154163 FeatureState .create_initial_feature_states_for_environment (environment = self )
155164
156- @hook (AFTER_UPDATE )
157- def clear_environment_cache (self ): # type: ignore[no-untyped-def]
165+ @hook (AFTER_UPDATE ) # type: ignore[misc]
166+ def clear_environment_cache (self ) -> None :
158167 # TODO: this could rebuild the cache itself (using an async task)
159168 environment_cache .delete (self .initial_value ("api_key" ))
160169
161- @hook (AFTER_DELETE )
162- def delete_from_dynamo (self ): # type: ignore[no-untyped-def]
170+ @hook (AFTER_UPDATE , when = "api_key" , has_changed = True ) # type: ignore[misc]
171+ def update_environment_document_cache (self ) -> None :
172+ environment_document_cache .delete (self .initial_value ("api_key" ))
173+ self .write_environment_documents (self .id )
174+
175+ @hook (AFTER_DELETE ) # type: ignore[misc]
176+ def delete_from_dynamo (self ) -> None :
163177 if self .project .enable_dynamo_db and environment_wrapper .is_enabled :
164178 from environments .tasks import delete_environment_from_dynamo
165179
166180 delete_environment_from_dynamo .delay (args = (self .api_key , self .id ))
167181
182+ @hook (AFTER_DELETE ) # type: ignore[misc]
183+ def delete_environment_document_from_cache (self ) -> None :
184+ if (
185+ settings .CACHE_ENVIRONMENT_DOCUMENT_MODE
186+ == EnvironmentDocumentCacheMode .PERSISTENT
187+ or settings .CACHE_ENVIRONMENT_DOCUMENT_SECONDS > 0
188+ ):
189+ environment_document_cache .delete (self .api_key )
190+
168191 def __str__ (self ): # type: ignore[no-untyped-def]
169192 return "Project %s - Environment %s" % (self .project .name , self .name )
170193
@@ -245,7 +268,7 @@ def get_from_cache(cls, api_key): # type: ignore[no-untyped-def]
245268 logger .info ("Environment with api_key %s does not exist" % api_key )
246269
247270 @classmethod
248- def write_environments_to_dynamodb (
271+ def write_environment_documents (
249272 cls ,
250273 environment_id : int = None , # type: ignore[assignment]
251274 project_id : int = None , # type: ignore[assignment]
@@ -281,17 +304,31 @@ def write_environments_to_dynamodb(
281304 # project (which should always be the case). Since we're working with fairly
282305 # small querysets here, this shouldn't have a noticeable impact on performance.
283306 project : Project | None = getattr (environments [0 ], "project" , None )
284- for environment in environments [1 :]:
285- if not environment .project == project :
286- raise RuntimeError ("Environments must all belong to the same project." )
287-
288- if not all ([project , project .enable_dynamo_db , environment_wrapper .is_enabled ]): # type: ignore[union-attr]
307+ if project is None : # pragma: no cover
289308 return
290309
291- environment_wrapper .write_environments (environments )
310+ for environment in environments [1 :]:
311+ if not environment .project == project : # pragma: no cover
312+ raise RuntimeError ("Environments must all belong to the same project." )
292313
293- if project .edge_v2_environments_migrated and environment_v2_wrapper .is_enabled : # type: ignore[union-attr]
294- environment_v2_wrapper .write_environments (environments )
314+ if project .enable_dynamo_db and environment_wrapper .is_enabled :
315+ environment_wrapper .write_environments (environments )
316+
317+ if (
318+ project .edge_v2_environments_migrated
319+ and environment_v2_wrapper .is_enabled
320+ ):
321+ environment_v2_wrapper .write_environments (environments )
322+ elif (
323+ settings .CACHE_ENVIRONMENT_DOCUMENT_MODE
324+ == EnvironmentDocumentCacheMode .PERSISTENT
325+ ):
326+ environment_document_cache .set_many (
327+ {
328+ e .api_key : map_environment_to_environment_document (e )
329+ for e in environments
330+ }
331+ )
295332
296333 def get_feature_state (
297334 self ,
@@ -364,7 +401,11 @@ def get_environment_document(
364401 cls ,
365402 api_key : str ,
366403 ) -> dict [str , typing .Any ]:
367- if settings .CACHE_ENVIRONMENT_DOCUMENT_SECONDS > 0 :
404+ if (
405+ settings .CACHE_ENVIRONMENT_DOCUMENT_SECONDS > 0
406+ or settings .CACHE_ENVIRONMENT_DOCUMENT_MODE
407+ == EnvironmentDocumentCacheMode .PERSISTENT
408+ ):
368409 return cls ._get_environment_document_from_cache (api_key )
369410 return cls ._get_environment_document_from_db (api_key )
370411
@@ -386,9 +427,14 @@ def _get_environment_document_from_cache(
386427 api_key : str ,
387428 ) -> dict [str , typing .Any ]:
388429 environment_document = environment_document_cache .get (api_key )
389- if not environment_document :
430+ if not ( cache_hit := environment_document is not None ) :
390431 environment_document = cls ._get_environment_document_from_db (api_key )
391432 environment_document_cache .set (api_key , environment_document )
433+
434+ flagsmith_environment_document_cache_queries_total .labels (
435+ result = CACHE_HIT if cache_hit else CACHE_MISS ,
436+ ).inc ()
437+
392438 return environment_document # type: ignore[no-any-return]
393439
394440 @classmethod
0 commit comments