mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 17:30: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>
467 lines
11 KiB
Go
467 lines
11 KiB
Go
//go:build tikv
|
|
|
|
package tikv
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha1"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/tikv/client-go/v2/config"
|
|
"github.com/tikv/client-go/v2/txnkv"
|
|
)
|
|
|
|
const defaultBatchCommitSize = 10000
|
|
|
|
var (
|
|
_ filer.FilerStore = ((*TikvStore)(nil))
|
|
)
|
|
|
|
func init() {
|
|
filer.Stores = append(filer.Stores, &TikvStore{})
|
|
}
|
|
|
|
type TikvStore struct {
|
|
client *txnkv.Client
|
|
onePC bool
|
|
batchCommitSize int
|
|
keyPrefix []byte
|
|
}
|
|
|
|
// Basic APIs
|
|
func (store *TikvStore) GetName() string {
|
|
return "tikv"
|
|
}
|
|
|
|
func (store *TikvStore) Initialize(config util.Configuration, prefix string) error {
|
|
ca := config.GetString(prefix + "ca_path")
|
|
cert := config.GetString(prefix + "cert_path")
|
|
key := config.GetString(prefix + "key_path")
|
|
verify_cn := strings.Split(config.GetString(prefix+"verify_cn"), ",")
|
|
pdAddrs := strings.Split(config.GetString(prefix+"pdaddrs"), ",")
|
|
keyPrefix := config.GetString(prefix + "keyPrefix")
|
|
|
|
bdc := config.GetInt(prefix + "batchdelete_count")
|
|
if bdc <= 0 {
|
|
bdc = defaultBatchCommitSize
|
|
}
|
|
|
|
store.onePC = config.GetBool(prefix + "enable_1pc")
|
|
store.batchCommitSize = bdc
|
|
store.keyPrefix = []byte(keyPrefix)
|
|
return store.initialize(ca, cert, key, verify_cn, pdAddrs)
|
|
}
|
|
|
|
func (store *TikvStore) initialize(ca, cert, key string, verify_cn, pdAddrs []string) error {
|
|
config.UpdateGlobal(func(conf *config.Config) {
|
|
conf.Security = config.NewSecurity(ca, cert, key, verify_cn)
|
|
})
|
|
client, err := txnkv.NewClient(pdAddrs)
|
|
store.client = client
|
|
return err
|
|
}
|
|
|
|
func (store *TikvStore) Shutdown() {
|
|
err := store.client.Close()
|
|
if err != nil {
|
|
glog.V(0).Infof("Shutdown TiKV client got error: %v", err)
|
|
}
|
|
}
|
|
|
|
// ~ Basic APIs
|
|
|
|
// Entry APIs
|
|
func (store *TikvStore) InsertEntry(ctx context.Context, entry *filer.Entry) error {
|
|
dir, name := entry.DirAndName()
|
|
key := store.generateKey(dir, name)
|
|
|
|
value, err := entry.EncodeAttributesAndChunks()
|
|
if err != nil {
|
|
return fmt.Errorf("encoding %s %+v: %v", entry.FullPath, entry.Attr, err)
|
|
}
|
|
txn, err := store.getTxn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = txn.RunInTxn(ctx, func(txn *txnkv.KVTxn) error {
|
|
return txn.Set(key, value)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("persisting %s : %v", entry.FullPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *TikvStore) UpdateEntry(ctx context.Context, entry *filer.Entry) error {
|
|
return store.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
func (store *TikvStore) FindEntry(ctx context.Context, path util.FullPath) (*filer.Entry, error) {
|
|
dir, name := path.DirAndName()
|
|
key := store.generateKey(dir, name)
|
|
|
|
txn, err := store.getTxn(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var value []byte = nil
|
|
err = txn.RunInTxn(ctx, func(txn *txnkv.KVTxn) error {
|
|
val, err := txn.Get(ctx, key)
|
|
if err == nil {
|
|
value = val
|
|
}
|
|
return err
|
|
})
|
|
|
|
if isNotExists(err) || value == nil {
|
|
return nil, filer_pb.ErrNotFound
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get %s : %v", path, err)
|
|
}
|
|
|
|
entry := &filer.Entry{
|
|
FullPath: path,
|
|
}
|
|
err = entry.DecodeAttributesAndChunks(value)
|
|
if err != nil {
|
|
return entry, fmt.Errorf("decode %s : %v", entry.FullPath, err)
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
func (store *TikvStore) DeleteEntry(ctx context.Context, path util.FullPath) error {
|
|
dir, name := path.DirAndName()
|
|
key := store.generateKey(dir, name)
|
|
|
|
txn, err := store.getTxn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = txn.RunInTxn(ctx, func(txn *txnkv.KVTxn) error {
|
|
return txn.Delete(key)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s : %v", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ~ Entry APIs
|
|
|
|
// Directory APIs
|
|
func (store *TikvStore) DeleteFolderChildren(ctx context.Context, path util.FullPath) error {
|
|
directoryPrefix := store.genDirectoryKeyPrefix(path, "")
|
|
|
|
iterTxn, err := store.getTxn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !iterTxn.inContext {
|
|
defer func() {
|
|
_ = iterTxn.Rollback()
|
|
}()
|
|
}
|
|
|
|
iter, err := iterTxn.Iter(directoryPrefix, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer iter.Close()
|
|
|
|
var keys [][]byte
|
|
|
|
flush := func() error {
|
|
if len(keys) == 0 {
|
|
return nil
|
|
}
|
|
if err := store.deleteBatch(ctx, keys); err != nil {
|
|
return fmt.Errorf("delete batch in %s, error: %w", path, err)
|
|
}
|
|
keys = keys[:0]
|
|
return nil
|
|
}
|
|
|
|
for iter.Valid() {
|
|
key := iter.Key()
|
|
if !bytes.HasPrefix(key, directoryPrefix) {
|
|
break
|
|
}
|
|
|
|
keys = append(keys, append([]byte(nil), key...))
|
|
|
|
if len(keys) >= store.batchCommitSize {
|
|
if err := flush(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := iter.Next(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return flush()
|
|
}
|
|
|
|
func (store *TikvStore) deleteBatch(ctx context.Context, keys [][]byte) error {
|
|
deleteTxn, err := store.getTxn(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !deleteTxn.inContext {
|
|
defer func() { _ = deleteTxn.Rollback() }()
|
|
}
|
|
|
|
for _, key := range keys {
|
|
if err := deleteTxn.Delete(key); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if !deleteTxn.inContext {
|
|
return deleteTxn.Commit(ctx)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *TikvStore) ListDirectoryEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc filer.ListEachEntryFunc) (string, error) {
|
|
return store.ListDirectoryPrefixedEntries(ctx, dirPath, startFileName, includeStartFile, limit, "", eachEntryFunc)
|
|
}
|
|
|
|
func (store *TikvStore) ListDirectoryPrefixedEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (string, error) {
|
|
lastFileName := ""
|
|
directoryPrefix := store.genDirectoryKeyPrefix(dirPath, prefix)
|
|
lastFileStart := directoryPrefix
|
|
if startFileName > prefix {
|
|
lastFileStart = store.genDirectoryKeyPrefix(dirPath, startFileName)
|
|
}
|
|
|
|
txn, err := store.getTxn(ctx)
|
|
if err != nil {
|
|
return lastFileName, err
|
|
}
|
|
var callbackErr error
|
|
err = txn.RunInTxn(ctx, func(txn *txnkv.KVTxn) error {
|
|
iter, err := txn.Iter(lastFileStart, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer iter.Close()
|
|
i := int64(0)
|
|
for iter.Valid() {
|
|
key := iter.Key()
|
|
if !bytes.HasPrefix(key, directoryPrefix) {
|
|
break
|
|
}
|
|
fileName := store.getNameFromKey(key)
|
|
if fileName == "" {
|
|
if err := iter.Next(); err != nil {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
if fileName == startFileName && !includeStartFile {
|
|
if err := iter.Next(); err != nil {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check limit only before processing valid entries
|
|
if limit > 0 && i >= limit {
|
|
break
|
|
}
|
|
|
|
lastFileName = fileName
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(dirPath), fileName),
|
|
}
|
|
|
|
// println("list", entry.FullPath, "chunks", len(entry.GetChunks()))
|
|
if decodeErr := entry.DecodeAttributesAndChunks(util.MaybeDecompressData(iter.Value())); decodeErr != nil {
|
|
err = decodeErr
|
|
glog.V(0).InfofCtx(ctx, "list %s : %v", entry.FullPath, err)
|
|
break
|
|
}
|
|
|
|
// Check TTL expiration before calling eachEntryFunc (similar to Redis stores)
|
|
if entry.TtlSec > 0 {
|
|
if entry.Crtime.Add(time.Duration(entry.TtlSec) * time.Second).Before(time.Now()) {
|
|
// Entry is expired, delete it and continue without counting toward limit
|
|
if deleteErr := store.DeleteEntry(ctx, entry.FullPath); deleteErr != nil {
|
|
glog.V(0).InfofCtx(ctx, "failed to delete expired entry %s: %v", entry.FullPath, deleteErr)
|
|
}
|
|
if err := iter.Next(); err != nil {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Only increment counter for non-expired entries
|
|
i++
|
|
|
|
resEachEntryFunc, resEachEntryFuncErr := eachEntryFunc(entry)
|
|
if resEachEntryFuncErr != nil {
|
|
glog.V(0).InfofCtx(ctx, "failed to process eachEntryFunc for entry %q: %v", fileName, resEachEntryFuncErr)
|
|
callbackErr = resEachEntryFuncErr
|
|
break
|
|
}
|
|
|
|
nextErr := iter.Next()
|
|
if nextErr != nil {
|
|
err = nextErr
|
|
break
|
|
}
|
|
|
|
if !resEachEntryFunc {
|
|
break
|
|
}
|
|
}
|
|
return err
|
|
})
|
|
|
|
if callbackErr != nil {
|
|
return lastFileName, fmt.Errorf(
|
|
"failed to process eachEntryFunc for dir %q, entry %q: %w",
|
|
dirPath, lastFileName, callbackErr,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return lastFileName, fmt.Errorf("prefix list %s : %v", dirPath, err)
|
|
}
|
|
return lastFileName, nil
|
|
}
|
|
|
|
// ~ Directory APIs
|
|
|
|
// Transaction Related APIs
|
|
func (store *TikvStore) BeginTransaction(ctx context.Context) (context.Context, error) {
|
|
tx, err := store.client.Begin()
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
if store.onePC {
|
|
tx.SetEnable1PC(store.onePC)
|
|
}
|
|
return context.WithValue(ctx, "tx", tx), nil
|
|
}
|
|
|
|
func (store *TikvStore) CommitTransaction(ctx context.Context) error {
|
|
if tx, ok := ctx.Value("tx").(*txnkv.KVTxn); ok {
|
|
return tx.Commit(context.Background())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (store *TikvStore) RollbackTransaction(ctx context.Context) error {
|
|
if tx, ok := ctx.Value("tx").(*txnkv.KVTxn); ok {
|
|
return tx.Rollback()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ~ Transaction Related APIs
|
|
|
|
// Transaction Wrapper
|
|
type TxnWrapper struct {
|
|
*txnkv.KVTxn
|
|
inContext bool
|
|
}
|
|
|
|
func (w *TxnWrapper) RunInTxn(ctx context.Context, f func(txn *txnkv.KVTxn) error) error {
|
|
err := f(w.KVTxn)
|
|
if !w.inContext {
|
|
if err != nil {
|
|
w.KVTxn.Rollback()
|
|
return err
|
|
}
|
|
return w.KVTxn.Commit(ctx)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (store *TikvStore) getTxn(ctx context.Context) (*TxnWrapper, error) {
|
|
if tx, ok := ctx.Value("tx").(*txnkv.KVTxn); ok {
|
|
return &TxnWrapper{tx, true}, nil
|
|
}
|
|
txn, err := store.client.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if store.onePC {
|
|
txn.SetEnable1PC(store.onePC)
|
|
}
|
|
return &TxnWrapper{txn, false}, nil
|
|
}
|
|
|
|
// ~ Transaction Wrapper
|
|
|
|
// Encoding Functions
|
|
func hashToBytes(dir string) []byte {
|
|
h := sha1.New()
|
|
io.WriteString(h, dir)
|
|
b := h.Sum(nil)
|
|
return b
|
|
}
|
|
|
|
func (store *TikvStore) getKey(key []byte) []byte {
|
|
if len(store.keyPrefix) == 0 {
|
|
return key
|
|
}
|
|
result := make([]byte, len(store.keyPrefix)+len(key))
|
|
copy(result, store.keyPrefix)
|
|
copy(result[len(store.keyPrefix):], key)
|
|
return result
|
|
}
|
|
|
|
func (store *TikvStore) generateKey(dirPath, fileName string) []byte {
|
|
key := hashToBytes(dirPath)
|
|
key = append(key, []byte(fileName)...)
|
|
return store.getKey(key)
|
|
}
|
|
|
|
func (store *TikvStore) getNameFromKey(key []byte) string {
|
|
minKeyLen := len(store.keyPrefix) + sha1.Size
|
|
if len(key) < minKeyLen {
|
|
glog.Warningf("malformed key: length %d is less than minimum %d", len(key), minKeyLen)
|
|
return ""
|
|
}
|
|
return string(key[minKeyLen:])
|
|
}
|
|
|
|
func (store *TikvStore) genDirectoryKeyPrefix(fullpath util.FullPath, startFileName string) (keyPrefix []byte) {
|
|
keyPrefix = hashToBytes(string(fullpath))
|
|
if len(startFileName) > 0 {
|
|
keyPrefix = append(keyPrefix, []byte(startFileName)...)
|
|
}
|
|
return store.getKey(keyPrefix)
|
|
}
|
|
|
|
func isNotExists(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
if err.Error() == "not exist" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ~ Encoding Functions
|