alibaba/canal · error · CanalMetaManagerException

batchId:%d is not the firstly:%d

Error message

batchId:%d is not the firstly:%d

What it means

Thrown by MemoryMetaManager's MemoryClientIdentityBatch.removePositionRange when an ack/rollback is attempted for a batchId that exists but is not the smallest (oldest) outstanding batch. Canal requires batches to be acknowledged strictly in the order they were handed out, to prevent data gaps. Acking out of order is treated as a data-integrity hazard.

Source

Thrown at meta/src/main/java/com/alibaba/otter/canal/meta/MemoryMetaManager.java:152

        }

        public synchronized void addPositionRange(PositionRange positionRange, Long batchId) {
            updateMaxId(batchId);
            batches.put(batchId, positionRange);
        }

        public synchronized Long addPositionRange(PositionRange positionRange) {
            Long batchId = atomicMaxBatchId.getAndIncrement();
            batches.put(batchId, positionRange);
            return batchId;
        }

        public synchronized PositionRange removePositionRange(Long batchId) {
            if (batches.containsKey(batchId)) {
                Long minBatchId = Collections.min(batches.keySet());
                if (!minBatchId.equals(batchId)) {
                    // 检查一下提交的ack/rollback,必须按batchId分出去的顺序提交,否则容易出现丢数据
                    throw new CanalMetaManagerException(String.format("batchId:%d is not the firstly:%d",
                        batchId,
                        minBatchId));
                }
                return batches.remove(batchId);
            } else {
                return null;
            }
        }

        public synchronized PositionRange getPositionRange(Long batchId) {
            return batches.get(batchId);
        }

        public synchronized PositionRange getLastestPositionRange() {
            if (batches.size() == 0) {
                return null;
            } else {
                Long batchId = Collections.max(batches.keySet());

View on GitHub (pinned to 87be50e876)

Solutions

  1. Always ack the smallest outstanding batchId first; track and ack in ascending order.
  2. If you must skip, use rollback on the older batch before acking newer ones, or reset the client identity.
  3. Avoid sharing a clientId across concurrent consumers that ack independently; use distinct clientId per consumer.
  4. Consider a persistent meta manager (ZooKeeper/file) so batch order survives restarts instead of in-memory loss.

Example fix

// before: ack whatever batch arrived last
client.ack(receivedBatchId);

// after: ack strictly the oldest outstanding
long oldest = client.getWithoutAck(...); // fetch in order
client.ack(oldest);
Defensive patterns

Strategy: validation

Validate before calling

// before ack, ensure this batchId is the oldest outstanding
Long oldest = memoryClientBatch.getFirstBatchId(); // expose/get min key
if (oldest == null || !oldest.equals(batchId)) {
    throw new IllegalStateException("refusing out-of-order ack; oldest=" + oldest + " acking=" + batchId);
}

Try / catch

try { client.ack(batchId); }
catch (CanalMetaManagerException e) {
    if (e.getMessage().startsWith("batchId:")) { client.rollback(batchId); /* or reset cursor */ }
    else throw e;
}

Prevention

When it happens

Trigger: A client calls ack(batchId) or rollback(batchId) via MemoryMetaManager where batches.containsKey(batchId) is true but Collections.min(batches.keySet()) != batchId, i.e. an earlier batch remains unacked.

Common situations: Client acknowledges a newer batch before an older one (concurrent/out-of-order consumers), a client crashed mid-batch and restarted picking a newer batch, or logic that picks the latest batch instead of processing in FIFO order. With in-memory meta this also means prior state is lost on restart so ordering assumptions break.

Related errors


AI-assisted analysis of alibaba/canal@87be50e876 (2026-08-14). Data as JSON: /api/errors/9b2a07c745543482. Report an issue: GitHub.