chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/cache"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Error variables for KV storage operations
|
||||
var (
|
||||
// ErrKVKeyNotFound is returned when the requested key does not exist in the KV storage
|
||||
ErrKVKeyNotFound = fmt.Errorf("key not found in KV storage")
|
||||
// ErrKVGroupEmpty is returned when a required group name is empty
|
||||
ErrKVGroupEmpty = fmt.Errorf("group name is empty")
|
||||
// ErrKVKeyEmpty is returned when a required key name is empty
|
||||
ErrKVKeyEmpty = fmt.Errorf("key name is empty")
|
||||
// ErrKVKeyAndGroupEmpty is returned when both key and group names are empty
|
||||
ErrKVKeyAndGroupEmpty = fmt.Errorf("both key and group are empty")
|
||||
// ErrKVTransactionFailed is returned when a KV storage transaction operation fails
|
||||
ErrKVTransactionFailed = fmt.Errorf("KV storage transaction failed")
|
||||
)
|
||||
|
||||
// KVParams is the parameters for KV storage operations
|
||||
type KVParams struct {
|
||||
Group string
|
||||
Key string
|
||||
Value string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// KVOperator provides methods to interact with the key-value storage system for plugins
|
||||
type KVOperator struct {
|
||||
data *Data
|
||||
session *xorm.Session
|
||||
pluginSlugName string
|
||||
cacheTTL time.Duration
|
||||
}
|
||||
|
||||
// KVStorageOption defines a function type that configures a KVOperator
|
||||
type KVStorageOption func(*KVOperator)
|
||||
|
||||
// WithCacheTTL is the option to set the cache TTL; the default value is 30 minutes.
|
||||
// If ttl is less than 0, the cache will not be used
|
||||
func WithCacheTTL(ttl time.Duration) KVStorageOption {
|
||||
return func(kv *KVOperator) {
|
||||
kv.cacheTTL = ttl
|
||||
}
|
||||
}
|
||||
|
||||
// Option is used to set the options for the KV storage
|
||||
func (kv *KVOperator) Option(opts ...KVStorageOption) {
|
||||
for _, opt := range opts {
|
||||
opt(kv)
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *KVOperator) getSession(ctx context.Context) (*xorm.Session, func()) {
|
||||
session := kv.session
|
||||
cleanup := func() {}
|
||||
if session == nil {
|
||||
session = kv.data.DB.NewSession().Context(ctx)
|
||||
cleanup = func() {
|
||||
if session != nil {
|
||||
_ = session.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
return session, cleanup
|
||||
}
|
||||
|
||||
func (kv *KVOperator) getCacheKey(params KVParams) string {
|
||||
return fmt.Sprintf("plugin_kv_storage:%s:group:%s:key:%s", kv.pluginSlugName, params.Group, params.Key)
|
||||
}
|
||||
|
||||
func (kv *KVOperator) setCache(ctx context.Context, params KVParams) {
|
||||
if kv.cacheTTL < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ttl := kv.cacheTTL
|
||||
if ttl > 10 {
|
||||
ttl += time.Duration(float64(ttl) * 0.1 * (1 - rand.Float64()))
|
||||
}
|
||||
|
||||
cacheKey := kv.getCacheKey(params)
|
||||
if err := kv.data.Cache.SetString(ctx, cacheKey, params.Value, ttl); err != nil {
|
||||
log.Warnf("cache set failed: %v, key: %s", err, cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (kv *KVOperator) getCache(ctx context.Context, params KVParams) (string, bool, error) {
|
||||
if kv.cacheTTL < 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
cacheKey := kv.getCacheKey(params)
|
||||
return kv.data.Cache.GetString(ctx, cacheKey)
|
||||
}
|
||||
|
||||
func (kv *KVOperator) cleanCache(ctx context.Context, params KVParams) {
|
||||
if kv.cacheTTL < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err := kv.data.Cache.Del(ctx, kv.getCacheKey(params)); err != nil {
|
||||
log.Warnf("Failed to delete cache for key %s: %v", params.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from KV storage by group and key.
|
||||
// Returns the value as a string or an error if the key is not found.
|
||||
func (kv *KVOperator) Get(ctx context.Context, params KVParams) (string, error) {
|
||||
if params.Key == "" {
|
||||
return "", ErrKVKeyEmpty
|
||||
}
|
||||
|
||||
if value, exist, err := kv.getCache(ctx, params); err == nil && exist {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// query
|
||||
data := entity.PluginKVStorage{}
|
||||
query, cleanup := kv.getSession(ctx)
|
||||
defer cleanup()
|
||||
|
||||
query.Where(builder.Eq{
|
||||
"plugin_slug_name": kv.pluginSlugName,
|
||||
"`group`": params.Group,
|
||||
"`key`": params.Key,
|
||||
})
|
||||
|
||||
has, err := query.Get(&data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !has {
|
||||
return "", ErrKVKeyNotFound
|
||||
}
|
||||
|
||||
params.Value = data.Value
|
||||
kv.setCache(ctx, params)
|
||||
|
||||
return data.Value, nil
|
||||
}
|
||||
|
||||
// Set stores a value in KV storage with the specified group and key.
|
||||
// Updates the value if it already exists.
|
||||
func (kv *KVOperator) Set(ctx context.Context, params KVParams) error {
|
||||
if params.Key == "" {
|
||||
return ErrKVKeyEmpty
|
||||
}
|
||||
|
||||
query, cleanup := kv.getSession(ctx)
|
||||
defer cleanup()
|
||||
|
||||
data := &entity.PluginKVStorage{
|
||||
PluginSlugName: kv.pluginSlugName,
|
||||
Group: params.Group,
|
||||
Key: params.Key,
|
||||
Value: params.Value,
|
||||
}
|
||||
|
||||
kv.cleanCache(ctx, params)
|
||||
|
||||
affected, err := query.Where(builder.Eq{
|
||||
"plugin_slug_name": kv.pluginSlugName,
|
||||
"`group`": params.Group,
|
||||
"`key`": params.Key,
|
||||
}).Cols("value").Update(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if affected == 0 {
|
||||
_, err = query.Insert(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Del removes values from KV storage by group and/or key.
|
||||
// If both group and key are provided, only that specific entry is deleted.
|
||||
// If only group is provided, all entries in that group are deleted.
|
||||
// At least one of group or key must be provided.
|
||||
func (kv *KVOperator) Del(ctx context.Context, params KVParams) error {
|
||||
if params.Key == "" && params.Group == "" {
|
||||
return ErrKVKeyAndGroupEmpty
|
||||
}
|
||||
|
||||
kv.cleanCache(ctx, params)
|
||||
|
||||
session, cleanup := kv.getSession(ctx)
|
||||
defer cleanup()
|
||||
|
||||
session.Where(builder.Eq{
|
||||
"plugin_slug_name": kv.pluginSlugName,
|
||||
})
|
||||
if params.Group != "" {
|
||||
session.Where(builder.Eq{"`group`": params.Group})
|
||||
}
|
||||
if params.Key != "" {
|
||||
session.Where(builder.Eq{"`key`": params.Key})
|
||||
}
|
||||
|
||||
_, err := session.Delete(&entity.PluginKVStorage{})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetByGroup retrieves all key-value pairs for a specific group with pagination support.
|
||||
// Returns a map of keys to values or an error if the group is empty or not found.
|
||||
func (kv *KVOperator) GetByGroup(ctx context.Context, params KVParams) (map[string]string, error) {
|
||||
if params.Group == "" {
|
||||
return nil, ErrKVGroupEmpty
|
||||
}
|
||||
|
||||
if params.Page < 1 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.PageSize < 1 {
|
||||
params.PageSize = 10
|
||||
}
|
||||
|
||||
query, cleanup := kv.getSession(ctx)
|
||||
defer cleanup()
|
||||
|
||||
var items []entity.PluginKVStorage
|
||||
err := query.Where(builder.Eq{"plugin_slug_name": kv.pluginSlugName, "`group`": params.Group}).
|
||||
Limit(params.PageSize, (params.Page-1)*params.PageSize).
|
||||
OrderBy("id ASC").
|
||||
Find(&items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]string, len(items))
|
||||
for _, item := range items {
|
||||
result[item.Key] = item.Value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Tx executes a function within a transaction context. If the KVOperator already has a session,
|
||||
// it will use that session. Otherwise, it creates a new transaction session.
|
||||
// The transaction will be committed if the function returns nil, or rolled back if it returns an error.
|
||||
func (kv *KVOperator) Tx(ctx context.Context, fn func(ctx context.Context, kv *KVOperator) error) error {
|
||||
var (
|
||||
txKv = kv
|
||||
shouldCommit bool
|
||||
)
|
||||
|
||||
if kv.session == nil {
|
||||
session := kv.data.DB.NewSession().Context(ctx)
|
||||
if err := session.Begin(); err != nil {
|
||||
_ = session.Close()
|
||||
return fmt.Errorf("%w: begin transaction failed: %v", ErrKVTransactionFailed, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if !shouldCommit {
|
||||
if rollbackErr := session.Rollback(); rollbackErr != nil {
|
||||
log.Errorf("rollback failed: %v", rollbackErr)
|
||||
}
|
||||
}
|
||||
_ = session.Close()
|
||||
}()
|
||||
|
||||
txKv = &KVOperator{
|
||||
session: session,
|
||||
data: kv.data,
|
||||
pluginSlugName: kv.pluginSlugName,
|
||||
}
|
||||
shouldCommit = true
|
||||
}
|
||||
|
||||
if err := fn(ctx, txKv); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrKVTransactionFailed, err)
|
||||
}
|
||||
|
||||
if shouldCommit {
|
||||
if err := txKv.session.Commit(); err != nil {
|
||||
return fmt.Errorf("%w: commit failed: %v", ErrKVTransactionFailed, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KVStorage defines the interface for plugins that need data storage capabilities
|
||||
type KVStorage interface {
|
||||
Info() Info
|
||||
SetOperator(operator *KVOperator)
|
||||
}
|
||||
|
||||
var (
|
||||
CallKVStorage,
|
||||
registerKVStorage = MakePlugin[KVStorage](true)
|
||||
)
|
||||
|
||||
// NewKVOperator creates a new KV storage operator with the specified database engine, cache and plugin name.
|
||||
// It returns a KVOperator instance that can be used to interact with the plugin's storage.
|
||||
func NewKVOperator(db *xorm.Engine, cache cache.Cache, pluginSlugName string) *KVOperator {
|
||||
return &KVOperator{
|
||||
data: &Data{DB: db, Cache: cache},
|
||||
pluginSlugName: pluginSlugName,
|
||||
cacheTTL: 30 * time.Minute,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user