修复调度缓存异常时间阻塞

Refs Wei-Shaw/sub2api#60

Refs Wei-Shaw/sub2api#1608
This commit is contained in:
jjaw
2026-07-12 05:54:46 +08:00
parent e316ebf528
commit fe184f8c33
2 changed files with 114 additions and 21 deletions
+48 -21
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"strconv"
"time"
@@ -163,14 +164,15 @@ func (c *schedulerCache) SetSnapshot(ctx context.Context, bucket service.Schedul
versionStr := strconv.FormatInt(version, 10)
snapshotKey := schedulerSnapshotKey(bucket, versionStr)
if err := c.writeAccounts(ctx, accounts); err != nil {
cacheableAccounts, err := c.writeAccounts(ctx, accounts)
if err != nil {
return err
}
if len(accounts) > 0 {
if len(cacheableAccounts) > 0 {
// 使用序号作为 score,保持数据库返回的排序语义。
members := make([]redis.Z, 0, len(accounts))
for idx, account := range accounts {
members := make([]redis.Z, 0, len(cacheableAccounts))
for idx, account := range cacheableAccounts {
members = append(members, redis.Z{
Score: float64(idx),
Member: strconv.FormatInt(account.ID, 10),
@@ -224,7 +226,14 @@ func (c *schedulerCache) SetAccount(ctx context.Context, account *service.Accoun
if account == nil || account.ID <= 0 {
return nil
}
return c.writeAccounts(ctx, []service.Account{*account})
cacheableAccounts, err := c.writeAccounts(ctx, []service.Account{*account})
if err != nil {
return err
}
if len(cacheableAccounts) == 0 {
return c.DeleteAccount(ctx, account.ID)
}
return nil
}
func (c *schedulerCache) DeleteAccount(ctx context.Context, accountID int64) error {
@@ -262,13 +271,14 @@ func (c *schedulerCache) UpdateLastUsed(ctx context.Context, updates map[int64]t
return err
}
account.LastUsedAt = ptrTime(updates[ids[i]])
updated, err := json.Marshal(account)
updated, metaPayload, err := marshalSchedulerCacheAccount(*account)
if err != nil {
return err
}
metaPayload, err := json.Marshal(buildSchedulerMetadataAccount(*account))
if err != nil {
return err
slog.Warn("scheduler cache removes account with unencodable payload",
"account_id", ids[i],
"error", err,
)
pipe.Del(ctx, keys[i], schedulerAccountMetaKey(strconv.FormatInt(ids[i], 10)))
continue
}
pipe.Set(ctx, keys[i], updated, 0)
pipe.Set(ctx, schedulerAccountMetaKey(strconv.FormatInt(ids[i], 10)), metaPayload, 0)
@@ -359,12 +369,13 @@ func decodeCachedAccount(val any) (*service.Account, error) {
return &account, nil
}
func (c *schedulerCache) writeAccounts(ctx context.Context, accounts []service.Account) error {
func (c *schedulerCache) writeAccounts(ctx context.Context, accounts []service.Account) ([]service.Account, error) {
if len(accounts) == 0 {
return nil
return nil, nil
}
pipe := c.rdb.Pipeline()
cacheableAccounts := make([]service.Account, 0, len(accounts))
pending := 0
flush := func() error {
if pending == 0 {
@@ -379,27 +390,43 @@ func (c *schedulerCache) writeAccounts(ctx context.Context, accounts []service.A
}
for _, account := range accounts {
fullPayload, err := json.Marshal(account)
fullPayload, metaPayload, err := marshalSchedulerCacheAccount(account)
if err != nil {
return err
}
metaPayload, err := json.Marshal(buildSchedulerMetadataAccount(account))
if err != nil {
return err
slog.Warn("scheduler cache skips account with unencodable payload",
"account_id", account.ID,
"error", err,
)
continue
}
id := strconv.FormatInt(account.ID, 10)
pipe.Set(ctx, schedulerAccountKey(id), fullPayload, 0)
pipe.Set(ctx, schedulerAccountMetaKey(id), metaPayload, 0)
cacheableAccounts = append(cacheableAccounts, account)
pending++
if pending >= c.writeChunkSize {
if err := flush(); err != nil {
return err
return nil, err
}
}
}
return flush()
if err := flush(); err != nil {
return nil, err
}
return cacheableAccounts, nil
}
func marshalSchedulerCacheAccount(account service.Account) ([]byte, []byte, error) {
fullPayload, err := json.Marshal(account)
if err != nil {
return nil, nil, fmt.Errorf("marshal account: %w", err)
}
metaPayload, err := json.Marshal(buildSchedulerMetadataAccount(account))
if err != nil {
return nil, nil, fmt.Errorf("marshal account metadata: %w", err)
}
return fullPayload, metaPayload, nil
}
func (c *schedulerCache) mgetChunked(ctx context.Context, keys []string) ([]any, error) {
@@ -3,12 +3,78 @@
package repository
import (
"context"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
func newSchedulerCacheUnit(t *testing.T) *schedulerCache {
t.Helper()
mr := miniredis.RunT(t)
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() { _ = rdb.Close() })
cache, ok := newSchedulerCacheWithChunkSizes(rdb, defaultSchedulerSnapshotMGetChunkSize, defaultSchedulerSnapshotWriteChunkSize).(*schedulerCache)
require.True(t, ok)
return cache
}
func TestSchedulerCacheWriteAccountsSkipsUnencodableTimes(t *testing.T) {
ctx := context.Background()
cache := newSchedulerCacheUnit(t)
invalidTime := time.Date(10000, time.January, 1, 0, 0, 0, 0, time.UTC)
cacheable, err := cache.writeAccounts(ctx, []service.Account{
{ID: 111, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey},
{ID: 112, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey, ExpiresAt: &invalidTime},
})
require.NoError(t, err)
require.Len(t, cacheable, 1)
require.Equal(t, int64(111), cacheable[0].ID)
cached, err := cache.GetAccount(ctx, 111)
require.NoError(t, err)
require.NotNil(t, cached)
invalid, err := cache.GetAccount(ctx, 112)
require.NoError(t, err)
require.Nil(t, invalid)
}
func TestSchedulerCacheSetAccountClearsUnencodablePayload(t *testing.T) {
ctx := context.Background()
cache := newSchedulerCacheUnit(t)
account := service.Account{ID: 113, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey}
require.NoError(t, cache.SetAccount(ctx, &account))
invalidTime := time.Date(10000, time.January, 1, 0, 0, 0, 0, time.UTC)
account.ExpiresAt = &invalidTime
require.NoError(t, cache.SetAccount(ctx, &account))
cached, err := cache.GetAccount(ctx, account.ID)
require.NoError(t, err)
require.Nil(t, cached)
}
func TestSchedulerCacheUpdateLastUsedClearsUnencodablePayload(t *testing.T) {
ctx := context.Background()
cache := newSchedulerCacheUnit(t)
account := service.Account{ID: 114, Platform: service.PlatformOpenAI, Type: service.AccountTypeAPIKey}
require.NoError(t, cache.SetAccount(ctx, &account))
invalidTime := time.Date(10000, time.January, 1, 0, 0, 0, 0, time.UTC)
require.NoError(t, cache.UpdateLastUsed(ctx, map[int64]time.Time{account.ID: invalidTime}))
cached, err := cache.GetAccount(ctx, account.ID)
require.NoError(t, err)
require.Nil(t, cached)
}
func TestBuildSchedulerMetadataAccount_KeepsOpenAIWSFlags(t *testing.T) {
account := service.Account{
ID: 42,