diff --git a/docs/usage.md b/docs/usage.md index 6abd3f6..61c5f02 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -178,6 +178,10 @@ async def send_welcome_email(user_id: int, locale: str) -> None: ... ``` +If a listed field is absent from a task's kwargs, it is dropped from the +fingerprint and a warning is logged, since this can make genuinely different +calls collide on the same lock. + ## Opting out per task ```python diff --git a/src/taskiq_deduplication/middleware.py b/src/taskiq_deduplication/middleware.py index 29c8cd5..8ad748f 100644 --- a/src/taskiq_deduplication/middleware.py +++ b/src/taskiq_deduplication/middleware.py @@ -144,11 +144,19 @@ class RedisDeduplicationMiddleware(TaskiqMiddleware): key_fields = parse_list_label( message.labels.get(DEDUP_KEY_FIELDS_LABEL), DEDUP_KEY_FIELDS_LABEL ) - kwargs = ( - {k: v for k, v in message.kwargs.items() if k in key_fields} - if key_fields is not None - else message.kwargs - ) + if key_fields is not None: + missing = [field for field in key_fields if field not in message.kwargs] + if missing: + logger.warning( + "Task %s requested deduplication_key_fields %r but they are " + "absent from kwargs; they are dropped from the fingerprint, which " + "may cause distinct calls to collide.", + message.task_name, + missing, + ) + kwargs = {k: v for k, v in message.kwargs.items() if k in key_fields} + else: + kwargs = message.kwargs try: payload = json.dumps( {"task": message.task_name, "kwargs": kwargs}, diff --git a/tests/test_middleware.py b/tests/test_middleware.py index d8800c4..c214149 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -605,6 +605,33 @@ class TestLabelTypeParsing: assert any("{'a': 1}" in r.message for r in caplog.records) assert key is not None + async def test_missing_key_fields_warns(self, middleware, make_message, caplog): + import logging + + msg = make_message( + kwargs={"order_id": 1}, + labels={DEDUP_KEY_FIELDS_LABEL: ["user_id", "order_id"]}, + ) + with caplog.at_level(logging.WARNING, logger="taskiq_deduplication.middleware"): + key = middleware._build_deduplication_key(msg) + assert any("user_id" in r.message for r in caplog.records) + # the field that is present must not be reported as missing + assert not any("order_id" in r.message for r in caplog.records) + assert key is not None + + async def test_present_key_fields_do_not_warn( + self, middleware, make_message, caplog + ): + import logging + + msg = make_message( + kwargs={"user_id": 1, "order_id": 2}, + labels={DEDUP_KEY_FIELDS_LABEL: ["user_id"]}, + ) + with caplog.at_level(logging.WARNING, logger="taskiq_deduplication.middleware"): + middleware._build_deduplication_key(msg) + assert not any("absent from kwargs" in r.message for r in caplog.records) + def test_stringified_key_fields_parsed_correctly(self, middleware, make_message): # taskiq's prepare_label() stringifies ["a", "b"] → "['a', 'b']" before pre_send m1 = make_message(