{"record":{"id":"de0e687da07994ee","repo":"alibaba/canal","slug":"batchid-d-is-not-the-firstly-d-de0e68","errorCode":null,"errorMessage":"batchId:%d is not the firstly:%d","messagePattern":"batchId:(.+?) is not the firstly:(.+?)","errorType":"validation","errorClass":"CanalMetaManagerException","httpStatus":null,"severity":"error","filePath":"meta/src/main/java/com/alibaba/otter/canal/meta/ZooKeeperMetaManager.java","lineNumber":201,"sourceCode":"\n    public PositionRange removeBatch(ClientIdentity clientIdentity, Long batchId) throws CanalMetaManagerException {\n        String batchsPath = ZookeeperPathUtils.getBatchMarkPath(clientIdentity.getDestination(),\n            clientIdentity.getClientId());\n        List<String> nodes = zkClientx.getChildren(batchsPath);\n        if (CollectionUtils.isEmpty(nodes)) {\n            // 没有batch记录\n            return null;\n        }\n\n        // 找到最小的Id\n        ArrayList<Long> batchIds = new ArrayList<>(nodes.size());\n        for (String batchIdString : nodes) {\n            batchIds.add(Long.valueOf(batchIdString));\n        }\n        Long minBatchId = Collections.min(batchIds);\n        if (!minBatchId.equals(batchId)) {\n            // 检查一下提交的ack/rollback，必须按batchId分出去的顺序提交，否则容易出现丢数据\n            throw new CanalMetaManagerException(String.format(\"batchId:%d is not the firstly:%d\", batchId, minBatchId));\n        }\n\n        if (!batchIds.contains(batchId)) {\n            // 不存在对应的batchId\n            return null;\n        }\n        PositionRange positionRange = getBatch(clientIdentity, batchId);\n        if (positionRange != null) {\n            String path = ZookeeperPathUtils\n                .getBatchMarkWithIdPath(clientIdentity.getDestination(), clientIdentity.getClientId(), batchId);\n            zkClientx.delete(path);\n        }\n\n        return positionRange;\n    }\n\n    public PositionRange getBatch(ClientIdentity clientIdentity, Long batchId) throws CanalMetaManagerException {\n        String path = ZookeeperPathUtils","sourceCodeStart":183,"sourceCodeEnd":219,"githubUrl":"https://github.com/alibaba/canal/blob/87be50e87686a3e8af08c368d0e1ffd1f59eb04a/meta/src/main/java/com/alibaba/otter/canal/meta/ZooKeeperMetaManager.java#L183-L219","documentation":"Thrown by ZooKeeperMetaManager during removePositionRange (ack/rollback path) when the given batchId exists in ZK but is not the minimum batchId. Like the memory manager, the ZK-backed meta enforces strict FIFO acknowledgement to avoid skipping events, but here the batch set is read from ZK child nodes of the batch-mark path.","triggerScenarios":"ZooKeeperMetaManager processes an ack/rollback for batchId; it lists batch nodes, converts them to Long, computes Collections.min, and if that min != batchId it throws. Occurs on client.ack(batchId) / client.rollback(batchId) when an older batch node still exists.","commonSituations":"Out-of-order ack across clients sharing a clientId, a client that fetched multiple batches but acks a later one first, stale batch nodes left in ZK after a crash, or two Canal clients racing on the same clientId.","solutions":["Ack batches in ascending batchId order; always resolve and ack the minimum outstanding first.","Ensure each consumer uses a unique clientId so concurrent acks do not interleave on the same batch namespace.","If stale nodes are corrupting order, clean the batch-mark ZK path for that destination/clientId (zkCli delete) as a last resort, after confirming no live consumer.","Audit client code for logic that reorders batches (e.g. acking after a retry that already advanced the cursor)."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// list ZK batch nodes and confirm batchId is the min before ack\nList<String> nodes = zkClient.getChildren(batchMarkPath);\nlong min = nodes.stream().mapToLong(Long::parseLong).min().orElseThrow();\nif (min != batchId) throw new IllegalStateException(\"out-of-order ack; oldest=\" + min);","typeGuard":null,"tryCatchPattern":"try { client.ack(batchId); }\ncatch (CanalMetaManagerException e) {\n    if (e.getMessage().startsWith(\"batchId:\")) { /* reconcile: ack oldest or rollback */ }\n    else throw e;\n}","preventionTips":["Ack in ascending batchId order.","One clientId per consumer; never share across concurrent ackers.","Clean stale batch nodes only when no live consumer is running."],"tags":["meta","zookeeper","ack-ordering","data-integrity"],"backgroundTag":null,"analyzedSha":"87be50e87686a3e8af08c368d0e1ffd1f59eb04a","analyzedAt":"2026-08-14T04:30:11.918Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}