{"record":{"id":"dffccc7283cb1475","repo":"apache/pulsar","slug":"dead-letter-topic-specified-however-max-retries-i","errorCode":null,"errorMessage":"Dead Letter Topic specified, however max retries is set to infinity","messagePattern":"Dead Letter Topic specified, however max retries is set to infinity","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java","lineNumber":876,"sourceCode":"\n        if (functionConfig.getTimeoutMs() != null && functionConfig.getTimeoutMs() <= 0) {\n            throw new IllegalArgumentException(\"Function timeout must be a positive number\");\n        }\n\n        if (functionConfig.getTimeoutMs() != null\n                && functionConfig.getProcessingGuarantees() != null\n                && functionConfig.getProcessingGuarantees() != FunctionConfig.ProcessingGuarantees.ATLEAST_ONCE) {\n            throw new IllegalArgumentException(\"Message timeout can only be specified with processing guarantee is \"\n                    + FunctionConfig.ProcessingGuarantees.ATLEAST_ONCE.name());\n        }\n\n        if (functionConfig.getMaxMessageRetries() != null && functionConfig.getMaxMessageRetries() >= 0\n                && functionConfig.getProcessingGuarantees() == FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE) {\n            throw new IllegalArgumentException(\"MaxMessageRetries and Effectively once don't gel well\");\n        }\n        if ((functionConfig.getMaxMessageRetries() == null || functionConfig.getMaxMessageRetries() < 0)\n                && !org.apache.commons.lang3.StringUtils.isEmpty(functionConfig.getDeadLetterTopic())) {\n            throw new IllegalArgumentException(\"Dead Letter Topic specified, however max retries is set to infinity\");\n        }\n        if (functionConfig.getRetainKeyOrdering() != null\n                && functionConfig.getRetainKeyOrdering()\n                && functionConfig.getProcessingGuarantees() != null\n                && functionConfig.getProcessingGuarantees() == FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE) {\n            throw new IllegalArgumentException(\n                    \"When effectively once processing guarantee is specified, retain Key ordering cannot be set\");\n        }\n        if (functionConfig.getRetainKeyOrdering() != null && functionConfig.getRetainKeyOrdering()\n                && functionConfig.getRetainOrdering() != null && functionConfig.getRetainOrdering()) {\n            throw new IllegalArgumentException(\"Only one of retain ordering or retain key ordering can be set\");\n        }\n\n        if (!isEmpty(functionConfig.getPy()) && !org.apache.pulsar.common.functions.Utils\n                .isFunctionPackageUrlSupported(functionConfig.getPy())\n                && functionConfig.getPy().startsWith(BUILTIN)) {\n            String filename = functionConfig.getPy();\n            if (filename.contains(\"..\")) {","sourceCodeStart":858,"sourceCodeEnd":894,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java#L858-L894","documentation":"A dead-letter topic only receives messages after a finite number of retries; if maxMessageRetries is null or negative (meaning infinite retries), messages would never reach the DLQ, so the combination is rejected. doCommonChecks throws this IllegalArgumentException when a deadLetterTopic is set but max retries is effectively infinite.","triggerScenarios":"createFunction/updateFunction where getDeadLetterTopic() is non-empty and (getMaxMessageRetries() == null || getMaxMessageRetries() < 0) — e.g. --dead-letter-topic set via CLI without --max-message-retries, or maxMessageRetries explicitly -1 in YAML.","commonSituations":"CLI scripts adding a DLQ flag but omitting the retries flag; manifests copied from examples that set deadLetterTopic only; confusion that setting -1 means 'use default' rather than 'infinite'.","solutions":["Set maxMessageRetries to a positive integer when a deadLetterTopic is configured","Or remove the deadLetterTopic if infinite retries are intended","Check config-merging code so setting DLQ always accompanies retries"],"exampleFix":"// before\nconfig.setDeadLetterTopic(\"persistent://public/default/dlq\");\n// after\nconfig.setDeadLetterTopic(\"persistent://public/default/dlq\");\nconfig.setMaxMessageRetries(3);","handlingStrategy":"validation","validationCode":"if (config.getDeadLetterTopic() != null && !config.getDeadLetterTopic().isEmpty()\n        && (config.getMaxMessageRetries() == null || config.getMaxMessageRetries() < 0)) {\n    throw new IllegalArgumentException(\"DLQ requires finite maxMessageRetries\");\n}","typeGuard":"boolean dlqRetriesPairing(FunctionConfig c) {\n    boolean hasDlq = c.getDeadLetterTopic() != null && !c.getDeadLetterTopic().isEmpty();\n    boolean finiteRetries = c.getMaxMessageRetries() != null && c.getMaxMessageRetries() >= 0;\n    return !hasDlq || finiteRetries;\n}","tryCatchPattern":"try {\n    admin.functions().createFunction(functionConfig, sourceConfigLocation);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"max retries is set to infinity\")) {\n        config.setMaxMessageRetries(3); // or remove DLQ\n    } else { throw e; }\n}","preventionTips":["Always set maxMessageRetries together with deadLetterTopic in the same config builder","Treat maxMessageRetries < 0 as 'infinite' — never use it as a default sentinel for DLQ configs","Lint function manifests for DLQ/retry pairing","Keep DLQ setup in one shared helper that enforces the pairing"],"tags":["pulsar-functions","validation","retries","dead-letter"],"backgroundTag":"incompatible-function-options","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}