weaviate/weaviate · error · ErrAlreadyClosed

%w: swapping bucket %q with %q in store %q

Error message

%w: swapping bucket %q with %q in store %q

What it means

SwapBucketPointer atomically redirects lookups of targetName to the bucket currently registered as sourceName. It returns wrapped ErrAlreadyClosed if the store has already been shut down — the pointer swap touches the in-memory registry and must not run on a closed store. Detect with errors.Is(err, lsmkv.ErrAlreadyClosed).

Source

Thrown at adapters/repos/db/lsmkv/store.go:738

//
// Registry side effect: the source bucket's on-disk path is released from
// [GlobalBucketRegistry] as part of the swap. The source bucket continues to
// serve queries from its original on-disk directory (the rename is deferred
// to next-restart finalization), but in-process callers may now load a fresh
// bucket at that path (typically after wiping the dir via
// cleanStaleSidecarDirs). Without this release a back-to-back migration in
// the same process — e.g. two consecutive filterable retokenizations on the
// same property — aborts at OnAfterLsmInit with
// "bucket already registered" when the second cycle's ingest bucket tries to
// claim the same path. The displaced (old-main) bucket has its own registry
// entry which is cleaned up by the caller's subsequent Shutdown of the
// returned bucket — that path is NOT released here.
func (s *Store) SwapBucketPointer(ctx context.Context, targetName, sourceName string) (*Bucket, error) {
	s.closeLock.RLock()
	defer s.closeLock.RUnlock()

	if s.closed {
		return nil, fmt.Errorf("%w: swapping bucket %q with %q in store %q",
			ErrAlreadyClosed, targetName, sourceName, s.dir)
	}

	s.bucketAccessLock.Lock()
	defer s.bucketAccessLock.Unlock()

	oldBucket, ok := s.bucketsByName[targetName]
	if !ok {
		return nil, fmt.Errorf("target bucket %q not found in store %q", targetName, s.dir)
	}

	sourceBucket, ok := s.bucketsByName[sourceName]
	if !ok {
		return nil, fmt.Errorf("source bucket %q not found in store %q", sourceName, s.dir)
	}

	s.bucketsByName[targetName] = sourceBucket
	delete(s.bucketsByName, sourceName)

View on GitHub (pinned to 75aa4b6d11)

Solutions

  1. Serialize the swap against shutdown: perform SwapBucketPointer before closing the store, or hold a lifecycle guard
  2. Treat ErrAlreadyClosed as a terminal no-op for migration swaps (migration is moot after shutdown)
  3. In tests, ensure swap goroutines complete (via WaitGroup) before store teardown

Example fix

// before
old, err := store.SwapBucketPointer(ctx, targetName, sourceName)
// after
old, err := store.SwapBucketPointer(ctx, targetName, sourceName)
if errors.Is(err, lsmkv.ErrAlreadyClosed) {
    return nil // store shut down; skip swap
}
Defensive patterns

Strategy: type-guard

Validate before calling

if storeIsShuttingDown { skipSwap() }

Type guard

func isStoreClosedErr(err error) bool {
    return errors.Is(err, lsmkv.ErrAlreadyClosed)
}

Try / catch

old, err := store.SwapBucketPointer(ctx, target, source)
if err != nil {
    if errors.Is(err, lsmkv.ErrAlreadyClosed) { return nil }
    return err
}

Prevention

When it happens

Trigger: Calling Store.SwapBucketPointer after Store.Shutdown(), or racing with a concurrent Shutdown that sets s.closed first.

Common situations: Asynchronous migration workers (e.g. filterable retokenization swaps) still running during graceful shutdown; test teardown closing the store while a swap test goroutine is mid-flight.

Related errors


AI-assisted analysis of weaviate/weaviate@75aa4b6d11 (2026-09-04). Data as JSON: /api/errors/a1f68871bff88744. Report an issue: GitHub.