ethereum/go-ethereum · critical

duplicated flush operation

Error message

duplicated flush operation

What it means

pathdb's in-memory buffer flushes dirty nodes to disk asynchronously, and flush marks the buffer as consumed by setting b.done. A second flush on the same buffer means the database attempted to persist two overlapping buffer generations — a lifecycle invariant violation that would risk writing inconsistent state, so it panics immediately.

Source

Thrown at triedb/pathdb/buffer.go:137

	return b.layers == 0
}

// full returns an indicator if the size of accumulated content exceeds the
// configured threshold.
func (b *buffer) full() bool {
	return b.size() > b.limit
}

// size returns the approximate memory size of the held content.
func (b *buffer) size() uint64 {
	return b.states.size + b.nodes.size
}

// flush persists the in-memory dirty trie node into the disk if the configured
// memory threshold is reached. Note, all data must be written atomically.
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
	if b.done != nil {
		panic("duplicated flush operation")
	}
	b.done = make(chan struct{}) // allocate the channel for notification

	// Schedule the background thread to construct the batch, which usually
	// take a few seconds.
	go func() {
		defer func() {
			if postFlush != nil {
				postFlush()
			}
			close(b.done)
		}()

		// Ensure the target state id is aligned with the internal counter.
		head := rawdb.ReadPersistentStateID(db)
		if head+b.layers != id {
			b.flushErr = fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id)
			return

View on GitHub (pinned to 6bb0588ad8)

Solutions

  1. Serialize commits: pathdb commits must go through the single db.commit lock; check for a fork that bypassed it
  2. Inspect the goroutine stacks in the panic dump to find which two paths both reached flush, and gate the second on the first's completion via the done channel
  3. Upgrade/patch if this reproduces on stock geth — it is an internal invariant break, not caller error

Example fix

// before
// two goroutines both trigger the limit path
go db.Commit(...)  // flush #1
go db.Commit(...)  // flush #2 -> duplicated flush operation

// after
db.lock.Lock()
defer db.lock.Unlock()
db.Commit(...) // flush serialized under the db lock
Defensive patterns

Strategy: validation

Validate before calling

// (Internal) callers must ensure single flush per buffer generation
if b.done != nil {
    return errors.New("buffer already flushing") // hypothetical guard if you fork flush
}

Try / catch

defer func() {
    if r := recover(); r != nil {
        log.Crit("pathdb double flush", "panic", r) // crash-dump, do not continue serving state
    }
}()
triedb.Commit(...)

Prevention

When it happens

Trigger: buffer.flush being invoked twice on the same buffer object — e.g. two concurrent commits hitting the memory limit, or a commit racing with a database close/reopen that re-flushes an already-draining buffer.

Common situations: Concurrent calls to triedb Commit/Flush from multiple goroutines without the outer lock; fork modifications that call flush manually; bugs in the layering that reuse a buffer after checkpointing.

Related errors


AI-assisted analysis of ethereum/go-ethereum@6bb0588ad8 (2026-08-15). Data as JSON: /api/errors/cf9680fc9e8cede7. Report an issue: GitHub.