diff --git a/cmd/frostfs-lens/internal/meta/inspect.go b/cmd/frostfs-lens/internal/meta/inspect.go index de0f24aeb..9eb60f966 100644 --- a/cmd/frostfs-lens/internal/meta/inspect.go +++ b/cmd/frostfs-lens/internal/meta/inspect.go @@ -5,7 +5,7 @@ import ( "fmt" common "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal" - "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobovnicza" + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/blobovniczatree" meta "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/metabase" objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" @@ -40,7 +40,7 @@ func inspectFunc(cmd *cobra.Command, _ []string) { common.ExitOnErr(cmd, common.Errf("could not check if the obj is small: %w", err)) if id := resStorageID.StorageID(); id != nil { - cmd.Printf("Object storageID: %s\n\n", blobovnicza.NewIDFromBytes(id).String()) + cmd.Printf("Object storageID: %s\n\n", blobovniczatree.NewIDFromBytes(id).Path()) } else { cmd.Printf("Object does not contain storageID\n\n") } diff --git a/internal/logs/logs.go b/internal/logs/logs.go index 4b8305e41..0c5e162b3 100644 --- a/internal/logs/logs.go +++ b/internal/logs/logs.go @@ -523,4 +523,10 @@ const ( BlobstoreRebuildStarted = "blobstore rebuild started" BlobstoreRebuildCompletedSuccessfully = "blobstore rebuild completed successfully" BlobstoreRebuildStopped = "blobstore rebuild stopped" + BlobovniczaTreeFixingFileExtensions = "fixing blobovnicza tree file extensions..." + BlobovniczaTreeFixingFileExtensionsCompletedSuccessfully = "fixing blobovnicza tree file extensions completed successfully" + BlobovniczaTreeFixingFileExtensionsFailed = "failed to fix blobovnicza tree file extensions" + BlobovniczaTreeFixingFileExtensionForFile = "fixing blobovnicza file extension..." + BlobovniczaTreeFixingFileExtensionCompletedSuccessfully = "fixing blobovnicza file extension completed successfully" + BlobovniczaTreeFixingFileExtensionFailed = "failed to fix blobovnicza file extension" ) diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/active.go b/pkg/local_object_storage/blobstor/blobovniczatree/active.go index def197318..da8880646 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/active.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/active.go @@ -154,7 +154,7 @@ func (m *activeDBManager) getNextSharedDB(lvlPath string) (*sharedDB, error) { var next *sharedDB for iterCount < m.leafWidth { - path := filepath.Join(lvlPath, u64ToHexString(idx)) + path := filepath.Join(lvlPath, u64ToHexStringExt(idx)) shDB := m.dbManager.GetByPath(path) db, err := shDB.Open() // open db to hold active DB open, will be closed if db is full, after m.replace or by activeDBManager.Close() if err != nil { diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/blobovnicza.go b/pkg/local_object_storage/blobstor/blobovniczatree/blobovnicza.go index fd5155ee3..d44049fa8 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/blobovnicza.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/blobovnicza.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "strings" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/common" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobstor/compression" @@ -63,6 +64,10 @@ var _ common.Storage = (*Blobovniczas)(nil) var errPutFailed = errors.New("could not save the object in any blobovnicza") +const ( + dbExtension = ".db" +) + // NewBlobovniczaTree returns new instance of blobovniczas tree. func NewBlobovniczaTree(opts ...Option) (blz *Blobovniczas) { blz = new(Blobovniczas) @@ -94,14 +99,16 @@ func addressHash(addr *oid.Address, path string) uint64 { return hrw.StringHash(a + path) } -// converts uint64 to hex string. func u64ToHexString(ind uint64) string { return strconv.FormatUint(ind, 16) } -// converts uint64 hex string to uint64. +func u64ToHexStringExt(ind uint64) string { + return strconv.FormatUint(ind, 16) + dbExtension +} + func u64FromHexString(str string) uint64 { - v, err := strconv.ParseUint(str, 16, 64) + v, err := strconv.ParseUint(strings.TrimSuffix(str, dbExtension), 16, 64) if err != nil { panic(fmt.Sprintf("blobovnicza name is not an index %s", str)) } diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/control.go b/pkg/local_object_storage/blobstor/blobovniczatree/control.go index d993767b7..75a30ad3d 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/control.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/control.go @@ -2,11 +2,17 @@ package blobovniczatree import ( "context" + "errors" + "os" + "path/filepath" + "strings" "git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs" "go.uber.org/zap" ) +var errFailedToChangeExtensionReadOnly = errors.New("failed to change blobovnicza extension: read only mode") + // Open opens blobovnicza tree. func (b *Blobovniczas) Open(readOnly bool) error { b.readOnly = readOnly @@ -21,6 +27,13 @@ func (b *Blobovniczas) Open(readOnly bool) error { func (b *Blobovniczas) Init() error { b.log.Debug(logs.BlobovniczatreeInitializingBlobovniczas) + b.log.Debug(logs.BlobovniczaTreeFixingFileExtensions) + if err := b.addDBExtensionToDBs(b.rootPath, 0); err != nil { + b.log.Error(logs.BlobovniczaTreeFixingFileExtensionsFailed, zap.Error(err)) + return err + } + b.log.Debug(logs.BlobovniczaTreeFixingFileExtensionsCompletedSuccessfully) + if b.readOnly { b.log.Debug(logs.BlobovniczatreeReadonlyModeSkipBlobovniczasInitialization) return nil @@ -64,3 +77,37 @@ func (b *Blobovniczas) getBlobovnicza(p string) *sharedDB { func (b *Blobovniczas) getBlobovniczaWithoutCaching(p string) *sharedDB { return b.commondbManager.GetByPath(p) } + +func (b *Blobovniczas) addDBExtensionToDBs(path string, depth uint64) error { + entries, err := os.ReadDir(path) + if os.IsNotExist(err) && depth == 0 { + return nil + } + + for _, entry := range entries { + if entry.IsDir() { + if err := b.addDBExtensionToDBs(filepath.Join(path, entry.Name()), depth+1); err != nil { + return err + } + continue + } + + if strings.HasSuffix(entry.Name(), dbExtension) { + continue + } + if b.readOnly { + return errFailedToChangeExtensionReadOnly + } + + sourcePath := filepath.Join(path, entry.Name()) + targetPath := filepath.Join(path, entry.Name()+dbExtension) + b.log.Debug(logs.BlobovniczaTreeFixingFileExtensionForFile, zap.String("source", sourcePath), zap.String("target", targetPath)) + if err := os.Rename(sourcePath, targetPath); err != nil { + b.log.Error(logs.BlobovniczaTreeFixingFileExtensionFailed, zap.String("source", sourcePath), zap.String("target", targetPath), zap.Error(err)) + return err + } + b.log.Debug(logs.BlobovniczaTreeFixingFileExtensionCompletedSuccessfully, zap.String("source", sourcePath), zap.String("target", targetPath)) + } + + return nil +} diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/control_test.go b/pkg/local_object_storage/blobstor/blobovniczatree/control_test.go new file mode 100644 index 000000000..0bf304fb1 --- /dev/null +++ b/pkg/local_object_storage/blobstor/blobovniczatree/control_test.go @@ -0,0 +1,69 @@ +package blobovniczatree + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/blobovnicza" + "github.com/stretchr/testify/require" +) + +func TestDBExtensionFix(t *testing.T) { + root := t.TempDir() + createTestTree(t, 0, 2, 3, root) + t.Run("adds suffix if not exists", func(t *testing.T) { + openAndCloseTestTree(t, 2, 3, root) + validateTestTree(t, root) + }) + + t.Run("not adds second suffix if exists", func(t *testing.T) { + openAndCloseTestTree(t, 2, 3, root) + validateTestTree(t, root) + }) +} + +func createTestTree(t *testing.T, currentDepth, depth, width uint64, path string) { + if currentDepth == depth { + var w uint64 + for ; w < width; w++ { + dbPath := filepath.Join(path, u64ToHexString(w)) + b := blobovnicza.New(blobovnicza.WithPath(dbPath)) + require.NoError(t, b.Open()) + require.NoError(t, b.Init()) + require.NoError(t, b.Close()) + } + return + } + + var w uint64 + for ; w < width; w++ { + createTestTree(t, currentDepth+1, depth, width, filepath.Join(path, u64ToHexString(w))) + } +} + +func openAndCloseTestTree(t *testing.T, depth, width uint64, path string) { + blz := NewBlobovniczaTree( + WithBlobovniczaShallowDepth(depth), + WithBlobovniczaShallowWidth(width), + WithRootPath(path), + ) + require.NoError(t, blz.Open(false)) + require.NoError(t, blz.Init()) + require.NoError(t, blz.Close()) +} + +func validateTestTree(t *testing.T, path string) { + entries, err := os.ReadDir(path) + require.NoError(t, err) + + for _, entry := range entries { + if entry.IsDir() { + validateTestTree(t, filepath.Join(path, entry.Name())) + } else { + require.True(t, strings.HasSuffix(entry.Name(), dbExtension)) + require.False(t, strings.HasSuffix(strings.TrimSuffix(entry.Name(), dbExtension), dbExtension)) + } + } +} diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/delete.go b/pkg/local_object_storage/blobstor/blobovniczatree/delete.go index 28e3a8f36..9008672da 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/delete.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/delete.go @@ -47,8 +47,8 @@ func (b *Blobovniczas) Delete(ctx context.Context, prm common.DeletePrm) (res co bPrm.SetAddress(prm.Address) if prm.StorageID != nil { - id := blobovnicza.NewIDFromBytes(prm.StorageID) - shBlz := b.getBlobovnicza(id.String()) + id := NewIDFromBytes(prm.StorageID) + shBlz := b.getBlobovnicza(id.Path()) blz, err := shBlz.Open() if err != nil { return res, err diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/exists.go b/pkg/local_object_storage/blobstor/blobovniczatree/exists.go index e1a6f5ed5..514ee5f95 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/exists.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/exists.go @@ -36,8 +36,8 @@ func (b *Blobovniczas) Exists(ctx context.Context, prm common.ExistsPrm) (common defer span.End() if prm.StorageID != nil { - id := blobovnicza.NewIDFromBytes(prm.StorageID) - shBlz := b.getBlobovnicza(id.String()) + id := NewIDFromBytes(prm.StorageID) + shBlz := b.getBlobovnicza(id.Path()) blz, err := shBlz.Open() if err != nil { return common.ExistsRes{}, err diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/exists_test.go b/pkg/local_object_storage/blobstor/blobovniczatree/exists_test.go index c89262343..63df97595 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/exists_test.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/exists_test.go @@ -55,7 +55,7 @@ func TestExistsInvalidStorageID(t *testing.T) { // An invalid boltdb file is created so that it returns an error when opened require.NoError(t, os.MkdirAll(filepath.Join(dir, relBadFileDir), os.ModePerm)) - require.NoError(t, os.WriteFile(filepath.Join(dir, relBadFileDir, badFileName), []byte("not a boltdb file content"), 0o777)) + require.NoError(t, os.WriteFile(filepath.Join(dir, relBadFileDir, badFileName+".db"), []byte("not a boltdb file content"), 0o777)) res, err := b.Exists(context.Background(), common.ExistsPrm{Address: addr, StorageID: []byte(filepath.Join(relBadFileDir, badFileName))}) require.Error(t, err) diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/get.go b/pkg/local_object_storage/blobstor/blobovniczatree/get.go index 49849e759..0a1f29265 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/get.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/get.go @@ -47,8 +47,8 @@ func (b *Blobovniczas) Get(ctx context.Context, prm common.GetPrm) (res common.G bPrm.SetAddress(prm.Address) if prm.StorageID != nil { - id := blobovnicza.NewIDFromBytes(prm.StorageID) - shBlz := b.getBlobovnicza(id.String()) + id := NewIDFromBytes(prm.StorageID) + shBlz := b.getBlobovnicza(id.Path()) blz, err := shBlz.Open() if err != nil { return res, err diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/get_range.go b/pkg/local_object_storage/blobstor/blobovniczatree/get_range.go index 91d1e3ce0..3a8fae758 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/get_range.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/get_range.go @@ -46,8 +46,8 @@ func (b *Blobovniczas) GetRange(ctx context.Context, prm common.GetRangePrm) (re defer span.End() if prm.StorageID != nil { - id := blobovnicza.NewIDFromBytes(prm.StorageID) - shBlz := b.getBlobovnicza(id.String()) + id := NewIDFromBytes(prm.StorageID) + shBlz := b.getBlobovnicza(id.Path()) blz, err := shBlz.Open() if err != nil { return common.GetRangeRes{}, err diff --git a/pkg/local_object_storage/blobovnicza/id.go b/pkg/local_object_storage/blobstor/blobovniczatree/id.go similarity index 71% rename from pkg/local_object_storage/blobovnicza/id.go rename to pkg/local_object_storage/blobstor/blobovniczatree/id.go index 3d3ccf8b9..a080819bc 100644 --- a/pkg/local_object_storage/blobovnicza/id.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/id.go @@ -1,4 +1,4 @@ -package blobovnicza +package blobovniczatree // ID represents Blobovnicza identifier. type ID []byte @@ -8,8 +8,8 @@ func NewIDFromBytes(v []byte) *ID { return (*ID)(&v) } -func (id ID) String() string { - return string(id) +func (id ID) Path() string { + return string(id) + dbExtension } func (id ID) Bytes() []byte { diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/iterate.go b/pkg/local_object_storage/blobstor/blobovniczatree/iterate.go index d2b285c65..a0bfc374f 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/iterate.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/iterate.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path/filepath" + "strings" "time" "git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs" @@ -54,7 +55,7 @@ func (b *Blobovniczas) Iterate(ctx context.Context, prm common.IteratePrm) (comm return prm.Handler(common.IterationElement{ Address: elem.Address(), ObjectData: data, - StorageID: []byte(p), + StorageID: []byte(strings.TrimSuffix(p, dbExtension)), }) } return nil @@ -140,10 +141,16 @@ func (b *Blobovniczas) iterateSorted(ctx context.Context, addr *oid.Address, cur return false, ctx.Err() default: } + + lastPart := u64ToHexString(indices[i]) + if isLeafLevel { + lastPart = u64ToHexStringExt(indices[i]) + } + if i == 0 { - curPath = append(curPath, u64ToHexString(indices[i])) + curPath = append(curPath, lastPart) } else { - curPath[len(curPath)-1] = u64ToHexString(indices[i]) + curPath[len(curPath)-1] = lastPart } if exec { diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/manager.go b/pkg/local_object_storage/blobstor/blobovniczatree/manager.go index d695cb199..b89ff1de7 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/manager.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/manager.go @@ -117,7 +117,7 @@ func newLevelDBManager(width uint64, options []blobovnicza.Option, rootPath stri databases: make([]*sharedDB, width), } for idx := uint64(0); idx < width; idx++ { - result.databases[idx] = newSharedDB(options, filepath.Join(rootPath, lvlPath, u64ToHexString(idx)), readOnly, metrics, openDBCounter, closedFlog, log) + result.databases[idx] = newSharedDB(options, filepath.Join(rootPath, lvlPath, u64ToHexStringExt(idx)), readOnly, metrics, openDBCounter, closedFlog, log) } return result } diff --git a/pkg/local_object_storage/blobstor/blobovniczatree/put.go b/pkg/local_object_storage/blobstor/blobovniczatree/put.go index 6f9c8c0de..8c8697c20 100644 --- a/pkg/local_object_storage/blobstor/blobovniczatree/put.go +++ b/pkg/local_object_storage/blobstor/blobovniczatree/put.go @@ -70,7 +70,7 @@ func (b *Blobovniczas) Put(ctx context.Context, prm common.PutPrm) (common.PutRe type putIterator struct { B *Blobovniczas - ID *blobovnicza.ID + ID *ID AllFull bool PutPrm blobovnicza.PutPrm } @@ -113,7 +113,7 @@ func (i *putIterator) iterate(ctx context.Context, lvlPath string) (bool, error) } idx := u64FromHexString(filepath.Base(active.Path())) - i.ID = blobovnicza.NewIDFromBytes([]byte(filepath.Join(lvlPath, u64ToHexString(idx)))) + i.ID = NewIDFromBytes([]byte(filepath.Join(lvlPath, u64ToHexString(idx)))) return true, nil }