242 lines
9.4 KiB
Go
242 lines
9.4 KiB
Go
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
|
|
}
|
|
}
|