Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 098d7a1

Browse files
DynamoDB: Add support for WarmThroughput parameters (#13235)
1 parent ed5c787 commit 098d7a1

5 files changed

Lines changed: 657 additions & 91 deletions

File tree

localstack-core/localstack/services/dynamodb/provider.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,9 @@ def create_table(
748748
if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
749749
table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
750750

751+
if "WarmThroughput" in table_description:
752+
table_description["WarmThroughput"]["Status"] = "UPDATING"
753+
751754
tags = table_definitions.pop("Tags", [])
752755
if tags:
753756
get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
@@ -765,6 +768,13 @@ def delete_table(
765768
) -> DeleteTableOutput:
766769
global_table_region = self.get_global_table_region(context, table_name)
767770

771+
self.ensure_table_exists(
772+
context.account_id,
773+
global_table_region,
774+
table_name,
775+
error_message=f"Requested resource not found: Table: {table_name} not found",
776+
)
777+
768778
# Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
769779
# This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
770780

@@ -825,6 +835,9 @@ def describe_table(
825835
table_description["TableClassSummary"] = {
826836
"TableClass": table_definitions["TableClass"]
827837
}
838+
if warm_throughput := table_definitions.get("WarmThroughput"):
839+
table_description["WarmThroughput"] = warm_throughput.copy()
840+
table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
828841

829842
if "GlobalSecondaryIndexes" in table_description:
830843
for gsi in table_description["GlobalSecondaryIndexes"]:
@@ -837,6 +850,17 @@ def describe_table(
837850
# Terraform depends on this parity for update operations
838851
gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
839852

853+
# Set defaults for warm throughput
854+
if "WarmThroughput" not in table_description:
855+
billing_mode = table_definitions.get("BillingMode") if table_definitions else None
856+
table_description["WarmThroughput"] = {
857+
"ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
858+
"WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
859+
}
860+
table_description["WarmThroughput"]["Status"] = (
861+
table_description.get("TableStatus") or "ACTIVE"
862+
)
863+
840864
return DescribeTableOutput(
841865
Table=select_from_typed_dict(TableDescription, table_description)
842866
)

localstack-core/localstack/services/dynamodb/v2/provider.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ def create_table(
560560
if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
561561
table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
562562

563+
if "WarmThroughput" in table_description:
564+
table_description["WarmThroughput"]["Status"] = "UPDATING"
565+
563566
tags = table_definitions.pop("Tags", [])
564567
if tags:
565568
get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
@@ -577,6 +580,13 @@ def delete_table(
577580
) -> DeleteTableOutput:
578581
global_table_region = self.get_global_table_region(context, table_name)
579582

583+
self.ensure_table_exists(
584+
context.account_id,
585+
global_table_region,
586+
table_name,
587+
error_message=f"Requested resource not found: Table: {table_name} not found",
588+
)
589+
580590
# Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
581591
# This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
582592

@@ -636,6 +646,9 @@ def describe_table(
636646
table_description["TableClassSummary"] = {
637647
"TableClass": table_definitions["TableClass"]
638648
}
649+
if warm_throughput := table_definitions.get("WarmThroughput"):
650+
table_description["WarmThroughput"] = warm_throughput.copy()
651+
table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
639652

640653
if "GlobalSecondaryIndexes" in table_description:
641654
for gsi in table_description["GlobalSecondaryIndexes"]:
@@ -648,6 +661,17 @@ def describe_table(
648661
# Terraform depends on this parity for update operations
649662
gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
650663

664+
# Set defaults for warm throughput
665+
if "WarmThroughput" not in table_description:
666+
billing_mode = table_definitions.get("BillingMode") if table_definitions else None
667+
table_description["WarmThroughput"] = {
668+
"ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
669+
"WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
670+
}
671+
table_description["WarmThroughput"]["Status"] = (
672+
table_description.get("TableStatus") or "ACTIVE"
673+
)
674+
651675
return DescribeTableOutput(
652676
Table=select_from_typed_dict(TableDescription, table_description)
653677
)

tests/aws/services/dynamodb/test_dynamodb.py

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from botocore.config import Config
1212
from botocore.exceptions import ClientError
1313
from localstack_snapshot.snapshots.transformer import SortingTransformer
14+
from localstack_snapshot.snapshots.transformer_utility import TransformerUtility
1415

1516
from localstack import config
1617
from localstack.aws.api.dynamodb import (
@@ -1490,32 +1491,101 @@ def test_create_duplicate_table(self, dynamodb_create_table_with_parameters, sna
14901491
)
14911492
snapshot.match("Error", ctx.value)
14921493

1493-
@markers.aws.only_localstack(
1494-
reason="timing issues - needs a check to see if table is successfully deleted"
1494+
@markers.aws.validated
1495+
@markers.snapshot.skip_snapshot_verify(
1496+
paths=[
1497+
"$..TableDescription.TableStatus", # DDBLocal directly goes to ACTIVE status
1498+
"$..Table.ProvisionedThroughput.LastDecreaseDateTime", # Not returned by DDBLocal
1499+
"$..Table.ProvisionedThroughput.LastIncreaseDateTime", # Not returned by DDBLocal
1500+
# The following attributes (prefixed TableDescription) are returned by DDBLocal DeleteTable but not in parity with AWS
1501+
"$..TableDescription.AttributeDefinitions",
1502+
"$..TableDescription.CreationDateTime",
1503+
"$..TableDescription.KeySchema",
1504+
"$..TableDescription.ProvisionedThroughput.LastDecreaseDateTime",
1505+
"$..TableDescription.ProvisionedThroughput.LastIncreaseDateTime",
1506+
"$..TableDescription.TableId",
1507+
]
14951508
)
1496-
def test_delete_table(self, dynamodb_create_table, aws_client):
1509+
def test_table_crud(self, aws_client, cleanups, snapshot, dynamodb_wait_for_table_active):
1510+
snapshot.add_transformer(
1511+
[
1512+
TransformerUtility.key_value("TableName"),
1513+
TransformerUtility.key_value("TableArn"),
1514+
]
1515+
)
1516+
14971517
table_name = f"test-ddb-table-{short_uid()}"
14981518

1499-
tables_before = len(aws_client.dynamodb.list_tables()["TableNames"])
1519+
# CreateTable
1520+
response = aws_client.dynamodb.create_table(
1521+
TableName=table_name,
1522+
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
1523+
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
1524+
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
1525+
)
1526+
cleanups.append(lambda: aws_client.dynamodb.delete_table(TableName=table_name))
1527+
snapshot.match("create-table", response)
1528+
1529+
dynamodb_wait_for_table_active(table_name=table_name)
15001530

1501-
dynamodb_create_table(
1502-
table_name=table_name,
1503-
partition_key=PARTITION_KEY,
1531+
# ListTables
1532+
assert table_name in aws_client.dynamodb.list_tables()["TableNames"]
1533+
1534+
# DescribeTable
1535+
response = aws_client.dynamodb.describe_table(TableName=table_name)
1536+
snapshot.match("describe-table", response)
1537+
1538+
# DeleteTable
1539+
response = aws_client.dynamodb.delete_table(TableName=table_name)
1540+
snapshot.match("delete-table", response)
1541+
1542+
# ListTable: after DeleteTable
1543+
retry(
1544+
lambda: table_name not in aws_client.dynamodb.list_tables()["TableNames"],
1545+
sleep=1,
1546+
retries=30,
15041547
)
15051548

1506-
table_list = aws_client.dynamodb.list_tables()
1507-
# TODO: fix assertion, to enable parallel test execution!
1508-
assert tables_before + 1 == len(table_list["TableNames"])
1509-
assert table_name in table_list["TableNames"]
1549+
# DeleteTable: delete non-existent table
1550+
with pytest.raises(ClientError) as exc:
1551+
aws_client.dynamodb.delete_table(TableName="non-existent")
1552+
snapshot.match("delete-non-existent-table", exc.value.response)
15101553

1511-
aws_client.dynamodb.delete_table(TableName=table_name)
1554+
@markers.aws.validated
1555+
@markers.snapshot.skip_snapshot_verify(
1556+
paths=[
1557+
"$..TableDescription.TableStatus", # DDBLocal directly goes to ACTIVE status
1558+
"$..Table.ProvisionedThroughput.LastDecreaseDateTime", # Not returned by DDBLocal
1559+
"$..Table.ProvisionedThroughput.LastIncreaseDateTime", # Not returned by DDBLocal
1560+
]
1561+
)
1562+
def test_table_warm_throughput(
1563+
self, dynamodb_create_table_with_parameters, snapshot, aws_client
1564+
):
1565+
"""
1566+
This test ensures that WarmThroughput params provided to CreateTable are reflected in DescribeTable
1567+
"""
15121568

1513-
table_list = aws_client.dynamodb.list_tables()
1514-
assert tables_before == len(table_list["TableNames"])
1569+
snapshot.add_transformer(
1570+
[
1571+
TransformerUtility.key_value("TableName"),
1572+
TransformerUtility.key_value("TableArn"),
1573+
]
1574+
)
15151575

1516-
with pytest.raises(Exception) as ctx:
1517-
aws_client.dynamodb.delete_table(TableName=table_name)
1518-
assert ctx.match("ResourceNotFoundException")
1576+
table_name = f"test-ddb-table-{short_uid()}"
1577+
1578+
response = dynamodb_create_table_with_parameters(
1579+
TableName=table_name,
1580+
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
1581+
AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}],
1582+
ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
1583+
WarmThroughput={"ReadUnitsPerSecond": 1000, "WriteUnitsPerSecond": 1200},
1584+
)
1585+
snapshot.match("create-table", response)
1586+
1587+
response = aws_client.dynamodb.describe_table(TableName=table_name)
1588+
snapshot.match("describe-table", response)
15191589

15201590
@markers.aws.validated
15211591
def test_transaction_write_items(
@@ -1947,9 +2017,14 @@ def test_dynamodb_create_table_with_sse_specification(
19472017
assert result["TableDescription"]["SSEDescription"]["KMSMasterKeyArn"] == kms_master_key_arn
19482018

19492019
@markers.aws.validated
2020+
@markers.snapshot.skip_snapshot_verify(
2021+
paths=["$..KeyMetadata.CurrentKeyMaterialId"] # Not supported by LS
2022+
)
19502023
def test_dynamodb_create_table_with_partial_sse_specification(
19512024
self, dynamodb_create_table_with_parameters, snapshot, aws_client
19522025
):
2026+
snapshot.add_transformer(TransformerUtility.key_value("CurrentKeyMaterialId"))
2027+
19532028
table_name = f"test_table_{short_uid()}"
19542029
sse_specification = {"Enabled": True}
19552030

@@ -1979,9 +2054,14 @@ def test_dynamodb_create_table_with_partial_sse_specification(
19792054
assert "SSESpecification" not in result["Table"]
19802055

19812056
@markers.aws.validated
2057+
@markers.snapshot.skip_snapshot_verify(
2058+
paths=["$..KeyMetadata.CurrentKeyMaterialId"] # Not supported by LS
2059+
)
19822060
def test_dynamodb_update_table_without_sse_specification_change(
19832061
self, dynamodb_create_table_with_parameters, snapshot, aws_client
19842062
):
2063+
snapshot.add_transformer(TransformerUtility.key_value("CurrentKeyMaterialId"))
2064+
19852065
table_name = f"test_table_{short_uid()}"
19862066

19872067
sse_specification = {"Enabled": True}
@@ -2284,6 +2364,7 @@ def test_data_encoding_consistency(
22842364
paths=[
22852365
"$..PointInTimeRecoveryDescription..EarliestRestorableDateTime",
22862366
"$..PointInTimeRecoveryDescription..LatestRestorableDateTime",
2367+
"$..ContinuousBackupsDescription.PointInTimeRecoveryDescription.RecoveryPeriodInDays",
22872368
]
22882369
)
22892370
@markers.aws.validated
@@ -2638,6 +2719,8 @@ def _get_records_amount(record_amount: int):
26382719
@pytest.mark.parametrize("billing_mode", ["PAY_PER_REQUEST", "PROVISIONED"])
26392720
@markers.snapshot.skip_snapshot_verify(
26402721
paths=[
2722+
# Warm throughput for GSI is not implemented in LS. DDB Local doesn't support it either.
2723+
"$..Table.GlobalSecondaryIndexes..WarmThroughput",
26412724
# LS returns those and not AWS, probably because no changes happened there yet
26422725
"$..ProvisionedThroughput.LastDecreaseDateTime",
26432726
"$..ProvisionedThroughput.LastIncreaseDateTime",

0 commit comments

Comments
 (0)