mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-29 00:10:42 +01:00
* test: prefixed directory listing with start below the prefix A listing constrained to prefix "item-b" and resumed from "item" must return item-b.versions. The seek-based stores return an empty page because the iterator lands before the prefix range and breaks on the first non-matching name. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: clamp prefixed-listing seek to the prefix range Seek-based stores (leveldb, leveldb2, leveldb3, rocksdb, tikv, etcd) started the scan at startFileName and stopped at the first key outside the prefix. A startFileName below the prefix landed the iterator on a non-matching sibling first, so the listing returned an empty page even though matching entries followed. etcd had the mirror problem: a range beginning below the prefix end leaked non-matching entries. Begin the seek at max(startFileName, prefix), which is what foundationdb and the sql stores already do. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer(hbase): honor startFileName lower bound in prefixed listings The scan starts at the prefix but only skipped the exact startFileName, so entries sorting before it were returned again on resume. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: keep paging a prefixed listing while matches can still follow The fallback for stores without native prefix support stopped paging as soon as the last scanned name reached the prefix, dropping prefix matches that continue on the next page. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: return the last delivered name from the prefixed-listing fallback The fallback for stores without native prefix support pages raw entries and filters client-side. Keep the scan cursor and the returned cursor separate: resuming must continue after the last delivered match, not the last scanned name, or matches still inside the current raw page are skipped on the next page. Also keep paging while the scan cursor is inside or below the prefix range instead of stopping at the first name that reaches the prefix. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer(hbase): start prefixed-listing scans at the resume key Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
327 lines
9.2 KiB
Go
327 lines
9.2 KiB
Go
package leveldb
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
leveldb_errors "github.com/syndtr/goleveldb/leveldb/errors"
|
|
"github.com/syndtr/goleveldb/leveldb/filter"
|
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
|
leveldb_util "github.com/syndtr/goleveldb/leveldb/util"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
weed_util "github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func init() {
|
|
filer.Stores = append(filer.Stores, &LevelDB2Store{})
|
|
}
|
|
|
|
type LevelDB2Store struct {
|
|
dbs []*leveldb.DB
|
|
dbCount int
|
|
ReadOnly bool
|
|
|
|
blockCacheSizeMB int
|
|
writeBufferSizeMB int
|
|
blockSizeKB int
|
|
compactionTableSizeMB int
|
|
}
|
|
|
|
func (store *LevelDB2Store) GetName() string {
|
|
return "leveldb2"
|
|
}
|
|
|
|
func (store *LevelDB2Store) Initialize(configuration weed_util.Configuration, prefix string) (err error) {
|
|
dir := configuration.GetString(prefix + "dir")
|
|
store.blockCacheSizeMB = configuration.GetInt(prefix + "block_cache_size_mb")
|
|
store.writeBufferSizeMB = configuration.GetInt(prefix + "write_buffer_size_mb")
|
|
store.blockSizeKB = configuration.GetInt(prefix + "block_size_kb")
|
|
store.compactionTableSizeMB = configuration.GetInt(prefix + "compaction_table_size_mb")
|
|
if err := store.validateSizeOptions(); err != nil {
|
|
return err
|
|
}
|
|
return store.initialize(dir, 8)
|
|
}
|
|
|
|
// validateSizeOptions rejects sizes too large to convert to bytes without
|
|
// overflowing int on this platform.
|
|
func (store *LevelDB2Store) validateSizeOptions() error {
|
|
for name, kv := range map[string][2]int{
|
|
"block_cache_size_mb": {store.blockCacheSizeMB, 1 << 20},
|
|
"write_buffer_size_mb": {store.writeBufferSizeMB, 1 << 20},
|
|
"block_size_kb": {store.blockSizeKB, 1 << 10},
|
|
"compaction_table_size_mb": {store.compactionTableSizeMB, 1 << 20},
|
|
} {
|
|
if kv[0] > math.MaxInt/kv[1] {
|
|
return fmt.Errorf("leveldb2 option %s=%d overflows int", name, kv[0])
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) buildOptions() *opt.Options {
|
|
opts := &opt.Options{
|
|
BlockCacheCapacity: 32 * 1024 * 1024, // default value is 8MiB
|
|
WriteBuffer: 16 * 1024 * 1024, // default value is 4MiB
|
|
Filter: filter.NewBloomFilter(8), // false positive rate 0.02
|
|
ReadOnly: store.ReadOnly,
|
|
}
|
|
if store.blockCacheSizeMB > 0 {
|
|
opts.BlockCacheCapacity = store.blockCacheSizeMB * 1024 * 1024
|
|
}
|
|
if store.writeBufferSizeMB > 0 {
|
|
opts.WriteBuffer = store.writeBufferSizeMB * 1024 * 1024
|
|
}
|
|
if store.blockSizeKB > 0 {
|
|
opts.BlockSize = store.blockSizeKB * 1024
|
|
}
|
|
if store.compactionTableSizeMB > 0 {
|
|
opts.CompactionTableSize = store.compactionTableSizeMB * 1024 * 1024
|
|
}
|
|
return opts
|
|
}
|
|
|
|
func (store *LevelDB2Store) initialize(dir string, dbCount int) (err error) {
|
|
glog.Infof("filer store leveldb2 dir: %s", dir)
|
|
os.MkdirAll(dir, 0755)
|
|
if err := weed_util.TestFolderWritable(dir); err != nil {
|
|
return fmt.Errorf("Check Level Folder %s Writable: %s", dir, err)
|
|
}
|
|
|
|
opts := store.buildOptions()
|
|
|
|
for d := 0; d < dbCount; d++ {
|
|
dbFolder := fmt.Sprintf("%s/%02d", dir, d)
|
|
os.MkdirAll(dbFolder, 0755)
|
|
db, dbErr := leveldb.OpenFile(dbFolder, opts)
|
|
if leveldb_errors.IsCorrupted(dbErr) {
|
|
db, dbErr = leveldb.RecoverFile(dbFolder, opts)
|
|
}
|
|
if dbErr != nil {
|
|
glog.Errorf("filer store open dir %s: %v", dbFolder, dbErr)
|
|
return dbErr
|
|
}
|
|
store.dbs = append(store.dbs, db)
|
|
}
|
|
store.dbCount = dbCount
|
|
|
|
return
|
|
}
|
|
|
|
func (store *LevelDB2Store) BeginTransaction(ctx context.Context) (context.Context, error) {
|
|
return ctx, nil
|
|
}
|
|
func (store *LevelDB2Store) CommitTransaction(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
func (store *LevelDB2Store) RollbackTransaction(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
var syncWriteOptions = &opt.WriteOptions{Sync: true}
|
|
|
|
func writeOptions(ctx context.Context) *opt.WriteOptions {
|
|
if filer.SyncWrite(ctx) {
|
|
return syncWriteOptions
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) InsertEntry(ctx context.Context, entry *filer.Entry) (err error) {
|
|
dir, name := entry.DirAndName()
|
|
key, partitionId := genKey(dir, name, store.dbCount)
|
|
|
|
value, err := entry.EncodeAttributesAndChunks()
|
|
if err != nil {
|
|
return fmt.Errorf("encoding %s %+v: %v", entry.FullPath, entry.Attr, err)
|
|
}
|
|
|
|
if len(entry.GetChunks()) > filer.CountEntryChunksForGzip {
|
|
value = weed_util.MaybeGzipData(value)
|
|
}
|
|
|
|
err = store.dbs[partitionId].Put(key, value, writeOptions(ctx))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("persisting %s : %v", entry.FullPath, err)
|
|
}
|
|
|
|
// println("saved", entry.FullPath, "chunks", len(entry.GetChunks()))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) UpdateEntry(ctx context.Context, entry *filer.Entry) (err error) {
|
|
|
|
return store.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
func (store *LevelDB2Store) FindEntry(ctx context.Context, fullpath weed_util.FullPath) (entry *filer.Entry, err error) {
|
|
dir, name := fullpath.DirAndName()
|
|
key, partitionId := genKey(dir, name, store.dbCount)
|
|
|
|
data, err := store.dbs[partitionId].Get(key, nil)
|
|
|
|
if err == leveldb.ErrNotFound {
|
|
return nil, filer_pb.ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get %s : %v", fullpath, err)
|
|
}
|
|
|
|
entry = &filer.Entry{
|
|
FullPath: fullpath,
|
|
}
|
|
err = entry.DecodeAttributesAndChunks(weed_util.MaybeDecompressData(data))
|
|
if err != nil {
|
|
return entry, fmt.Errorf("decode %s : %v", entry.FullPath, err)
|
|
}
|
|
|
|
// println("read", entry.FullPath, "chunks", len(entry.GetChunks()), "data", len(data), string(data))
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) DeleteEntry(ctx context.Context, fullpath weed_util.FullPath) (err error) {
|
|
dir, name := fullpath.DirAndName()
|
|
key, partitionId := genKey(dir, name, store.dbCount)
|
|
|
|
err = store.dbs[partitionId].Delete(key, writeOptions(ctx))
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s : %v", fullpath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) DeleteFolderChildren(ctx context.Context, fullpath weed_util.FullPath) (err error) {
|
|
directoryPrefix, partitionId := genDirectoryKeyPrefix(fullpath, "", store.dbCount)
|
|
|
|
batch := new(leveldb.Batch)
|
|
|
|
iter := store.dbs[partitionId].NewIterator(&leveldb_util.Range{Start: directoryPrefix}, nil)
|
|
for iter.Next() {
|
|
key := iter.Key()
|
|
if !bytes.HasPrefix(key, directoryPrefix) {
|
|
break
|
|
}
|
|
fileName := getNameFromKey(key)
|
|
if fileName == "" {
|
|
continue
|
|
}
|
|
batch.Delete(append(directoryPrefix, []byte(fileName)...))
|
|
}
|
|
iter.Release()
|
|
|
|
err = store.dbs[partitionId].Write(batch, writeOptions(ctx))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s : %v", fullpath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *LevelDB2Store) ListDirectoryEntries(ctx context.Context, dirPath weed_util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) {
|
|
return store.ListDirectoryPrefixedEntries(ctx, dirPath, startFileName, includeStartFile, limit, "", eachEntryFunc)
|
|
}
|
|
|
|
func (store *LevelDB2Store) ListDirectoryPrefixedEntries(ctx context.Context, dirPath weed_util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) {
|
|
|
|
directoryPrefix, partitionId := genDirectoryKeyPrefix(dirPath, prefix, store.dbCount)
|
|
lastFileStart := directoryPrefix
|
|
if startFileName > prefix {
|
|
lastFileStart, _ = genDirectoryKeyPrefix(dirPath, startFileName, store.dbCount)
|
|
}
|
|
|
|
iter := store.dbs[partitionId].NewIterator(&leveldb_util.Range{Start: lastFileStart}, nil)
|
|
for iter.Next() {
|
|
key := iter.Key()
|
|
if !bytes.HasPrefix(key, directoryPrefix) {
|
|
break
|
|
}
|
|
fileName := getNameFromKey(key)
|
|
if fileName == "" {
|
|
continue
|
|
}
|
|
if fileName == startFileName && !includeStartFile {
|
|
continue
|
|
}
|
|
limit--
|
|
if limit < 0 {
|
|
break
|
|
}
|
|
lastFileName = fileName
|
|
entry := &filer.Entry{
|
|
FullPath: weed_util.NewFullPath(string(dirPath), fileName),
|
|
}
|
|
|
|
// println("list", entry.FullPath, "chunks", len(entry.GetChunks()))
|
|
if decodeErr := entry.DecodeAttributesAndChunks(weed_util.MaybeDecompressData(iter.Value())); decodeErr != nil {
|
|
err = decodeErr
|
|
glog.V(0).InfofCtx(ctx, "list %s : %v", entry.FullPath, err)
|
|
break
|
|
}
|
|
|
|
resEachEntryFunc, resEachEntryFuncErr := eachEntryFunc(entry)
|
|
if resEachEntryFuncErr != nil {
|
|
err = fmt.Errorf("failed to process eachEntryFunc: %w", resEachEntryFuncErr)
|
|
break
|
|
}
|
|
|
|
if !resEachEntryFunc {
|
|
break
|
|
}
|
|
}
|
|
iter.Release()
|
|
|
|
return lastFileName, err
|
|
}
|
|
|
|
func genKey(dirPath, fileName string, dbCount int) (key []byte, partitionId int) {
|
|
key, partitionId = hashToBytes(dirPath, dbCount)
|
|
key = append(key, []byte(fileName)...)
|
|
return key, partitionId
|
|
}
|
|
|
|
func genDirectoryKeyPrefix(fullpath weed_util.FullPath, startFileName string, dbCount int) (keyPrefix []byte, partitionId int) {
|
|
keyPrefix, partitionId = hashToBytes(string(fullpath), dbCount)
|
|
if len(startFileName) > 0 {
|
|
keyPrefix = append(keyPrefix, []byte(startFileName)...)
|
|
}
|
|
return keyPrefix, partitionId
|
|
}
|
|
|
|
func getNameFromKey(key []byte) string {
|
|
|
|
return string(key[md5.Size:])
|
|
|
|
}
|
|
|
|
// hash directory, and use last byte for partitioning
|
|
func hashToBytes(dir string, dbCount int) ([]byte, int) {
|
|
h := md5.New()
|
|
io.WriteString(h, dir)
|
|
|
|
b := h.Sum(nil)
|
|
|
|
x := b[len(b)-1]
|
|
|
|
return b, int(x) % dbCount
|
|
}
|
|
|
|
func (store *LevelDB2Store) Shutdown() {
|
|
for d := 0; d < store.dbCount; d++ {
|
|
store.dbs[d].Close()
|
|
}
|
|
}
|