Repository: localstack/localstack

Issue:
bug: Index not found temporarily returned after update table operations
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

As part of our test suite, we setup DynamoDB tables starting from initial table creation to a set of updates we've made over time. The tables are created and updates are applied successfully, so we begin to run our tests but when our tests run quickly, we end up getting an `Index not found` error returned when searching against a global secondary index that was created as part of an update table operation.

### Expected Behavior

The expected behavior should be that we should be able to query against the updated secondary index that has been created in a table.

### How are you starting LocalStack?

With a `docker run` command

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    docker run localstack/localstack

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

We have an internal set of migrations we run against DynamoDB that get the tables to the state that we expect. The main issue pops up when we do a replication update as part of a table update operation and then do the global secondary key creation. When doing the replication update, the schema gets cached internally and a bad version of the schema stays valid until the initial TTL expires (20s default).

### Environment

```markdown
- OS: Mac OS Ventura (Docker 4.23.0)
- LocalStack: latest (also tried 2.2.0, and some older versions)
```


### Anything else?

_No response_


Discussion:

Resolving patch:
diff --git a/localstack/services/dynamodb/provider.py b/localstack/services/dynamodb/provider.py
--- a/localstack/services/dynamodb/provider.py
+++ b/localstack/services/dynamodb/provider.py
@@ -657,11 +657,17 @@ def update_table(
                 store.REPLICAS[table_name] = replicas
 
             # update response content
+            SchemaExtractor.invalidate_table_schema(
+                table_name, context.account_id, global_table_region
+            )
+
             schema = SchemaExtractor.get_table_schema(
                 table_name, context.account_id, global_table_region
             )
             return UpdateTableOutput(TableDescription=schema["Table"])
 
+        SchemaExtractor.invalidate_table_schema(table_name, context.account_id, global_table_region)
+
         # TODO: DDB streams must also be created for replicas
         if update_table_input.get("StreamSpecification"):
             create_dynamodb_stream(
diff --git a/localstack/services/dynamodb/utils.py b/localstack/services/dynamodb/utils.py
--- a/localstack/services/dynamodb/utils.py
+++ b/localstack/services/dynamodb/utils.py
@@ -114,6 +114,16 @@ def get_table_schema(cls, table_name: str, account_id: str = None, region_name:
                 raise
         return schema
 
+    @classmethod
+    def invalidate_table_schema(cls, table_name: str, account_id: str, region_name: str):
+        """
+        Allow cached table schemas to be invalidated without waiting for the TTL to expire
+        """
+        key = dynamodb_table_arn(
+            table_name=table_name, account_id=account_id, region_name=region_name
+        )
+        SCHEMA_CACHE.pop(key, None)
+
 
 class ItemFinder:
     @staticmethod


Test patch:
diff --git a/tests/unit/test_dynamodb.py b/tests/unit/test_dynamodb.py
--- a/tests/unit/test_dynamodb.py
+++ b/tests/unit/test_dynamodb.py
@@ -3,9 +3,16 @@
 import pytest
 
 from localstack.aws.accounts import get_aws_account_id
+from localstack.constants import TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME
 from localstack.services.dynamodb.provider import DynamoDBProvider, get_store
-from localstack.services.dynamodb.utils import ItemSet, SchemaExtractor, dynamize_value
+from localstack.services.dynamodb.utils import (
+    SCHEMA_CACHE,
+    ItemSet,
+    SchemaExtractor,
+    dynamize_value,
+)
 from localstack.utils.aws import aws_stack
+from localstack.utils.aws.arns import dynamodb_table_arn
 
 
 def test_fix_region_in_headers():
@@ -79,6 +86,37 @@ def test_get_key_schema_without_table_definition(mock_get_table_schema):
     )
 
 
+def test_invalidate_table_schema():
+    schema_extractor = SchemaExtractor()
+
+    key_schema = [{"AttributeName": "id", "KeyType": "HASH"}]
+    attr_definitions = [
+        {"AttributeName": "Artist", "AttributeType": "S"},
+        {"AttributeName": "SongTitle", "AttributeType": "S"},
+    ]
+    table_name = "nonexistent_table"
+
+    key = dynamodb_table_arn(
+        table_name=table_name, account_id=TEST_AWS_ACCOUNT_ID, region_name=TEST_AWS_REGION_NAME
+    )
+
+    table_schema = {"Table": {"KeySchema": key_schema, "AttributeDefinitions": attr_definitions}}
+    # This isn't great but we need to inject into the cache here so that we're not trying to hit dynamodb
+    # to look up the table later on
+    SCHEMA_CACHE[key] = table_schema
+
+    schema = schema_extractor.get_table_schema(
+        table_name, account_id=TEST_AWS_ACCOUNT_ID, region_name=TEST_AWS_REGION_NAME
+    )
+
+    # Assert output is expected from the get_table_schema (fallback)
+    assert schema == table_schema
+    # Invalidate the cache now for the table
+    schema_extractor.invalidate_table_schema(table_name, TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME)
+    # Assert that the key is now set to None
+    assert SCHEMA_CACHE.get(key) is None
+
+
 @pytest.mark.parametrize(
     "value, result",
     [