+149
-72
@@ -12,6 +12,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
@@ -908,92 +909,167 @@ type channelTestSummary struct {
|
||||
Enabled int `json:"enabled"`
|
||||
}
|
||||
|
||||
// performChannelTests runs the channel test loop synchronously, honoring ctx
|
||||
// cancellation so a system-task runner that loses its lease stops promptly. When
|
||||
// report is non-nil it is called after each channel with (processed, total) so
|
||||
// the system task can surface progress.
|
||||
func performChannelTests(ctx context.Context, channels []*model.Channel, testUserID int, allowDisable bool, report func(processed, total int)) channelTestSummary {
|
||||
func testChannelForHealthCheck(ctx context.Context, channel *model.Channel, testUserID int, allowDisable bool, disableThreshold int64) channelTestSummary {
|
||||
summary := channelTestSummary{}
|
||||
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
||||
if disableThreshold == 0 {
|
||||
disableThreshold = 10000000 // a impossible value
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
result := testChannel(ctx, channel, testUserID, "", "", shouldUseStreamForAutomaticChannelTest(channel))
|
||||
milliseconds := time.Since(tik).Milliseconds()
|
||||
if ctx.Err() != nil {
|
||||
return summary
|
||||
}
|
||||
|
||||
summary.Tested++
|
||||
|
||||
shouldBanChannel := false
|
||||
newAPIError := result.newAPIError
|
||||
if newAPIError != nil {
|
||||
shouldBanChannel = service.ShouldDisableChannel(result.newAPIError)
|
||||
}
|
||||
|
||||
if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
|
||||
if milliseconds > disableThreshold {
|
||||
err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
|
||||
newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout)
|
||||
shouldBanChannel = true
|
||||
}
|
||||
}
|
||||
|
||||
if newAPIError == nil {
|
||||
summary.Succeeded++
|
||||
} else {
|
||||
summary.Failed++
|
||||
}
|
||||
|
||||
if allowDisable && isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
|
||||
processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
summary.Disabled++
|
||||
}
|
||||
|
||||
if result.localErr == nil && !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
|
||||
service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
|
||||
summary.Enabled++
|
||||
}
|
||||
|
||||
channel.UpdateResponseTime(milliseconds)
|
||||
return summary
|
||||
}
|
||||
|
||||
// runChannelTestWorkers executes independent channel tests with bounded
|
||||
// concurrency. Results and progress are reduced by the caller goroutine, so
|
||||
// summary counts and the progress reporter remain serialized.
|
||||
func runChannelTestWorkers(
|
||||
ctx context.Context,
|
||||
channels []*model.Channel,
|
||||
concurrency int,
|
||||
run func(context.Context, *model.Channel) channelTestSummary,
|
||||
report func(processed, total int),
|
||||
) channelTestSummary {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
total := len(channels)
|
||||
for index, channel := range channels {
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if report != nil {
|
||||
report(index, total) // channels completed before this one
|
||||
}
|
||||
if channel.Status == common.ChannelStatusManuallyDisabled {
|
||||
continue
|
||||
}
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
result := testChannel(ctx, channel, testUserID, "", "", shouldUseStreamForAutomaticChannelTest(channel))
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if report != nil {
|
||||
report(0, total)
|
||||
}
|
||||
if total == 0 {
|
||||
return channelTestSummary{}
|
||||
}
|
||||
|
||||
summary.Tested++
|
||||
workerCount := min(operation_setting.NormalizeChannelTestConcurrency(concurrency), total)
|
||||
jobs := make(chan *model.Channel)
|
||||
results := make(chan channelTestSummary)
|
||||
|
||||
shouldBanChannel := false
|
||||
newAPIError := result.newAPIError
|
||||
// request error disables the channel
|
||||
if newAPIError != nil {
|
||||
shouldBanChannel = service.ShouldDisableChannel(result.newAPIError)
|
||||
}
|
||||
|
||||
// 当错误检查通过,才检查响应时间
|
||||
if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
|
||||
if milliseconds > disableThreshold {
|
||||
err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
|
||||
newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout)
|
||||
shouldBanChannel = true
|
||||
}
|
||||
}
|
||||
|
||||
if newAPIError == nil {
|
||||
summary.Succeeded++
|
||||
} else {
|
||||
summary.Failed++
|
||||
}
|
||||
|
||||
// disable channel
|
||||
if allowDisable && isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
|
||||
processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
|
||||
summary.Disabled++
|
||||
}
|
||||
|
||||
// enable channel
|
||||
if result.localErr == nil && !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
|
||||
service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
|
||||
summary.Enabled++
|
||||
}
|
||||
|
||||
channel.UpdateResponseTime(milliseconds)
|
||||
if common.RequestInterval > 0 {
|
||||
if ctx == nil {
|
||||
time.Sleep(common.RequestInterval)
|
||||
} else {
|
||||
var workers sync.WaitGroup
|
||||
workers.Add(workerCount)
|
||||
for range workerCount {
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return summary
|
||||
case <-time.After(common.RequestInterval):
|
||||
return
|
||||
case channel, ok := <-jobs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := channelTestSummary{}
|
||||
if channel != nil && channel.Status != common.ChannelStatusManuallyDisabled {
|
||||
result = run(ctx, channel)
|
||||
}
|
||||
|
||||
results <- result
|
||||
|
||||
if common.RequestInterval > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(common.RequestInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(jobs)
|
||||
for _, channel := range channels {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case jobs <- channel:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
workers.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
summary := channelTestSummary{}
|
||||
processed := 0
|
||||
for result := range results {
|
||||
summary.Tested += result.Tested
|
||||
summary.Succeeded += result.Succeeded
|
||||
summary.Failed += result.Failed
|
||||
summary.Disabled += result.Disabled
|
||||
summary.Enabled += result.Enabled
|
||||
processed++
|
||||
if report != nil && ctx.Err() == nil {
|
||||
report(processed, total)
|
||||
}
|
||||
}
|
||||
if report != nil && (ctx == nil || ctx.Err() == nil) {
|
||||
report(total, total) // mark complete only when the full set was tested
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// performChannelTests runs channel health checks with the configured bounded
|
||||
// concurrency and honors cancellation when a system-task runner loses its
|
||||
// lease.
|
||||
func performChannelTests(ctx context.Context, channels []*model.Channel, testUserID int, allowDisable bool, concurrency int, report func(processed, total int)) channelTestSummary {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
disableThreshold := int64(common.ChannelDisableThreshold * 1000)
|
||||
if disableThreshold == 0 {
|
||||
disableThreshold = 10000000 // an impossible value
|
||||
}
|
||||
return runChannelTestWorkers(
|
||||
ctx,
|
||||
channels,
|
||||
concurrency,
|
||||
func(ctx context.Context, channel *model.Channel) channelTestSummary {
|
||||
return testChannelForHealthCheck(ctx, channel, testUserID, allowDisable, disableThreshold)
|
||||
},
|
||||
report,
|
||||
)
|
||||
}
|
||||
|
||||
// runChannelTestTask runs one synchronous channel test cycle for the system task
|
||||
// runner (both the scheduled job and the manual "test all channels" trigger go
|
||||
// through here). It honors ctx cancellation so a runner that loses its lease
|
||||
@@ -1016,7 +1092,8 @@ func runChannelTestTask(ctx context.Context, mode string, notify bool, report fu
|
||||
}
|
||||
selected := selectChannelsForAutomaticTest(channels, mode)
|
||||
allowDisable := mode != operation_setting.ChannelTestModePassiveRecovery
|
||||
summary := performChannelTests(ctx, selected, testUserID, allowDisable, report)
|
||||
concurrency := operation_setting.GetMonitorSetting().ChannelTestConcurrency
|
||||
summary := performChannelTests(ctx, selected, testUserID, allowDisable, concurrency, report)
|
||||
if notify && (ctx == nil || ctx.Err() == nil) {
|
||||
service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user