mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 08:06:37 +08:00
feat(pages): 支持 Remote 部署源同步
新增部署源配置与运行态模型、安全下载、租约续期、原子激活和失败补偿。 接入内部任务与脱敏前端交互,并阻止数据库 Trace 和日志展开敏感查询参数。
This commit is contained in:
@@ -428,7 +428,7 @@ func notifyTaskCompleted(ctx context.Context, execution *model.TaskExecution, re
|
||||
}
|
||||
|
||||
func shouldFlushTaskExecutionLog(ctx context.Context, execErr error) bool {
|
||||
if execErr == nil {
|
||||
if isTerminalTaskExecutionError(execErr) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -440,6 +440,10 @@ func shouldFlushTaskExecutionLog(ctx context.Context, execErr error) bool {
|
||||
return retryCount >= maxRetry
|
||||
}
|
||||
|
||||
func isTerminalTaskExecutionError(execErr error) bool {
|
||||
return execErr == nil || errors.Is(execErr, asynq.SkipRetry)
|
||||
}
|
||||
|
||||
func handleFailedTask(ctx context.Context, execution *model.TaskExecution, t *asynq.Task, duration time.Duration, execErr error, span trace.Span) {
|
||||
execution.Status = model.TaskExecutionStatusFailed
|
||||
execution.ErrorMessage = execErr.Error()
|
||||
|
||||
@@ -6,10 +6,12 @@ package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
"github.com/hibiken/asynq"
|
||||
@@ -284,6 +286,55 @@ func TestCompleteTaskExecutionFlushesLog(t *testing.T) {
|
||||
assert.Contains(t, found.Log, "任务执行成功")
|
||||
}
|
||||
|
||||
func TestCompleteTaskExecutionFlushesPermanentFailureLog(t *testing.T) {
|
||||
cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
execution := &model.TaskExecution{
|
||||
TaskID: "complete_permanent_flush_001",
|
||||
TaskType: testTaskType,
|
||||
TaskName: "测试任务",
|
||||
Status: model.TaskExecutionStatusRunning,
|
||||
Retryable: true,
|
||||
MaxRetry: 3,
|
||||
TriggeredBy: "manual",
|
||||
}
|
||||
err := model.CreateTaskExecution(ctx, execution)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = withTaskID(ctx, execution.TaskID)
|
||||
AppendLog(ctx, "永久失败前的日志")
|
||||
execErr := PermanentError("来源配置无效")
|
||||
|
||||
finishTime := time.Now()
|
||||
completeTaskExecution(
|
||||
ctx,
|
||||
execution,
|
||||
asynq.NewTask(testTaskType, nil),
|
||||
100*time.Millisecond,
|
||||
finishTime,
|
||||
nil,
|
||||
execErr,
|
||||
trace.SpanFromContext(ctx),
|
||||
)
|
||||
|
||||
found, err := model.GetTaskExecutionByTaskID(ctx, execution.TaskID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.TaskExecutionStatusFailed, found.Status)
|
||||
assert.Equal(t, "来源配置无效", found.ErrorMessage)
|
||||
assert.Contains(t, found.Log, "永久失败前的日志")
|
||||
assert.Contains(t, found.Log, "任务执行失败")
|
||||
keys, err := db.Redis.Keys(ctx, "*"+execution.TaskID+"*").Result()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, keys)
|
||||
}
|
||||
|
||||
func TestPermanentErrorIsTerminalForLogFlush(t *testing.T) {
|
||||
assert.True(t, isTerminalTaskExecutionError(PermanentError("配置无效")))
|
||||
assert.False(t, isTerminalTaskExecutionError(errors.New("temporary failure")))
|
||||
}
|
||||
|
||||
func TestRetryTask(t *testing.T) {
|
||||
cleanup := setupTest(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -8,6 +8,7 @@ package handlers
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/pages"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/upload"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/user"
|
||||
@@ -51,6 +52,10 @@ func Register() {
|
||||
task.RegisterHandler(openflare.UptimeKumaSyncTask, &openflare.UptimeKumaSyncHandler{})
|
||||
task.RegisterTaskMeta(openflare.UptimeKumaSyncMeta)
|
||||
|
||||
// pages source actions are only dispatched by the Pages domain API/scanner.
|
||||
task.RegisterHandler(pages.PagesSourceActionTask, &pages.SourceActionHandler{})
|
||||
task.RegisterTaskMeta(pages.PagesSourceActionMeta)
|
||||
|
||||
// tls single renew
|
||||
task.RegisterHandler(tls.SSLSingleRenewTask, &tls.SSLSingleRenewHandler{})
|
||||
task.RegisterTaskMeta(tls.SSLSingleRenewMeta)
|
||||
|
||||
@@ -31,6 +31,7 @@ type TaskMeta struct {
|
||||
MaxRetry int `json:"max_retry"`
|
||||
Queue string `json:"queue"`
|
||||
Retryable bool `json:"retryable"` // 是否支持手动重试
|
||||
InternalOnly bool `json:"-"` // 是否仅允许内部业务入口调度
|
||||
Params []TaskParam `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
@@ -51,13 +52,18 @@ func RegisterTaskMeta(meta TaskMeta) {
|
||||
dispatchableTasks = append(dispatchableTasks, meta)
|
||||
}
|
||||
|
||||
// GetDispatchableTasks 获取所有已注册的元数据列表(返回副本以避免并发并发读写冲突)
|
||||
// GetDispatchableTasks 获取允许通过通用 Admin 入口调度的元数据列表。
|
||||
func GetDispatchableTasks() []TaskMeta {
|
||||
dispatchableTasksMutex.RLock()
|
||||
defer dispatchableTasksMutex.RUnlock()
|
||||
|
||||
metas := make([]TaskMeta, len(dispatchableTasks))
|
||||
copy(metas, dispatchableTasks)
|
||||
metas := make([]TaskMeta, 0, len(dispatchableTasks))
|
||||
for _, meta := range dispatchableTasks {
|
||||
if meta.InternalOnly {
|
||||
continue
|
||||
}
|
||||
metas = append(metas, meta)
|
||||
}
|
||||
return metas
|
||||
}
|
||||
|
||||
|
||||
@@ -29,3 +29,28 @@ func TestDuplicateTaskMeta(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalOnlyTaskMetaIsHiddenFromDispatchableTasks(t *testing.T) {
|
||||
const taskType = "test_internal_only_meta"
|
||||
meta := task.TaskMeta{
|
||||
Type: taskType,
|
||||
AsynqTask: "test:internal_only_meta",
|
||||
Name: "内部测试任务",
|
||||
InternalOnly: true,
|
||||
}
|
||||
task.RegisterTaskMeta(meta)
|
||||
|
||||
registered := task.GetTaskMeta(taskType)
|
||||
if registered == nil {
|
||||
t.Fatal("GetTaskMeta() did not return internal-only metadata")
|
||||
}
|
||||
if !registered.InternalOnly {
|
||||
t.Fatal("GetTaskMeta() lost InternalOnly flag")
|
||||
}
|
||||
|
||||
for _, dispatchable := range task.GetDispatchableTasks() {
|
||||
if dispatchable.Type == taskType {
|
||||
t.Fatalf("GetDispatchableTasks() exposed internal-only task %q", taskType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
const defaultPermanentErrorMessage = "任务无法继续执行"
|
||||
|
||||
type permanentTaskError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
// PermanentError marks a safe domain message as a non-retryable task failure.
|
||||
// It intentionally accepts no underlying error so Error never exposes provider,
|
||||
// URL, header, response-body, or other sensitive implementation details.
|
||||
func PermanentError(message string) error {
|
||||
message = strings.TrimSpace(message)
|
||||
if message == "" {
|
||||
message = defaultPermanentErrorMessage
|
||||
}
|
||||
return &permanentTaskError{message: message}
|
||||
}
|
||||
|
||||
func (e *permanentTaskError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (e *permanentTaskError) Unwrap() error {
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPermanentErrorSkipsRetryWithoutExposingAsynqMessage(t *testing.T) {
|
||||
err := PermanentError(" 来源配置无效 ")
|
||||
|
||||
assert.True(t, errors.Is(err, asynq.SkipRetry))
|
||||
assert.Equal(t, "来源配置无效", err.Error())
|
||||
assert.NotContains(t, err.Error(), asynq.SkipRetry.Error())
|
||||
}
|
||||
|
||||
func TestPermanentErrorUsesSafeFallbackForBlankMessage(t *testing.T) {
|
||||
err := PermanentError(" ")
|
||||
|
||||
assert.True(t, errors.Is(err, asynq.SkipRetry))
|
||||
assert.Equal(t, defaultPermanentErrorMessage, err.Error())
|
||||
}
|
||||
Reference in New Issue
Block a user