refactor: share admin state validation

This commit is contained in:
youfak 2026-07-29 21:47:50 +08:00
parent 2dbfe2f742
commit c5311aa9c0
5 changed files with 59 additions and 34 deletions

View File

@ -51,6 +51,9 @@ Repository。
行为契约。Controller Admin 应用层只负责 DTO 映射、配置解析/校验和运行态发布,
不自行拼装数据库事务。
所有命令和查询在领域类型上提供公用 `Validate()`Memory 与 PostgreSQL
Adapter 必须在访问存储前调用同一方法,不复制名称、引用、分页或租约校验。
### 全局修订
每次产生管理状态变化时分配单调递增的全局 `revision`

View File

@ -194,7 +194,8 @@ type AcknowledgeCommand struct {
EventIDs []uint64
}
func validateSetUpstreamCommand(command SetUpstreamCommand) error {
// Validate checks whether the command is safe for every Store adapter to execute.
func (command SetUpstreamCommand) Validate() error {
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
!validIdentifier(command.Name) {
return ErrInvalidCommand
@ -202,7 +203,8 @@ func validateSetUpstreamCommand(command SetUpstreamCommand) error {
return nil
}
func validateSwitchRoutingCommand(command SwitchRoutingCommand) error {
// Validate checks whether the command is safe for every Store adapter to execute.
func (command SwitchRoutingCommand) Validate() error {
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
!validIdentifier(command.Name) || !validIdentifier(command.ExpectedCurrent) ||
!validIdentifier(command.Target) || !validOptionalText(command.Reason, MaxReasonBytes) {
@ -211,7 +213,8 @@ func validateSwitchRoutingCommand(command SwitchRoutingCommand) error {
return nil
}
func validateCommitConfigCommand(command CommitConfigCommand) error {
// Validate checks the complete management snapshot and all of its references.
func (command CommitConfigCommand) Validate() error {
if err := validateMutationBase(command.RequestID, command.Actor, command.OccurredAt); err != nil ||
!validRequiredText(command.ConfigVersion, MaxIdentifierBytes) ||
!validSHA256(command.Checksum) || !validRequiredText(command.Source, MaxSourceBytes) ||
@ -260,7 +263,8 @@ func validateCommitConfigCommand(command CommitConfigCommand) error {
return nil
}
func validateClaimCommand(command ClaimCommand) error {
// Validate checks the bounded outbox claim request.
func (command ClaimCommand) Validate() error {
if !validIdentifier(command.ConsumerID) || command.Now.IsZero() || command.Limit <= 0 ||
command.Limit > MaxPageSize || command.Lease <= 0 {
return ErrInvalidCommand
@ -268,7 +272,8 @@ func validateClaimCommand(command ClaimCommand) error {
return nil
}
func validateAcknowledgeCommand(command AcknowledgeCommand) error {
// Validate checks the bounded, duplicate-free outbox acknowledgement request.
func (command AcknowledgeCommand) Validate() error {
if !validIdentifier(command.ConsumerID) || command.Now.IsZero() || len(command.EventIDs) == 0 ||
len(command.EventIDs) > MaxPageSize {
return ErrInvalidCommand
@ -286,7 +291,8 @@ func validateAcknowledgeCommand(command AcknowledgeCommand) error {
return nil
}
func validateAuditQuery(query AuditQuery) error {
// Validate checks the bounded audit pagination request.
func (query AuditQuery) Validate() error {
if query.Limit <= 0 || query.Limit > MaxPageSize {
return ErrInvalidCommand
}

View File

@ -39,7 +39,7 @@ func (store *MemoryStore) CommitConfig(ctx context.Context, command CommitConfig
if err := contextError(ctx); err != nil {
return result, err
}
if store == nil || validateCommitConfigCommand(command) != nil {
if store == nil || command.Validate() != nil {
return result, ErrInvalidCommand
}
command = cloneCommitConfigCommand(command)
@ -109,7 +109,7 @@ func (store *MemoryStore) SetUpstreamEnabled(ctx context.Context, command SetUps
if err := contextError(ctx); err != nil {
return result, err
}
if store == nil || validateSetUpstreamCommand(command) != nil {
if store == nil || command.Validate() != nil {
return result, ErrInvalidCommand
}
@ -158,7 +158,7 @@ func (store *MemoryStore) SwitchRouting(ctx context.Context, command SwitchRouti
if err := contextError(ctx); err != nil {
return result, err
}
if store == nil || validateSwitchRoutingCommand(command) != nil {
if store == nil || command.Validate() != nil {
return result, ErrInvalidCommand
}
@ -248,7 +248,7 @@ func (store *MemoryStore) ReadAudit(ctx context.Context, query AuditQuery) ([]Au
if err := contextError(ctx); err != nil {
return nil, err
}
if store == nil || validateAuditQuery(query) != nil {
if store == nil || query.Validate() != nil {
return nil, ErrInvalidCommand
}
store.mu.Lock()
@ -273,7 +273,7 @@ func (store *MemoryStore) Claim(ctx context.Context, command ClaimCommand) ([]Ev
if err := contextError(ctx); err != nil {
return nil, err
}
if store == nil || validateClaimCommand(command) != nil {
if store == nil || command.Validate() != nil {
return nil, ErrInvalidCommand
}
store.mu.Lock()
@ -303,7 +303,7 @@ func (store *MemoryStore) Acknowledge(ctx context.Context, command AcknowledgeCo
if err := contextError(ctx); err != nil {
return err
}
if store == nil || validateAcknowledgeCommand(command) != nil {
if store == nil || command.Validate() != nil {
return ErrInvalidCommand
}
store.mu.Lock()

View File

@ -15,8 +15,8 @@ func TestValidateSetUpstreamCommand(t *testing.T) {
RequestID: "req-upstream", Actor: Actor{ID: "admin-a", SourceIP: "192.0.2.10"},
OccurredAt: now, Name: "provider-a", Enabled: true,
}
if err := validateSetUpstreamCommand(valid); err != nil {
t.Fatalf("validateSetUpstreamCommand(valid): %v", err)
if err := valid.Validate(); err != nil {
t.Fatalf("SetUpstreamCommand.Validate(valid): %v", err)
}
tests := []struct {
@ -35,8 +35,8 @@ func TestValidateSetUpstreamCommand(t *testing.T) {
t.Parallel()
command := valid
test.mutate(&command)
if err := validateSetUpstreamCommand(command); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("validateSetUpstreamCommand() error = %v, want ErrInvalidCommand", err)
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("SetUpstreamCommand.Validate() error = %v, want ErrInvalidCommand", err)
}
})
}
@ -49,8 +49,8 @@ func TestValidateSwitchRoutingCommand(t *testing.T) {
RequestID: "req-switch", Actor: Actor{ID: "admin-a"}, OccurredAt: now,
Name: "checkout", ExpectedCurrent: "provider-a", Target: "provider-b", Reason: "capacity",
}
if err := validateSwitchRoutingCommand(valid); err != nil {
t.Fatalf("validateSwitchRoutingCommand(valid): %v", err)
if err := valid.Validate(); err != nil {
t.Fatalf("SwitchRoutingCommand.Validate(valid): %v", err)
}
for _, test := range []struct {
@ -67,8 +67,8 @@ func TestValidateSwitchRoutingCommand(t *testing.T) {
t.Parallel()
command := valid
test.mutate(&command)
if err := validateSwitchRoutingCommand(command); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("validateSwitchRoutingCommand() error = %v, want ErrInvalidCommand", err)
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("SwitchRoutingCommand.Validate() error = %v, want ErrInvalidCommand", err)
}
})
}
@ -77,8 +77,8 @@ func TestValidateSwitchRoutingCommand(t *testing.T) {
func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
t.Parallel()
valid := validCommitConfigCommand()
if err := validateCommitConfigCommand(valid); err != nil {
t.Fatalf("validateCommitConfigCommand(valid): %v", err)
if err := valid.Validate(); err != nil {
t.Fatalf("CommitConfigCommand.Validate(valid): %v", err)
}
for _, test := range []struct {
@ -102,8 +102,8 @@ func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
t.Parallel()
command := cloneCommitConfigCommand(valid)
test.mutate(&command)
if err := validateCommitConfigCommand(command); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("validateCommitConfigCommand() error = %v, want ErrInvalidCommand", err)
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("CommitConfigCommand.Validate() error = %v, want ErrInvalidCommand", err)
}
})
}
@ -112,15 +112,15 @@ func TestValidateCommitConfigCommandAndReferences(t *testing.T) {
func TestValidateOutboxCommands(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 29, 10, 0, 0, 0, time.UTC)
if err := validateClaimCommand(ClaimCommand{
if err := (ClaimCommand{
ConsumerID: "publisher-a", Now: now, Limit: 100, Lease: time.Minute,
}); err != nil {
t.Fatalf("validateClaimCommand(valid): %v", err)
}).Validate(); err != nil {
t.Fatalf("ClaimCommand.Validate(valid): %v", err)
}
if err := validateAcknowledgeCommand(AcknowledgeCommand{
if err := (AcknowledgeCommand{
ConsumerID: "publisher-a", Now: now, EventIDs: []uint64{1, 2},
}); err != nil {
t.Fatalf("validateAcknowledgeCommand(valid): %v", err)
}).Validate(); err != nil {
t.Fatalf("AcknowledgeCommand.Validate(valid): %v", err)
}
invalidClaims := []ClaimCommand{
@ -131,8 +131,8 @@ func TestValidateOutboxCommands(t *testing.T) {
{ConsumerID: "publisher-a", Now: now, Limit: 1},
}
for _, command := range invalidClaims {
if err := validateClaimCommand(command); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("validateClaimCommand(%+v) error = %v", command, err)
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("ClaimCommand.Validate(%+v) error = %v", command, err)
}
}
@ -144,8 +144,20 @@ func TestValidateOutboxCommands(t *testing.T) {
{ConsumerID: "publisher-a", Now: now, EventIDs: []uint64{1, 1}},
}
for _, command := range invalidAcks {
if err := validateAcknowledgeCommand(command); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("validateAcknowledgeCommand(%+v) error = %v", command, err)
if err := command.Validate(); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("AcknowledgeCommand.Validate(%+v) error = %v", command, err)
}
}
}
func TestAuditQueryValidate(t *testing.T) {
t.Parallel()
if err := (AuditQuery{Limit: 10}).Validate(); err != nil {
t.Fatalf("AuditQuery.Validate(valid): %v", err)
}
for _, query := range []AuditQuery{{}, {Limit: -1}, {Limit: MaxPageSize + 1}} {
if err := query.Validate(); !errors.Is(err, ErrInvalidCommand) {
t.Fatalf("AuditQuery.Validate(%+v) error = %v, want ErrInvalidCommand", query, err)
}
}
}

View File

@ -2,6 +2,10 @@
## 2026-07-29
- `adminstate` 六类命令/查询已封装公用 `Validate()`MemoryStore 改为统一复用;
后续 pgx Adapter 不再重复实现名称、引用、分页和 Outbox 租约输入校验。
- 一次命令装配审计误用了不存在的 `controller/distribution/service.go`;实际领域
服务位于 `controller/extraction`Distribution 包当前只负责 HTTP Handler。
- 根据对话最终定稿统一 Sequential至少两个 Upstream省略 `endBehavior`
默认 `stop`新增领域和严格配置回归测试。disabled candidate、跨实例游标和
`onUnavailable` 请求链仍未提前标记完成。