{"record":{"id":"a1f68871bff88744","repo":"weaviate/weaviate","slug":"w-swapping-bucket-q-with-q-in-store-q","errorCode":null,"errorMessage":"%w: swapping bucket %q with %q in store %q","messagePattern":"%w: swapping bucket %q with %q in store %q","errorType":"exception","errorClass":"ErrAlreadyClosed","httpStatus":null,"severity":"error","filePath":"adapters/repos/db/lsmkv/store.go","lineNumber":738,"sourceCode":"//\n// Registry side effect: the source bucket's on-disk path is released from\n// [GlobalBucketRegistry] as part of the swap. The source bucket continues to\n// serve queries from its original on-disk directory (the rename is deferred\n// to next-restart finalization), but in-process callers may now load a fresh\n// bucket at that path (typically after wiping the dir via\n// cleanStaleSidecarDirs). Without this release a back-to-back migration in\n// the same process — e.g. two consecutive filterable retokenizations on the\n// same property — aborts at OnAfterLsmInit with\n// \"bucket already registered\" when the second cycle's ingest bucket tries to\n// claim the same path. The displaced (old-main) bucket has its own registry\n// entry which is cleaned up by the caller's subsequent Shutdown of the\n// returned bucket — that path is NOT released here.\nfunc (s *Store) SwapBucketPointer(ctx context.Context, targetName, sourceName string) (*Bucket, error) {\n\ts.closeLock.RLock()\n\tdefer s.closeLock.RUnlock()\n\n\tif s.closed {\n\t\treturn nil, fmt.Errorf(\"%w: swapping bucket %q with %q in store %q\",\n\t\t\tErrAlreadyClosed, targetName, sourceName, s.dir)\n\t}\n\n\ts.bucketAccessLock.Lock()\n\tdefer s.bucketAccessLock.Unlock()\n\n\toldBucket, ok := s.bucketsByName[targetName]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"target bucket %q not found in store %q\", targetName, s.dir)\n\t}\n\n\tsourceBucket, ok := s.bucketsByName[sourceName]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"source bucket %q not found in store %q\", sourceName, s.dir)\n\t}\n\n\ts.bucketsByName[targetName] = sourceBucket\n\tdelete(s.bucketsByName, sourceName)","sourceCodeStart":720,"sourceCodeEnd":756,"githubUrl":"https://github.com/weaviate/weaviate/blob/75aa4b6d11f8818305aafd4440b4e32794f7ca04/adapters/repos/db/lsmkv/store.go#L720-L756","documentation":"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).","triggerScenarios":"Calling Store.SwapBucketPointer after Store.Shutdown(), or racing with a concurrent Shutdown that sets s.closed first.","commonSituations":"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.","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"],"exampleFix":"// before\nold, err := store.SwapBucketPointer(ctx, targetName, sourceName)\n// after\nold, err := store.SwapBucketPointer(ctx, targetName, sourceName)\nif errors.Is(err, lsmkv.ErrAlreadyClosed) {\n    return nil // store shut down; skip swap\n}","handlingStrategy":"type-guard","validationCode":"if storeIsShuttingDown { skipSwap() }","typeGuard":"func isStoreClosedErr(err error) bool {\n    return errors.Is(err, lsmkv.ErrAlreadyClosed)\n}","tryCatchPattern":"old, err := store.SwapBucketPointer(ctx, target, source)\nif err != nil {\n    if errors.Is(err, lsmkv.ErrAlreadyClosed) { return nil }\n    return err\n}","preventionTips":["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"],"tags":["lsmkv","lifecycle","closed-store","bucket-swap"],"backgroundTag":"store-already-closed","analyzedSha":"75aa4b6d11f8818305aafd4440b4e32794f7ca04","analyzedAt":"2026-09-04T14:58:20.392Z","contentChangedAt":"2026-09-04T14:58:20.392Z","schemaVersion":2},"datasetVersion":"2026-09-11T21:17:09.523Z"}