refactor: share admin state validation
This commit is contained in:
parent
2dbfe2f742
commit
c5311aa9c0
@ -51,6 +51,9 @@ Repository。
|
||||
行为契约。Controller Admin 应用层只负责 DTO 映射、配置解析/校验和运行态发布,
|
||||
不自行拼装数据库事务。
|
||||
|
||||
所有命令和查询在领域类型上提供公用 `Validate()`;Memory 与 PostgreSQL
|
||||
Adapter 必须在访问存储前调用同一方法,不复制名称、引用、分页或租约校验。
|
||||
|
||||
### 全局修订
|
||||
|
||||
每次产生管理状态变化时分配单调递增的全局 `revision`:
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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` 请求链仍未提前标记完成。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user