{"record":{"id":"e23afe765031bfd6","repo":"apache/pulsar","slug":"maxmessageretries-and-effectively-once-don-t-gel-w","errorCode":null,"errorMessage":"MaxMessageRetries and Effectively once don't gel well","messagePattern":"MaxMessageRetries and Effectively once don't gel well","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java","lineNumber":872,"sourceCode":"\n        if (functionConfig.getResources() != null) {\n            ResourceConfigUtils.validate(functionConfig.getResources());\n        }\n\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","sourceCodeStart":854,"sourceCodeEnd":890,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-functions/utils/src/main/java/org/apache/pulsar/functions/utils/FunctionConfigUtils.java#L854-L890","documentation":"maxMessageRetries with a dead-letter topic implements retry-based redelivery, which is incompatible with EFFECTIVELY_ONCE guarantees (retries would violate exactly-once semantics). doCommonChecks throws this IllegalArgumentException when maxMessageRetries >= 0 is combined with ProcessingGuarantees.EFFECTIVELY_ONCE.","triggerScenarios":"createFunction/updateFunction where functionConfig.getMaxMessageRetries() != null && >= 0 and getProcessingGuarantees() == EFFECTIVELY_ONCE — e.g. adding --max-message-retries 3 to an effectively-once function.","commonSituations":"Reusing a config template between at-least-once and exactly-once functions; enabling retry features globally in generated manifests without checking the guarantee mode.","solutions":["Drop maxMessageRetries (set to null) when using EFFECTIVELY_ONCE","Or switch processingGuarantees to ATLEAST_ONCE if retry semantics are required","Parameterize retry settings per guarantee mode in config generation code"],"exampleFix":"// before\nconfig.setProcessingGuarantees(FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE);\nconfig.setMaxMessageRetries(5);\n// after\nconfig.setProcessingGuarantees(FunctionConfig.ProcessingGuarantees.ATLEAST_ONCE);\nconfig.setMaxMessageRetries(5);","handlingStrategy":"validation","validationCode":"if (config.getMaxMessageRetries() != null && config.getMaxMessageRetries() >= 0\n        && config.getProcessingGuarantees() == FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE) {\n    throw new IllegalArgumentException(\"retries are incompatible with EFFECTIVELY_ONCE\");\n}","typeGuard":"boolean retriesGuaranteeCompatible(FunctionConfig c) {\n    return c.getProcessingGuarantees() != FunctionConfig.ProcessingGuarantees.EFFECTIVELY_ONCE\n        || c.getMaxMessageRetries() == null || c.getMaxMessageRetries() < 0;\n}","tryCatchPattern":"try {\n    admin.functions().createFunction(functionConfig, sourceConfigLocation);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"MaxMessageRetries\")) {\n        config.setMaxMessageRetries(null); // or drop EFFECTIVELY_ONCE\n    } else { throw e; }\n}","preventionTips":["Keep retry settings only on ATLEAST_ONCE functions","Template separate config profiles per guarantee mode","Validate retry/guarantee pairing in CI before deploying","Avoid global 'enable retries everywhere' flags"],"tags":["pulsar-functions","validation","retries","processing-guarantees"],"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"}