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
- Serialize the swap against shutdown: perform SwapBucketPointer before closing the store, or hold a lifecycle guard
- Treat ErrAlreadyClosed as a terminal no-op for migration swaps (migration is moot after shutdown)
- 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
- Ensure swap workers finish before store.Shutdown (WaitGroup/errgroup join)
- Guard migration goroutines with a stop channel tied to shutdown
- In tests, join all concurrent swap goroutines before teardown of the store
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
- %w: updating buckets state in store %q
- %w: adding a bucket %q to store %q
- %w: closing store %q
- %w: writing wals of store %q
- %w: replacing bucket %q for %q in store %q
AI-assisted analysis of weaviate/weaviate@75aa4b6d11 (2026-09-04).
Data as JSON: /api/errors/a1f68871bff88744.
Report an issue: GitHub.