Files
Yeuoly 888ad788bc refactor: plugin lifecycle control panel (#499)
* refactor: introduce local plugin control panel and cleanup environment setup process

* fix: args

* refactor: new local runtime

* temp: stash work for refactor on RemotePluginServer

* refactor: unify local runtime lifetime and sperate init environment process

* chore: add missing files

* stash

* refactor: local plugin lifetime control

* refactor: complete installation process of control panel

* refactor: adapt service layer to new controlpanel

* refactor: pluginManager.Install

* fix: add routine wrap to InstallServerless, avoid blocking main thread

* feat: reinstall serverless runtime

* chore: add comments to Reinstall and update confusing naming

* refactor: unify install plugin service

* refactor: add labels to debugging runtime

* refactor: add getters to plugin manager

* refactor: split install service to decode/install_task/install service

* ???

* refactor: adapt controllers

* refactor: session write

* refactor: session runtime

* Refine install task orchestration (#501)

* refactor: installing task

* refactor cluster management, decouple lifetime management and cluster

* fix cli test command

* fix: cleanup TODO comments and implement GracefulStop for instance

* feat: add logger to control panel

* fix: multiple nil references

* refactor: better lifetime control

* refactor: better cycle interval

* fix(LocalPluginRuntime): prevent returning err when it's not  error

* fix: avoid adding empty PipExtraArgs

* fix: missing errors in Environment init

* fix: add truncateMessage to avoid db explosion

* cleanup: better lifecycle management

* fix: init status at the beginning of installation

* optimize: GracefulStop for pluginInstance

* refactor: tests

* refactor: centralize routine labels (#504)

* cleanup: RoutineKey

* fix: init routine pool

* fix: correctly handle cluster register error

* fix: memory leak

* fix: add \n to instance write

* fix(installer.go): set success to true after succeed for defer func

* refactor

* fix: missing cwd in testutils

* fix: scaleup default runtime nums to 1 when testing

* fix: localruntime appconfig in testing module

* Update internal/core/local_runtime/load_balancing.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: more efficiency implement in installer_local.go

* fix: returns after failing in onDebuggingRuntimeDisconnected

* fix: returns after failing in onDebuggingRuntimeDisconnected

* fix: splits tests

* refactor: naming

* refactor: manifest.VersionX

* fix: adapt SetDefault to tests

* fix: enforce use constants in DBType

* fix: generate

* fix: linter

* cleanup tests

* refactor: change  package to

* cleanup: useless codes

* Update internal/cluster/plugin.go

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* cleanup

* refactor: decouple connection_key management from debugging_time

* refactor: confused naming

* feat: recycle resources to adapt to https://github.com/langgenius/dify-plugin-daemon/pull/500

* refactor: confusing redirecting

* fix: support get serverless runtime

* fix: race condition in Launching

* fix: avoid ManifestValidate in first step of debugging handshake

* fix: adding ReleaseAllLocks to finalizers

* wtf: what a beautiful code

* refactor: rename Stream.Async to Stream.Process

* fix: kill process if daed instance was detected

* fix: correctly handle failures

* fix: consistence of difference interfaces

* fix: add stacktrace to panic

* fix: only trigger once  event

* fix: ensure plugin runtime was shutdown

* feat: cleanup install tasks

* fix: add scale logs

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-18 17:28:02 +08:00

580 lines
12 KiB
Go

package cache
import (
"context"
"crypto/tls"
"errors"
"strings"
"sync"
"time"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/log"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/parser"
"github.com/redis/go-redis/v9"
)
var (
client redis.UniversalClient
ctx = context.Background()
ErrDBNotInit = errors.New("redis client not init")
ErrNotFound = errors.New("key not found")
)
func getRedisOptions(addr, username, password string, useSsl bool, db int) *redis.Options {
opts := &redis.Options{
Addr: addr,
Username: username,
Password: password,
DB: db,
}
if useSsl {
opts.TLSConfig = &tls.Config{}
}
return opts
}
func InitRedisClient(addr, username, password string, useSsl bool, db int) error {
opts := getRedisOptions(addr, username, password, useSsl, db)
client = redis.NewClient(opts)
if _, err := client.Ping(ctx).Result(); err != nil {
return err
}
return nil
}
func InitRedisSentinelClient(sentinels []string, masterName, username, password, sentinelUsername, sentinelPassword string, useSsl bool, db int, socketTimeout float64) error {
opts := &redis.FailoverOptions{
MasterName: masterName,
SentinelAddrs: sentinels,
Username: username,
Password: password,
DB: db,
SentinelUsername: sentinelUsername,
SentinelPassword: sentinelPassword,
}
if useSsl {
opts.TLSConfig = &tls.Config{}
}
if socketTimeout > 0 {
opts.DialTimeout = time.Duration(socketTimeout * float64(time.Second))
}
client = redis.NewFailoverClient(opts)
if _, err := client.Ping(ctx).Result(); err != nil {
return err
}
return nil
}
// Close the redis client
func Close() error {
if client == nil {
return ErrDBNotInit
}
return client.Close()
}
func getCmdable(context ...redis.Cmdable) redis.Cmdable {
if len(context) > 0 {
return context[0]
}
return client
}
func serialKey(keys ...string) string {
return strings.Join(append(
[]string{"plugin_daemon"},
keys...,
), ":")
}
// Store the key-value pair
func Store(key string, value any, time time.Duration, context ...redis.Cmdable) error {
return store(serialKey(key), value, time, context...)
}
// store the key-value pair, without serialKey
func store(key string, value any, time time.Duration, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
if _, ok := value.(string); !ok {
var err error
value, err = parser.MarshalCBOR(value)
if err != nil {
return err
}
}
return getCmdable(context...).Set(ctx, key, value, time).Err()
}
// Get the value with key
func Get[T any](key string, context ...redis.Cmdable) (*T, error) {
return get[T](serialKey(key), context...)
}
func get[T any](key string, context ...redis.Cmdable) (*T, error) {
if client == nil {
return nil, ErrDBNotInit
}
val, err := getCmdable(context...).Get(ctx, key).Bytes()
if err != nil {
if err == redis.Nil {
return nil, ErrNotFound
}
return nil, err
}
if len(val) == 0 {
return nil, ErrNotFound
}
result, err := parser.UnmarshalCBOR[T](val)
return &result, err
}
// GetString get the string with key
func GetString(key string, context ...redis.Cmdable) (string, error) {
if client == nil {
return "", ErrDBNotInit
}
v, err := getCmdable(context...).Get(ctx, serialKey(key)).Result()
if err != nil {
if err == redis.Nil {
return "", ErrNotFound
}
}
return v, err
}
// Del the key
func Del(key string, context ...redis.Cmdable) (int64, error) {
return del(serialKey(key), context...)
}
func del(key string, context ...redis.Cmdable) (int64, error) {
if client == nil {
return 0, ErrDBNotInit
}
v, err := getCmdable(context...).Del(ctx, key).Result()
return v, err
}
// Exist check the key exist or not
func Exist(key string, context ...redis.Cmdable) (int64, error) {
if client == nil {
return 0, ErrDBNotInit
}
return getCmdable(context...).Exists(ctx, serialKey(key)).Result()
}
// Increase the key
func Increase(key string, context ...redis.Cmdable) (int64, error) {
if client == nil {
return 0, ErrDBNotInit
}
num, err := getCmdable(context...).Incr(ctx, serialKey(key)).Result()
if err != nil {
if err == redis.Nil {
return 0, ErrNotFound
}
return 0, err
}
return num, nil
}
// Decrease the key
func Decrease(key string, context ...redis.Cmdable) (int64, error) {
if client == nil {
return 0, ErrDBNotInit
}
return getCmdable(context...).Decr(ctx, serialKey(key)).Result()
}
// SetExpire set the expire time for the key
func SetExpire(key string, time time.Duration, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
return getCmdable(context...).Expire(ctx, serialKey(key), time).Err()
}
// SetMapField set the map field with key
func SetMapField(key string, v map[string]any, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
return getCmdable(context...).HMSet(ctx, serialKey(key), v).Err()
}
// SetMapOneField set the map field with key
func SetMapOneField(key string, field string, value any, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
if _, ok := value.(string); !ok {
value = parser.MarshalJson(value)
}
return getCmdable(context...).HSet(ctx, serialKey(key), field, value).Err()
}
// GetMapField get the map field with key
func GetMapField[T any](key string, field string, context ...redis.Cmdable) (*T, error) {
if client == nil {
return nil, ErrDBNotInit
}
val, err := getCmdable(context...).HGet(ctx, serialKey(key), field).Result()
if err != nil {
if err == redis.Nil {
return nil, ErrNotFound
}
return nil, err
}
result, err := parser.UnmarshalJson[T](val)
return &result, err
}
// GetMapFieldString get the string
func GetMapFieldString(key string, field string, context ...redis.Cmdable) (string, error) {
if client == nil {
return "", ErrDBNotInit
}
val, err := getCmdable(context...).HGet(ctx, serialKey(key), field).Result()
if err != nil {
if err == redis.Nil {
return "", ErrNotFound
}
return "", err
}
return val, nil
}
// DelMapField delete the map field with key
func DelMapField(key string, field string, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
return getCmdable(context...).HDel(ctx, serialKey(key), field).Err()
}
// GetMap get the map with key
func GetMap[V any](key string, context ...redis.Cmdable) (map[string]V, error) {
if client == nil {
return nil, ErrDBNotInit
}
val, err := getCmdable(context...).HGetAll(ctx, serialKey(key)).Result()
if err != nil {
if err == redis.Nil {
return nil, ErrNotFound
}
return nil, err
}
result := make(map[string]V)
for k, v := range val {
value, err := parser.UnmarshalJson[V](v)
if err != nil {
continue
}
result[k] = value
}
return result, nil
}
// ScanKeys scan the keys with match pattern
func ScanKeys(match string, context ...redis.Cmdable) ([]string, error) {
if client == nil {
return nil, ErrDBNotInit
}
result := make([]string, 0)
if err := ScanKeysAsync(match, func(keys []string) error {
result = append(result, keys...)
return nil
}); err != nil {
return nil, err
}
return result, nil
}
// ScanKeysAsync scan the keys with match pattern, format like "key*"
func ScanKeysAsync(match string, fn func([]string) error, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
cursor := uint64(0)
for {
keys, newCursor, err := getCmdable(context...).Scan(ctx, cursor, match, 32).Result()
if err != nil {
return err
}
if err := fn(keys); err != nil {
return err
}
if newCursor == 0 {
break
}
cursor = newCursor
}
return nil
}
// ScanMap scan the map with match pattern, format like "key*"
func ScanMap[V any](key string, match string, context ...redis.Cmdable) (map[string]V, error) {
if client == nil {
return nil, ErrDBNotInit
}
result := make(map[string]V)
ScanMapAsync[V](key, match, func(m map[string]V) error {
for k, v := range m {
result[k] = v
}
return nil
})
return result, nil
}
// ScanMapAsync scan the map with match pattern, format like "key*"
func ScanMapAsync[V any](key string, match string, fn func(map[string]V) error, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
cursor := uint64(0)
for {
kvs, newCursor, err := getCmdable(context...).
HScan(ctx, serialKey(key), cursor, match, 32).
Result()
if err != nil {
return err
}
result := make(map[string]V)
for i := 0; i < len(kvs); i += 2 {
value, err := parser.UnmarshalJson[V](kvs[i+1])
if err != nil {
continue
}
result[kvs[i]] = value
}
if err := fn(result); err != nil {
return err
}
if newCursor == 0 {
break
}
cursor = newCursor
}
return nil
}
// SetNX set the key-value pair with expire time
func SetNX[T any](key string, value T, expire time.Duration, context ...redis.Cmdable) (bool, error) {
if client == nil {
return false, ErrDBNotInit
}
// marshal the value
bytes, err := parser.MarshalCBOR(value)
if err != nil {
return false, err
}
return getCmdable(context...).SetNX(ctx, serialKey(key), bytes, expire).Result()
}
var (
ErrLockTimeout = errors.New("lock timeout")
)
var (
distributedLocks = sync.Map{}
)
// Lock key, expire time takes responsibility for expiration time
// try_lock_timeout takes responsibility for the timeout of trying to lock
func Lock(key string, expire time.Duration, tryLockTimeout time.Duration, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
const LOCK_DURATION = 20 * time.Millisecond
ticker := time.NewTicker(LOCK_DURATION)
defer ticker.Stop()
for range ticker.C {
if success, err := getCmdable(context...).SetNX(ctx, serialKey(key), "1", expire).Result(); err != nil {
return err
} else if success {
distributedLocks.Store(key, true)
return nil
}
tryLockTimeout -= LOCK_DURATION
if tryLockTimeout <= 0 {
return ErrLockTimeout
}
}
return nil
}
func Unlock(key string, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
if err := getCmdable(context...).Del(ctx, serialKey(key)).Err(); err != nil {
return err
}
distributedLocks.Delete(key)
return nil
}
// ReleaseAllLocks release all locks
func ReleaseAllLocks() error {
if client == nil {
// redis client not initialized, skip, no need to release any locks
return nil
}
distributedLocks.Range(func(key, value any) bool {
if err := Unlock(key.(string)); err != nil {
return false
}
return true
})
return nil
}
func Expire(key string, time time.Duration, context ...redis.Cmdable) (bool, error) {
if client == nil {
return false, ErrDBNotInit
}
return getCmdable(context...).Expire(ctx, serialKey(key), time).Result()
}
func Transaction(fn func(redis.Pipeliner) error) error {
if client == nil {
return ErrDBNotInit
}
return client.Watch(ctx, func(tx *redis.Tx) error {
_, err := tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
return fn(p)
})
if err == redis.Nil {
return nil
}
return err
})
}
func Publish(channel string, message any, context ...redis.Cmdable) error {
if client == nil {
return ErrDBNotInit
}
if _, ok := message.(string); !ok {
message = parser.MarshalJson(message)
}
return getCmdable(context...).Publish(ctx, channel, message).Err()
}
func Subscribe[T any](channel string) (<-chan T, func()) {
pubsub := client.Subscribe(ctx, channel)
ch := make(chan T)
connectionEstablished := make(chan bool)
go func() {
defer close(ch)
defer close(connectionEstablished)
alive := true
for alive {
iface, err := pubsub.Receive(context.Background())
if err != nil {
log.Error("failed to receive message from redis: %s, will retry in 1 second", err.Error())
time.Sleep(1 * time.Second)
continue
}
switch data := iface.(type) {
case *redis.Subscription:
connectionEstablished <- true
case *redis.Message:
v, err := parser.UnmarshalJson[T](data.Payload)
if err != nil {
continue
}
ch <- v
case *redis.Pong:
default:
alive = false
}
}
}()
// wait for the connection to be established
<-connectionEstablished
return ch, func() {
pubsub.Close()
}
}