Core: Rework model_data to only clear on exit (#5920)

This commit is contained in:
Bert Blommers 2023-02-12 16:58:30 -01:00 committed by GitHub
parent b8350f2801
commit 40a3a529f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 36 additions and 20 deletions

View File

@ -1,18 +1,15 @@
from boto3 import Session from boto3 import Session
import re import re
import string import string
from collections import defaultdict
from functools import lru_cache from functools import lru_cache
from threading import RLock from threading import RLock
from typing import Any, List, Dict, Optional, ClassVar, TypeVar, Iterator from typing import Any, List, Dict, Optional, ClassVar, TypeVar, Iterator
from uuid import uuid4 from uuid import uuid4
from moto.settings import allow_unknown_region from moto.settings import allow_unknown_region
from .model_instances import model_data
from .utils import convert_regex_to_flask_path from .utils import convert_regex_to_flask_path
model_data: Dict[str, Dict[str, object]] = defaultdict(dict)
class InstanceTrackerMeta(type): class InstanceTrackerMeta(type):
def __new__(meta, name: str, bases: Any, dct: Dict[str, Any]) -> type: def __new__(meta, name: str, bases: Any, dct: Dict[str, Any]) -> type:
cls = super(InstanceTrackerMeta, meta).__new__(meta, name, bases, dct) cls = super(InstanceTrackerMeta, meta).__new__(meta, name, bases, dct)
@ -31,16 +28,9 @@ class BaseBackend:
self.region_name = region_name self.region_name = region_name
self.account_id = account_id self.account_id = account_id
def _reset_model_refs(self) -> None:
# Remove all references to the models stored
for models in model_data.values():
for model in models.values():
model.instances = [] # type: ignore[attr-defined]
def reset(self) -> None: def reset(self) -> None:
region_name = self.region_name region_name = self.region_name
account_id = self.account_id account_id = self.account_id
self._reset_model_refs()
self.__dict__ = {} self.__dict__ = {}
self.__init__(region_name, account_id) # type: ignore[misc] self.__init__(region_name, account_id) # type: ignore[misc]

View File

@ -0,0 +1,15 @@
from collections import defaultdict
from typing import Dict
"""
Storage of all instances that extend BaseModel
This allows us to easily expose all internal state using the MotoServer dashboard
"""
model_data: Dict[str, Dict[str, object]] = defaultdict(dict)
def reset_model_data() -> None:
# Remove all references to the models stored
for models in model_data.values():
for model in models.values():
model.instances = [] # type: ignore[attr-defined]

View File

@ -23,6 +23,7 @@ from .custom_responses_mock import (
not_implemented_callback, not_implemented_callback,
reset_responses_mock, reset_responses_mock,
) )
from .model_instances import reset_model_data
DEFAULT_ACCOUNT_ID = "123456789012" DEFAULT_ACCOUNT_ID = "123456789012"
CALLABLE_RETURN = TypeVar("CALLABLE_RETURN") CALLABLE_RETURN = TypeVar("CALLABLE_RETURN")
@ -104,6 +105,7 @@ class BaseMockAWS:
pass pass
self.unmock_env_variables() self.unmock_env_variables()
self.__class__.mocks_active = False self.__class__.mocks_active = False
reset_model_data()
self.disable_patching() # type: ignore[attr-defined] self.disable_patching() # type: ignore[attr-defined]
def decorate_callable( def decorate_callable(

View File

@ -1693,7 +1693,6 @@ class IAMBackend(BaseBackend):
account_id = self.account_id account_id = self.account_id
# Do not reset these policies, as they take a long time to load # Do not reset these policies, as they take a long time to load
aws_policies = self.aws_managed_policies aws_policies = self.aws_managed_policies
self._reset_model_refs()
self.__dict__ = {} self.__dict__ = {}
self.__init__(region_name, account_id, aws_policies) self.__init__(region_name, account_id, aws_policies)

View File

@ -1,4 +1,5 @@
from moto.core import BaseBackend, DEFAULT_ACCOUNT_ID from moto.core import BaseBackend, DEFAULT_ACCOUNT_ID
from moto.core.model_instances import reset_model_data
from typing import Any, Dict from typing import Any, Dict
@ -14,6 +15,7 @@ class MotoAPIBackend(BaseBackend):
continue continue
for backend in backends_.values(): for backend in backends_.values():
backend.reset() backend.reset()
reset_model_data()
self.__init__(region_name, account_id) # type: ignore[misc] self.__init__(region_name, account_id) # type: ignore[misc]
def get_transition(self, model_name: str) -> Dict[str, Any]: def get_transition(self, model_name: str) -> Dict[str, Any]:

View File

@ -9,7 +9,7 @@ from typing import Any, Dict, List
class MotoAPIResponse(BaseResponse): class MotoAPIResponse(BaseResponse):
def reset_response( def reset_response(
self, self,
request: Any, # pylint: disable=unused-argument request: Any,
full_url: str, # pylint: disable=unused-argument full_url: str, # pylint: disable=unused-argument
headers: Any, # pylint: disable=unused-argument headers: Any, # pylint: disable=unused-argument
) -> TYPE_RESPONSE: ) -> TYPE_RESPONSE:
@ -22,7 +22,7 @@ class MotoAPIResponse(BaseResponse):
def reset_auth_response( def reset_auth_response(
self, self,
request: Any, # pylint: disable=unused-argument request: Any,
full_url: str, # pylint: disable=unused-argument full_url: str, # pylint: disable=unused-argument
headers: Any, # pylint: disable=unused-argument headers: Any, # pylint: disable=unused-argument
) -> TYPE_RESPONSE: ) -> TYPE_RESPONSE:
@ -52,7 +52,7 @@ class MotoAPIResponse(BaseResponse):
full_url: str, # pylint: disable=unused-argument full_url: str, # pylint: disable=unused-argument
headers: Any, # pylint: disable=unused-argument headers: Any, # pylint: disable=unused-argument
) -> TYPE_RESPONSE: ) -> TYPE_RESPONSE:
from moto.core.base_backend import model_data from moto.core.model_instances import model_data
results: Dict[str, Dict[str, List[Any]]] = {} results: Dict[str, Dict[str, List[Any]]] = {}
for service in sorted(model_data): for service in sorted(model_data):
@ -86,7 +86,7 @@ class MotoAPIResponse(BaseResponse):
def get_transition( def get_transition(
self, self,
request: Any, # pylint: disable=unused-argument request: Any,
full_url: str, # pylint: disable=unused-argument full_url: str, # pylint: disable=unused-argument
headers: Any, # pylint: disable=unused-argument headers: Any, # pylint: disable=unused-argument
) -> TYPE_RESPONSE: ) -> TYPE_RESPONSE:
@ -103,9 +103,9 @@ class MotoAPIResponse(BaseResponse):
def set_transition( def set_transition(
self, self,
request: Any, # pylint: disable=unused-argument request: Any,
full_url: str, # pylint: disable=unused-argument full_url: str, # pylint: disable=unused-argument
headers: Any, # pylint: disable=unused-argument headers: Any,
) -> TYPE_RESPONSE: ) -> TYPE_RESPONSE:
from .models import moto_api_backend from .models import moto_api_backend
@ -120,9 +120,9 @@ class MotoAPIResponse(BaseResponse):
def unset_transition( def unset_transition(
self, self,
request: Any, # pylint: disable=unused-argument request: Any,
full_url: str, # pylint: disable=unused-argument full_url: str, # pylint: disable=unused-argument
headers: Any, # pylint: disable=unused-argument headers: Any,
) -> TYPE_RESPONSE: ) -> TYPE_RESPONSE:
from .models import moto_api_backend from .models import moto_api_backend

View File

@ -3,6 +3,7 @@ import gc
import warnings import warnings
from functools import wraps from functools import wraps
from moto import settings from moto import settings
from moto.dynamodb.models import DynamoDBBackend
from moto.s3 import models as s3model from moto.s3 import models as s3model
from moto.s3.responses import S3ResponseInstance from moto.s3.responses import S3ResponseInstance
from unittest import SkipTest, TestCase from unittest import SkipTest, TestCase
@ -188,6 +189,13 @@ class TestS3FileHandleClosures(TestCase):
bucket_name="my-bucket", key_name="my-key", version_id=key._version_id bucket_name="my-bucket", key_name="my-key", version_id=key._version_id
) )
@verify_zero_warnings
def test_reset_other_backend(self):
db = DynamoDBBackend("us-west-1", "1234")
# This used to empty the entire list of `model_instances`, which can contain FakeKey-references
# Verify that we can reset an unrelated backend, without throwing away FakeKey-references that still need to be disposed
db.reset()
def test_verify_key_can_be_copied_after_disposing(): def test_verify_key_can_be_copied_after_disposing():
# https://github.com/getmoto/moto/issues/5588 # https://github.com/getmoto/moto/issues/5588