{"record":{"id":"c251c293595a85e5","repo":"alibaba/Sentinel","slug":"batchadd-is-unavailable-if-minmax-has-been-cal","errorCode":null,"errorMessage":"batchAdd() is unavailable if minMax() has been called","messagePattern":"batchAdd\\(\\) is unavailable if minMax\\(\\) has been called","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntryFunc.java","lineNumber":174,"sourceCode":"\n    @Override\n    public void countAndSum(long count, long value) {\n        throw new IllegalStateException(\"countAndSum() is unavailable if minMax() has been called\");\n    }\n\n    @Override\n    public void arrayAdd(long... values) {\n        throw new IllegalStateException(\"arrayAdd() is unavailable if minMax() has been called\");\n    }\n\n    @Override\n    public void arraySet(long... values) {\n        throw new IllegalStateException(\"arraySet() is unavailable if minMax() has been called\");\n    }\n\n    @Override\n    public void batchAdd(long... values) {\n        throw new IllegalStateException(\"batchAdd() is unavailable if minMax() has been called\");\n    }\n\n    @Override\n    public void minMax(long candidate, String ref) {\n        ValueRef lmax = max.get();\n        if (lmax.value <= candidate) {\n            final ValueRef cmax = new ValueRef(candidate, ref);\n            while (!max.compareAndSet(lmax, cmax) && (lmax = max.get()).value <= candidate) { ; }\n        }\n        ValueRef lmin = min.get();\n        if (lmin.value >= candidate) {\n            final ValueRef cmin = new ValueRef(candidate, ref);\n            while (!min.compareAndSet(lmin, cmin) && (lmin = min.get()).value >= candidate) { ; }\n        }\n    }\n\n    @Override\n    public void strArray(String... values) {","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/alibaba/Sentinel/blob/a3f40ba8e900c8489bd520274739f17235a7721c/sentinel-core/src/main/java/com/alibaba/csp/sentinel/eagleeye/StatEntryFunc.java#L156-L192","documentation":"StatEntryFuncMinMax throws IllegalStateException from batchAdd(long... values). Batch-add pushes several values into array slots at once; the min/max strategy instead folds every single observation into its two CAS-guarded ValueRef slots via the minMax method, so a batch API has no meaningful implementation here.","triggerScenarios":"A batching flusher calls batchAdd(buffer) on an entry that was created with the minMax strategy — e.g. buffered latency measurements flushed periodically to a min/max metric.","commonSituations":"Buffered async metric pipelines shared across metric types. Config-driven metric type switched to min/max while the flusher kept batchAdd. Code copied from an array metric module.","solutions":["Flush the batch by iterating minMax(v, ref) per element.","Or point the batch flusher at an array-strategy entry.","Centralize the type/method dispatch so batch flushers query getStatType() first."],"exampleFix":"// before\nfunc.batchAdd(buffered); // minMax func -> throws\n\n// after\nfor (LatencySample s : buffered) { func.minMax(s.value, s.ref); }","handlingStrategy":"type-guard","validationCode":"if (func.getStatType() == 4) {\n    for (Sample s : batch) func.minMax(s.value, s.ref);\n} else {\n    func.batchAdd(values);\n}","typeGuard":"boolean isMinMax(StatEntryFunc f) { return f.getStatType() == 4; }","tryCatchPattern":null,"preventionTips":["Async flush pipelines should resolve the strategy once and dispatch accordingly.","Per-value minMax calls are cheap (CAS loops); no need to batch.","Cover all strategies in flusher unit tests."],"tags":["eagleeye","stat-func","api-misuse","illegal-state","min-max","batch","sentinel"],"backgroundTag":null,"analyzedSha":"a3f40ba8e900c8489bd520274739f17235a7721c","analyzedAt":"2026-08-14T11:10:30.678Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}