暂存一下,防止丢失
This commit is contained in:
241
internal/application/cardobservation/series.go
Normal file
241
internal/application/cardobservation/series.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package cardobservation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// SeriesRequest 描述一次业务成功边界产生的观测序列请求。
|
||||
type SeriesRequest struct {
|
||||
Scene string `json:"scene"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
SyncType string `json:"sync_type"`
|
||||
ExpectedValue string `json:"expected_value,omitempty"`
|
||||
Source string `json:"source"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
}
|
||||
|
||||
// SeriesTaskPayload 是固定三次 Asynq 任务的结构化载荷。
|
||||
type SeriesTaskPayload struct {
|
||||
SeriesID string `json:"series_id"`
|
||||
Attempt int `json:"attempt"`
|
||||
ScheduledAt time.Time `json:"scheduled_at"`
|
||||
Scene string `json:"scene"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
SyncType string `json:"sync_type"`
|
||||
ExpectedValue string `json:"expected_value,omitempty"`
|
||||
Source string `json:"source"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
CorrelationID string `json:"correlation_id,omitempty"`
|
||||
}
|
||||
|
||||
// RunResult 描述一次实际 Gateway 请求及公共观测应用结果。
|
||||
type RunResult struct {
|
||||
StateChanged bool
|
||||
RateLimited bool
|
||||
}
|
||||
|
||||
// SeriesCoordinator 管理活跃序列、尝试幂等和实际请求互斥。
|
||||
type SeriesCoordinator interface {
|
||||
Reserve(ctx context.Context, request SeriesRequest, candidateSeriesID string, candidateBaseTime time.Time) (seriesID string, baseTime time.Time, merged bool, err error)
|
||||
IsCompleted(ctx context.Context, seriesID string) (bool, error)
|
||||
ClaimAttempt(ctx context.Context, seriesID string, attempt int) (bool, error)
|
||||
AcquireRequest(ctx context.Context, payload SeriesTaskPayload, provider string) (release func(), wait time.Duration, acquired bool, err error)
|
||||
FinishAttempt(ctx context.Context, payload SeriesTaskPayload) error
|
||||
CompleteSeries(ctx context.Context, payload SeriesTaskPayload) error
|
||||
CompleteResourceSeries(ctx context.Context, resourceType, resourceID, syncType string) error
|
||||
}
|
||||
|
||||
// SeriesScheduler 提交固定的零重试观测任务。
|
||||
type SeriesScheduler interface {
|
||||
Enqueue(ctx context.Context, payload SeriesTaskPayload) error
|
||||
}
|
||||
|
||||
// SeriesAttemptLogger 记录未访问 Gateway 的合并、互斥、限频和提前完成结果。
|
||||
type SeriesAttemptLogger interface {
|
||||
Record(ctx context.Context, payload SeriesTaskPayload, result, reason string) error
|
||||
RecordMerged(ctx context.Context, request SeriesRequest, seriesID string) error
|
||||
}
|
||||
|
||||
// SeriesRunner 查询本地快照并执行一次 Gateway 公共观测。
|
||||
type SeriesRunner interface {
|
||||
Provider(ctx context.Context, payload SeriesTaskPayload) (string, error)
|
||||
ExpectationMet(ctx context.Context, payload SeriesTaskPayload) (bool, error)
|
||||
Run(ctx context.Context, payload SeriesTaskPayload) (RunResult, error)
|
||||
}
|
||||
|
||||
// SeriesTrigger 创建或合并固定的立即、3 分钟、5 分钟任务序列。
|
||||
type SeriesTrigger struct {
|
||||
coordinator SeriesCoordinator
|
||||
scheduler SeriesScheduler
|
||||
logger SeriesAttemptLogger
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewSeriesTrigger 创建观测序列触发器。
|
||||
func NewSeriesTrigger(coordinator SeriesCoordinator, scheduler SeriesScheduler, logger SeriesAttemptLogger) *SeriesTrigger {
|
||||
return &SeriesTrigger{coordinator: coordinator, scheduler: scheduler, logger: logger, now: time.Now}
|
||||
}
|
||||
|
||||
// Trigger 创建新序列;同场景未结束序列只留合并记录,不延长原序列。
|
||||
func (s *SeriesTrigger) Trigger(ctx context.Context, request SeriesRequest) (string, bool, error) {
|
||||
if s == nil || s.coordinator == nil || s.scheduler == nil || s.logger == nil {
|
||||
return "", false, errors.New(errors.CodeInternalError, "卡观测序列触发能力未完整配置")
|
||||
}
|
||||
if err := validateSeriesRequest(request); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
candidateBaseTime := s.now().UTC()
|
||||
seriesID, baseTime, merged, err := s.coordinator.Reserve(ctx, request, uuid.NewString(), candidateBaseTime)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
// 重复触发仍以稳定任务 ID 补齐首次入队的局部失败;已存在任务由 Asynq 去重,不会延长原序列。
|
||||
for attempt := 1; attempt <= constants.CardObservationSeriesAttemptCount; attempt++ {
|
||||
scheduledAt := baseTime.Add(constants.CardObservationAttemptDelay(attempt))
|
||||
payload := SeriesTaskPayload{
|
||||
SeriesID: seriesID, Attempt: attempt, ScheduledAt: scheduledAt,
|
||||
Scene: request.Scene, ResourceType: request.ResourceType, ResourceID: request.ResourceID,
|
||||
SyncType: request.SyncType, ExpectedValue: request.ExpectedValue, Source: request.Source,
|
||||
RequestID: request.RequestID, CorrelationID: request.CorrelationID,
|
||||
}
|
||||
if err := s.scheduler.Enqueue(ctx, payload); err != nil {
|
||||
return seriesID, false, err
|
||||
}
|
||||
}
|
||||
if merged {
|
||||
if err := s.logger.RecordMerged(ctx, request, seriesID); err != nil {
|
||||
return "", true, err
|
||||
}
|
||||
}
|
||||
return seriesID, merged, nil
|
||||
}
|
||||
|
||||
// SeriesAttemptService 执行单次事件观测,不改变后续阶梯任务。
|
||||
type SeriesAttemptService struct {
|
||||
coordinator SeriesCoordinator
|
||||
runner SeriesRunner
|
||||
logger SeriesAttemptLogger
|
||||
}
|
||||
|
||||
// NewSeriesAttemptService 创建序列尝试服务。
|
||||
func NewSeriesAttemptService(coordinator SeriesCoordinator, runner SeriesRunner, logger SeriesAttemptLogger) *SeriesAttemptService {
|
||||
return &SeriesAttemptService{coordinator: coordinator, runner: runner, logger: logger}
|
||||
}
|
||||
|
||||
// Execute 执行一次幂等尝试;失败只结束当前任务。
|
||||
func (s *SeriesAttemptService) Execute(ctx context.Context, payload SeriesTaskPayload) error {
|
||||
if s == nil || s.coordinator == nil || s.runner == nil || s.logger == nil {
|
||||
return errors.New(errors.CodeInternalError, "卡观测序列执行能力未完整配置")
|
||||
}
|
||||
if err := validateSeriesPayload(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
claimed, err := s.coordinator.ClaimAttempt(ctx, payload.SeriesID, payload.Attempt)
|
||||
if err != nil || !claimed {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = s.coordinator.FinishAttempt(context.Background(), payload) }()
|
||||
|
||||
completed, err := s.coordinator.IsCompleted(ctx, payload.SeriesID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if completed {
|
||||
return s.completeRemainingAttempts(ctx, payload, "序列已提前完成")
|
||||
}
|
||||
met, err := s.runner.ExpectationMet(ctx, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if met {
|
||||
return s.completeRemainingAttempts(ctx, payload, "本地快照已达到预期")
|
||||
}
|
||||
provider, err := s.runner.Provider(ctx, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
release, wait, acquired, err := s.coordinator.AcquireRequest(ctx, payload, provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !acquired {
|
||||
return s.logger.Record(ctx, payload, constants.IntegrationResultIgnored, "实际 Gateway 请求正在执行")
|
||||
}
|
||||
defer release()
|
||||
if wait > 0 {
|
||||
timer := time.NewTimer(wait)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return s.logger.Record(ctx, payload, constants.IntegrationResultRateLimited, "最小请求间隔等待被取消")
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
result, err := s.runner.Run(ctx, payload)
|
||||
if result.RateLimited {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if payload.Attempt == constants.CardObservationSeriesAttemptCount {
|
||||
return s.coordinator.CompleteSeries(ctx, payload)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SeriesAttemptService) completeRemainingAttempts(ctx context.Context, payload SeriesTaskPayload, reason string) error {
|
||||
baseTime := payload.ScheduledAt.Add(-constants.CardObservationAttemptDelay(payload.Attempt))
|
||||
for attempt := payload.Attempt; attempt <= constants.CardObservationSeriesAttemptCount; attempt++ {
|
||||
remaining := payload
|
||||
remaining.Attempt = attempt
|
||||
remaining.ScheduledAt = baseTime.Add(constants.CardObservationAttemptDelay(attempt))
|
||||
if err := s.logger.Record(ctx, remaining, constants.IntegrationResultCompleted, reason); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.coordinator.CompleteSeries(ctx, payload)
|
||||
}
|
||||
|
||||
// CompleteResourceSeries 供可信回调在公共观测成功后提前完成同资源序列。
|
||||
func (s *SeriesAttemptService) CompleteResourceSeries(ctx context.Context, resourceType, resourceID, syncType string) error {
|
||||
if s == nil || s.coordinator == nil {
|
||||
return errors.New(errors.CodeInternalError, "卡观测序列协调器未配置")
|
||||
}
|
||||
return s.coordinator.CompleteResourceSeries(ctx, resourceType, resourceID, syncType)
|
||||
}
|
||||
|
||||
func validateSeriesRequest(request SeriesRequest) error {
|
||||
if strings.TrimSpace(request.Scene) == "" || strings.TrimSpace(request.ResourceType) == "" ||
|
||||
strings.TrimSpace(request.ResourceID) == "" || !validSyncType(request.SyncType) {
|
||||
return errors.New(errors.CodeInvalidParam, "卡观测序列参数不完整")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSeriesPayload(payload SeriesTaskPayload) error {
|
||||
if payload.SeriesID == "" || payload.Attempt < 1 || payload.Attempt > constants.CardObservationSeriesAttemptCount {
|
||||
return errors.New(errors.CodeInvalidParam, "卡观测序列任务载荷无效")
|
||||
}
|
||||
return validateSeriesRequest(SeriesRequest{Scene: payload.Scene, ResourceType: payload.ResourceType, ResourceID: payload.ResourceID, SyncType: payload.SyncType})
|
||||
}
|
||||
|
||||
func validSyncType(syncType string) bool {
|
||||
switch syncType {
|
||||
case constants.CardObservationSyncTypeRealname, constants.CardObservationSyncTypeTraffic,
|
||||
constants.CardObservationSyncTypeNetwork, constants.CardObservationSyncTypeDeviceInfo:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user