feat: add precise task business key and creation time queries
This commit is contained in:
@@ -33,7 +33,7 @@ type Task struct {
|
||||
PolicySnapshot core.JSON `db:"policy_snapshot"`
|
||||
// RecoverySafety 恢复安全等级;未知值按需要核查处理,不能默认自动重跑。
|
||||
RecoverySafety RecoverySafety `db:"recovery_safety"`
|
||||
// BusinessKey 业务侧关联键,用于检索追踪,不自动提供业务幂等;可为空,表示尚未设置该关联或值。
|
||||
// BusinessKey 可空业务关联键,按精确Unicode值检索,区分大小写及尾部空格;不自动提供业务幂等。
|
||||
BusinessKey *string `db:"business_key"`
|
||||
// ConcurrencyKey 显式互斥资源键,命名空间内跨队列共享;空表示不启用。
|
||||
ConcurrencyKey *string `db:"concurrency_key"`
|
||||
|
||||
@@ -132,6 +132,21 @@ func (s *Service) ListTasks(ctx context.Context, token auth.Token, in ListInput)
|
||||
default:
|
||||
return TaskPage{}, fault.Invalid("invalid task state")
|
||||
}
|
||||
if !utf8.ValidString(in.BusinessKey) || utf8.RuneCountInString(in.BusinessKey) > 191 || strings.ContainsAny(in.BusinessKey, "\x00\r\n") {
|
||||
return TaskPage{}, fault.Invalid("business_key must be at most 191 characters without control newlines")
|
||||
}
|
||||
for _, field := range []**time.Time{&in.CreatedFrom, &in.CreatedBefore} {
|
||||
if *field != nil {
|
||||
v := (**field).UTC()
|
||||
if v.Year() < 2000 || v.Year() > 9998 || v.Nanosecond()%1000 != 0 {
|
||||
return TaskPage{}, fault.Invalid("creation bounds must use supported years and microsecond precision")
|
||||
}
|
||||
*field = &v
|
||||
}
|
||||
}
|
||||
if in.CreatedFrom != nil && in.CreatedBefore != nil && !in.CreatedFrom.Before(*in.CreatedBefore) {
|
||||
return TaskPage{}, fault.Invalid("created_from must be before created_before")
|
||||
}
|
||||
return s.store.ListTasks(ctx, token, in)
|
||||
}
|
||||
func (s *Service) CancelTask(ctx context.Context, token auth.Token, id core.ID, key string, in CancelInput) (CancelResult, error) {
|
||||
|
||||
56
pulse/service/task_query_test.go
Normal file
56
pulse/service/task_query_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/fault"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
||||
)
|
||||
|
||||
type queryValidationStore struct {
|
||||
Store
|
||||
calls int
|
||||
input ListInput
|
||||
}
|
||||
|
||||
func (s *queryValidationStore) ListTasks(_ context.Context, _ auth.Token, in ListInput) (TaskPage, error) {
|
||||
s.calls++
|
||||
s.input = in
|
||||
return TaskPage{}, nil
|
||||
}
|
||||
func TestTaskQueryBoundsAndNormalization(t *testing.T) {
|
||||
store := &queryValidationStore{}
|
||||
api := New(store)
|
||||
from := time.Date(2026, 9, 17, 12, 0, 0, 123456000, time.FixedZone("example", 8*3600))
|
||||
before := from.Add(time.Hour)
|
||||
base := ListInput{QueueID: core.ID(strings.Repeat("1", 32)), BusinessKey: "订单 /+% é ", CreatedFrom: &from, CreatedBefore: &before}
|
||||
for name, change := range map[string]func(*ListInput){
|
||||
"bad_utf8": func(v *ListInput) { v.BusinessKey = "\xff" }, "long_key": func(v *ListInput) { v.BusinessKey = strings.Repeat("字", 192) }, "newline": func(v *ListInput) { v.BusinessKey = "x\n" }, "zero_time": func(v *ListInput) { z := time.Time{}; v.CreatedFrom = &z }, "nanosecond": func(v *ListInput) { z := from.Add(time.Nanosecond); v.CreatedFrom = &z }, "equal_bounds": func(v *ListInput) { v.CreatedBefore = &from }, "reverse_bounds": func(v *ListInput) { v.CreatedFrom = &before; v.CreatedBefore = &from },
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
in := base
|
||||
change(&in)
|
||||
if _, err := api.ListTasks(context.Background(), auth.Token{}, in); fault.Code(err) != "INVALID_ARGUMENT" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if store.calls != 0 {
|
||||
t.Fatal("invalid query reached persistence")
|
||||
}
|
||||
if _, err := api.ListTasks(context.Background(), auth.Token{}, base); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if store.input.BusinessKey != base.BusinessKey || store.input.CreatedFrom.Location() != time.UTC || !store.input.CreatedFrom.Equal(from) || store.input.Limit != 20 {
|
||||
t.Fatal("query normalization lost meaning")
|
||||
}
|
||||
for _, in := range []ListInput{{QueueID: base.QueueID, CreatedFrom: &from}, {QueueID: base.QueueID, CreatedBefore: &before}} {
|
||||
if _, err := api.ListTasks(context.Background(), auth.Token{}, in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,12 @@ type CancelResult struct {
|
||||
Deduplicated bool `json:"deduplicated"`
|
||||
}
|
||||
type ListInput struct {
|
||||
// BusinessKey 可选精确业务关联键;空表示不筛选,区分大小写、Unicode形式及尾部空格。
|
||||
BusinessKey string
|
||||
// CreatedFrom 可选创建时间下界(包含),UTC微秒精度。
|
||||
CreatedFrom *time.Time
|
||||
// CreatedBefore 可选创建时间上界(不包含),与下界同时给出时必须更晚。
|
||||
CreatedBefore *time.Time
|
||||
// QueueID 队列标识,必须属于当前命名空间。
|
||||
QueueID core.ID
|
||||
// State 当前任务状态;终态不会被重复请求重开。
|
||||
|
||||
Reference in New Issue
Block a user