{"record":{"id":"e9cc2effe78c4e5e","repo":"alibaba/canal","slug":"rollback-error-clientid-s-batchid-d-is-not-exis","errorCode":null,"errorMessage":"rollback error, clientId:%s batchId:%d is not exist , please check","messagePattern":"rollback error, clientId:(.+?) batchId:(.+?) is not exist , please check","errorType":"exception","errorClass":"CanalServerException","httpStatus":null,"severity":"error","filePath":"server/src/main/java/com/alibaba/otter/canal/server/embedded/CanalServerWithEmbedded.java","lineNumber":480,"sourceCode":"    /**\n     * 回滚到未进行 {@link #ack} 的地方，下次fetch的时候，可以从最后一个没有 {@link #ack} 的地方开始拿\n     */\n    @Override\n    public void rollback(ClientIdentity clientIdentity, Long batchId) throws CanalServerException {\n        checkStart(clientIdentity.getDestination());\n        CanalInstance canalInstance = canalInstances.get(clientIdentity.getDestination());\n\n        // 因为存在第一次链接时自动rollback的情况，所以需要忽略未订阅\n        boolean hasSubscribe = canalInstance.getMetaManager().hasSubscribe(clientIdentity);\n        if (!hasSubscribe) {\n            return;\n        }\n        synchronized (canalInstance) {\n            // 清除batch信息\n            PositionRange<LogPosition> positionRanges = canalInstance.getMetaManager().removeBatch(clientIdentity,\n                batchId);\n            if (positionRanges == null) { // 说明是重复的ack/rollback\n                throw new CanalServerException(String.format(\"rollback error, clientId:%s batchId:%d is not exist , please check\",\n                    clientIdentity.getClientId(),\n                    batchId));\n            }\n\n            // lastRollbackPostions.put(clientIdentity,\n            // positionRanges.getEnd());// 记录一下最后rollback的位置\n            // TODO 后续rollback到指定的batchId位置\n            canalInstance.getEventStore().rollback();// rollback\n                                                     // eventStore中的状态信息\n            logger.info(\"rollback successfully, clientId:{} batchId:{} position:{}\",\n                clientIdentity.getClientId(),\n                batchId,\n                positionRanges);\n        }\n    }\n\n    public Map<String, CanalInstance> getCanalInstances() {\n        return Maps.newHashMap(canalInstances);","sourceCodeStart":462,"sourceCodeEnd":498,"githubUrl":"https://github.com/alibaba/canal/blob/87be50e87686a3e8af08c368d0e1ffd1f59eb04a/server/src/main/java/com/alibaba/otter/canal/server/embedded/CanalServerWithEmbedded.java#L462-L498","documentation":"Thrown by CanalServerWithEmbedded.rollback(ClientIdentity, long batchId) when MetaManager.removeBatch(clientIdentity, batchId) returns null — the batchId is not registered (already acked, already rolled back, or never existed). Note the no-arg rollback path earlier in the method returns early if the client has no subscription, so this only fires for the batchId-specific rollback of a known-subscribed client.","triggerScenarios":"Calling rollback(clientIdentity, batchId) with a batchId that was already acked or already rolled back; rolling back a batchId never delivered by get(); rolling back after the batch metadata was cleared by a server restart or ZooKeeper session loss.","commonSituations":"Client retry logic that re-rolls back on failure (duplicate rollback); ack already happened then a late rollback arrives; client restart re-issuing rollback for a stale batchId.","solutions":["Treat an unknown-batch rollback as idempotent — catch CanalServerException mentioning 'is not exist' and log/continue rather than retry.","Guarantee rollback is invoked once per batchId and never after a successful ack of the same batch.","After restart, issue a no-arg rollback to reset cursor state instead of targeting a stale batchId.","Audit concurrent client threads sharing a clientId to prevent racing ack/rollback."],"exampleFix":"// before: rollback then retry blindly\ntry {\n    server.rollback(clientId, batchId);\n} catch (CanalServerException e) {\n    server.rollback(clientId, batchId);\n}\n// after: idempotent handling of stale batch\ntry {\n    server.rollback(clientId, batchId);\n} catch (CanalServerException e) {\n    if (!e.getMessage().contains(\"is not exist\")) throw e;\n    logger.warn(\"batch {} no longer tracked, rollback already applied\", batchId);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    server.rollback(clientId, batchId);\n} catch (CanalServerException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"is not exist\")) {\n        logger.warn(\"duplicate rollback ignored for batch {}\", batchId);\n        return;\n    }\n    throw e;\n}","preventionTips":["Do not rollback a batchId that was already acked.","On restart use no-arg rollback to reset cursor instead of targeting stale ids.","Serialize ack/rollback per clientId to prevent races."],"tags":["embedded-server","rollback","idempotency","duplicate-operation"],"backgroundTag":null,"analyzedSha":"87be50e87686a3e8af08c368d0e1ffd1f59eb04a","analyzedAt":"2026-08-14T04:30:11.918Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}