Files
knowledge-governance/internal/workflows/testweknora/poll.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

142 lines
4.6 KiB
Go

package testweknora
import (
"context"
"errors"
"time"
"knowledge-governance/internal/governance"
"knowledge-governance/internal/jobs"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/platform/weknora"
)
var (
ErrInvalidPollJobPayload = errors.New("test_poll_invalid_payload")
ErrPollRemoteFailed = errors.New("test_poll_remote_failed")
ErrPollRemoteIdentity = errors.New("test_poll_remote_identity_invalid")
ErrPollStateConflict = errors.New("test_poll_state_conflict")
)
type PollStore interface {
LoadPublicationForPoll(context.Context, string) (postgresstore.PublicationForPoll, error)
RecordRemoteStatusAndScheduleNextPoll(context.Context, string, int, governance.ProcessingState, *time.Time, time.Time, time.Time) (int, error)
RecordCompletedAndScheduleValidation(context.Context, string, int, *time.Time, time.Time) error
RecordProcessingFailure(context.Context, string, int, string, time.Time) error
MarkStalled(context.Context, string, int, time.Time, time.Time) error
}
type PollClient interface {
GetKnowledge(context.Context, string) (weknora.Knowledge, error)
}
type PollHandlerConfig struct {
Store PollStore
Client PollClient
PollDelay time.Duration
StallAfter time.Duration
Now func() time.Time
}
type PollHandler struct {
store PollStore
client PollClient
pollDelay time.Duration
stallAfter time.Duration
now func() time.Time
}
type pollPayload struct {
PublicationID string `json:"publication_id"`
Sequence int `json:"sequence"`
}
func NewPollHandler(config PollHandlerConfig) (*PollHandler, error) {
if config.Store == nil || config.Client == nil || config.PollDelay <= 0 || config.StallAfter <= 0 {
return nil, errors.New("invalid test poll handler configuration")
}
if config.Now == nil {
config.Now = time.Now
}
return &PollHandler{
store: config.Store, client: config.Client, pollDelay: config.PollDelay,
stallAfter: config.StallAfter, now: config.Now,
}, nil
}
func (handler *PollHandler) Handle(ctx context.Context, job jobs.Job) error {
var payload pollPayload
if err := decodeStrictJSON(job.Payload, &payload); err != nil || payload.PublicationID == "" || payload.Sequence < 1 {
return jobs.Permanent(jobs.ErrCodeHandlerFallback, ErrInvalidPollJobPayload)
}
publication, err := handler.store.LoadPublicationForPoll(ctx, payload.PublicationID)
if err != nil {
return ErrPollStateConflict
}
if publication.KnowledgeID == "" || publication.PollSequence != payload.Sequence {
return ErrPollStateConflict
}
now := handler.now()
knowledge, err := handler.client.GetKnowledge(ctx, publication.KnowledgeID)
if err != nil {
return ErrPollRemoteFailed
}
if knowledge.ID != publication.KnowledgeID || knowledge.KnowledgeBaseID != publication.TestKBID {
return ErrPollRemoteIdentity
}
state := mapParseStatus(knowledge.ParseStatus)
if state == governance.ProcessingCompleted {
if err := handler.store.RecordCompletedAndScheduleValidation(ctx, publication.ID, publication.PollSequence, &knowledge.UpdatedAt, now); err != nil {
return ErrPollStateConflict
}
return nil
}
if state == governance.ProcessingFailed {
if err := handler.store.RecordProcessingFailure(ctx, publication.ID, publication.PollSequence, "remote_parse_failed", now); err != nil {
return ErrPollStateConflict
}
return nil
}
if !state.Valid() {
if err := handler.store.RecordProcessingFailure(ctx, publication.ID, publication.PollSequence, "remote_unknown_status", now); err != nil {
return ErrPollStateConflict
}
return nil
}
cutoff := now.Add(-handler.stallAfter)
lastProgress := publication.LastProgressAt
if lastProgress == nil {
lastProgress = publication.ProcessingStartedAt
}
if lastProgress == nil {
lastProgress = publication.LastObservedAt
}
if lastProgress != nil && !lastProgress.After(cutoff) {
if err := handler.store.MarkStalled(ctx, publication.ID, publication.PollSequence, cutoff, now); err != nil {
return ErrPollStateConflict
}
return nil
}
if _, err := handler.store.RecordRemoteStatusAndScheduleNextPoll(ctx, publication.ID, publication.PollSequence, state, &knowledge.UpdatedAt, now, now.Add(handler.pollDelay)); err != nil {
return ErrPollStateConflict
}
return nil
}
func mapParseStatus(status string) governance.ProcessingState {
switch status {
case "pending":
return governance.ProcessingPending
case "processing":
return governance.ProcessingProcessing
case "finalizing":
return governance.ProcessingFinalizing
case "completed":
return governance.ProcessingCompleted
case "failed":
return governance.ProcessingFailed
default:
return governance.ProcessingState(status)
}
}