From 484eb59893a09ccae0100aeb16d80e6820ae8837 Mon Sep 17 00:00:00 2001
From: Dmitrii Stepanov <d.stepanov@yadro.com>
Date: Tue, 19 Sep 2023 18:08:38 +0300
Subject: [PATCH] [#661] blobovniczatree: Use .db extension for db files

Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
---
 cmd/frostfs-lens/internal/meta/inspect.go     |  4 +-
 internal/logs/logs.go                         |  6 ++
 .../blobstor/blobovniczatree/active.go        |  2 +-
 .../blobstor/blobovniczatree/blobovnicza.go   | 13 +++-
 .../blobstor/blobovniczatree/control.go       | 47 +++++++++++++
 .../blobstor/blobovniczatree/control_test.go  | 69 +++++++++++++++++++
 .../blobstor/blobovniczatree/delete.go        |  4 +-
 .../blobstor/blobovniczatree/exists.go        |  4 +-
 .../blobstor/blobovniczatree/exists_test.go   |  2 +-
 .../blobstor/blobovniczatree/get.go           |  4 +-
 .../blobstor/blobovniczatree/get_range.go     |  4 +-
 .../blobovniczatree}/id.go                    |  6 +-
 .../blobstor/blobovniczatree/iterate.go       | 13 +++-
 .../blobstor/blobovniczatree/manager.go       |  2 +-
 .../blobstor/blobovniczatree/put.go           |  4 +-
 15 files changed, 160 insertions(+), 24 deletions(-)
 create mode 100644 pkg/local_object_storage/blobstor/blobovniczatree/control_test.go
 rename pkg/local_object_storage/{blobovnicza => blobstor/blobovniczatree}/id.go (71%)

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
 }