{"id":"147bad3529dafffa","repo":"apache/kafka","slug":"circular-dependency-in-sensors-name-is-its-own","errorCode":null,"errorMessage":"Circular dependency in sensors: {name} is its own parent.","messagePattern":"Circular dependency in sensors: (.+?) is its own parent\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/common/metrics/Sensor.java","lineNumber":159,"sourceCode":"        super();\n        this.registry = registry;\n        this.name = Objects.requireNonNull(name);\n        this.parents = parents == null ? new Sensor[0] : parents;\n        this.metrics = new LinkedHashMap<>();\n        this.stats = new ArrayList<>();\n        this.config = config;\n        this.time = time;\n        this.inactiveSensorExpirationTimeMs = TimeUnit.MILLISECONDS.convert(inactiveSensorExpirationTimeSeconds, TimeUnit.SECONDS);\n        this.lastRecordTime = time.milliseconds();\n        this.recordingLevel = recordingLevel;\n        this.metricLock = new Object();\n        checkForest(new HashSet<>());\n    }\n\n    /* Validate that this sensor doesn't end up referencing itself */\n    private void checkForest(Set<Sensor> sensors) {\n        if (!sensors.add(this))\n            throw new IllegalArgumentException(\"Circular dependency in sensors: \" + name() + \" is its own parent.\");\n        for (Sensor parent : parents)\n            parent.checkForest(sensors);\n    }\n\n    /**\n     * The name this sensor is registered with. This name will be unique among all registered sensors.\n     */\n    public String name() {\n        return this.name;\n    }\n\n    List<Sensor> parents() {\n        return unmodifiableList(asList(parents));\n    }\n\n    /**\n     * @return true if the sensor's record level indicates that the metric will be recorded, false otherwise\n     */","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/common/metrics/Sensor.java#L141-L177","documentation":"Thrown by the Sensor constructor's checkForest(Set) when the set of parent sensors already contains the sensor being added, i.e. the parent graph is cyclic. Kafka models sensors as a forest (DAG of parents propagated via record()), and a cycle would cause infinite recursion in recordInternal/checkQuotas. The library rejects this at construction with an IllegalArgumentException naming the offending sensor.","triggerScenarios":"Creating a Sensor via Metrics.sensor(...) where the parents[] array transitively includes the sensor being created. The simplest case is passing a sensor that is already an ancestor of itself; the recursive checkForest visits each parent and fails when Set.add returns false for the same identity.","commonSituations":"Code that wires sensors together dynamically (e.g. aggregating per-partition sensors into a parent) and accidentally passes a descendant as a parent of its own ancestor; copy-paste of parent arrays; refactors that reverse the parent/child direction; plugins building derived sensors from sensors fetched out of the Metrics registry.","solutions":["Inspect the parents[] array passed to Metrics.sensor(name, config, parents...) and remove any sensor whose ancestor chain already contains the new sensor.","Build the parent graph from leaves to root so a child never references a sensor that has not yet been created, avoiding accidental back-edges.","Maintain a registry of created sensor names and verify the new sensor's name is not present in any candidate parent's parent chain before registering.","If the cycle arises from dynamic wiring, treat the parent relationship as a DAG and deduplicate parents against a visited set before construction."],"exampleFix":"// before\nSensor parent = metrics.sensor(\"agg\");\nSensor child = metrics.sensor(\"child\", new Sensor[]{parent});\n// bug: re-registering 'agg' with 'child' as a parent\nSensor bad = metrics.sensor(\"agg-2\", new Sensor[]{child}); // child -> parent -> agg-2 cycle\n\n// after\nSensor parent = metrics.sensor(\"agg\");\nSensor child = metrics.sensor(\"child\", new Sensor[]{parent});\n// parents only flow upward; never wire a descendant as ancestor","handlingStrategy":"validation","validationCode":"// Before calling metrics.sensor(name, parents...), ensure the parent array is a forest:\nSet<Sensor> seen = new HashSet<>();\nfor (Sensor p : parents) {\n    if (!seen.add(p)) {\n        throw new IllegalArgumentException(\"Duplicate parent sensor \" + p.name()\n            + \" — would trip the cycle detector\");\n    }\n}","typeGuard":"static boolean isAcyclicParents(Sensor[] parents) {\n    Set<Sensor> seen = new HashSet<>();\n    for (Sensor p : parents) {\n        if (!seen.add(p)) return false;\n    }\n    return true;\n}","tryCatchPattern":null,"preventionTips":["Never wire a sensor (directly or transitively) back into its own parent chain.","Dedupe the parents array before registering — duplicate ancestors trip the forest check.","Build parent sensors before children and keep the parent graph a strict tree/forest."],"tags":["metrics","sensor","cycle-detection","configuration"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}