package contract import ( "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/fullsync/supply" "time" ) // RunMeta retains every existing queue metadata field and JSON tag. The // optional fields are additive; zero extension fields are legal on old runs. type RunMeta struct { SupplyObservation *supply.Snapshot `json:"supplyObservation,omitempty"` RunID string `json:"runId"` Operation string `json:"operation"` QueryMonth string `json:"queryMonth"` Status RunStatus `json:"status"` ProducerDone bool `json:"producerDone"` CurrentRound int `json:"currentRound"` ActiveCount int64 `json:"activeCount"` InflightCount int64 `json:"inflightCount"` NextRoundCount int64 `json:"nextRoundCount"` SuccessCount int64 `json:"successCount"` FailedCount int64 `json:"failedCount"` CreatedAt time.Time `json:"createdAt"` ProducerCompletedAt time.Time `json:"producerCompletedAt"` CompletedAt time.Time `json:"completedAt"` SyncType SyncType `json:"syncType,omitempty"` SourceRunID string `json:"sourceRunId,omitempty"` ReplayCycle int `json:"replayCycle,omitempty"` TriggerSource string `json:"triggerSource,omitempty"` ScannedCount int64 `json:"scannedCount,omitempty"` EnqueuedCount int64 `json:"enqueuedCount,omitempty"` UpstreamAttemptCount int64 `json:"upstreamAttemptCount,omitempty"` BatchCount int64 `json:"batchCount,omitempty"` MaxRound int `json:"maxRound,omitempty"` PendingFailureCount int64 `json:"pendingFailureCount,omitempty"` StartedAt *time.Time `json:"startedAt,omitempty"` ScanCompletedAt *time.Time `json:"scanCompletedAt,omitempty"` ArchiveStatus ArchiveStatus `json:"archiveStatus,omitempty"` ArchiveVersion int `json:"archiveVersion,omitempty"` ArchivePath string `json:"archivePath,omitempty"` ArchiveChecksum string `json:"archiveChecksum,omitempty"` ArchivedAt *time.Time `json:"archivedAt,omitempty"` ReplayDisposition ReplayDisposition `json:"replayDisposition,omitempty"` // Scope fields mirror zset.RunMeta. ScopeChannels == 0 means a full sync. ScopeChannels int `json:"scopeChannels,omitempty"` ScopeDigest string `json:"scopeDigest,omitempty"` ScopeChannelsSample string `json:"scopeChannelsSample,omitempty"` RoundStats []RoundStats `json:"rounds,omitempty"` } func (m RunMeta) Validate() error { if err := ValidateID(m.RunID); err != nil { return err } if m.Status != "" && !m.Status.Valid() { return invalid("unknown run status") } if m.SyncType != "" && !m.SyncType.Valid() { return invalid("unknown sync type") } if m.ArchiveStatus != "" && !m.ArchiveStatus.Valid() { return invalid("unknown archive status") } if m.ReplayDisposition != "" && !m.ReplayDisposition.Valid() { return invalid("unknown replay disposition") } if m.SourceRunID != "" { if err := ValidateID(m.SourceRunID); err != nil { return err } if m.SourceRunID == m.RunID { return invalid("a run cannot replay itself") } } if m.SyncType == SyncReplay && (m.SourceRunID == "" || m.ReplayCycle < 1) { return invalid("replay requires sourceRunId and a positive replayCycle") } if m.SyncType == SyncNormal && (m.SourceRunID != "" || m.ReplayCycle != 0) { return invalid("normal runs cannot have replay provenance") } if m.MaxRound > 0 && m.CurrentRound > m.MaxRound { return invalid("currentRound exceeds maxRound") } return nonnegative(int64(m.CurrentRound), int64(m.MaxRound), int64(m.ReplayCycle), int64(m.ArchiveVersion), m.ActiveCount, m.InflightCount, m.NextRoundCount, m.SuccessCount, m.FailedCount, m.ScannedCount, m.EnqueuedCount, m.UpstreamAttemptCount, m.BatchCount, m.PendingFailureCount, int64(m.ScopeChannels)) } // ValidateAccounting is opt-in for runs whose extended counters are known. // Do not apply it to legacy metadata with a missing enqueuedCount. Inflight is // a subset of active and is never added to the completion equation. func (m RunMeta) ValidateAccounting() error { if err := m.Validate(); err != nil { return err } remaining := m.EnqueuedCount for _, count := range []int64{m.SuccessCount, m.FailedCount, m.ActiveCount, m.NextRoundCount} { if count > remaining { return invalid("enqueuedCount does not balance run outcomes") } remaining -= count } if remaining != 0 || m.InflightCount > m.ActiveCount || m.PendingFailureCount > m.FailedCount { return invalid("inconsistent run counters") } if m.Status.Terminal() && (!m.ProducerDone || m.ActiveCount != 0 || m.NextRoundCount != 0) { return invalid("terminal run still has producer or queue work") } return nil } // RoundStats contains only bounded aggregate fields. It must never grow with // the number of cards, channels, task IDs, upstream codes, or error messages. type RoundStats struct { RunID string `json:"runId"` Round int `json:"round"` EnqueuedCount int64 `json:"enqueuedCount"` ActiveCount int64 `json:"activeCount"` NextRoundCount int64 `json:"nextRoundCount"` SuccessCount int64 `json:"successCount"` FailedCount int64 `json:"failedCount"` UpstreamAttemptCount int64 `json:"upstreamAttemptCount"` BatchCount int64 `json:"batchCount"` StartedAt *time.Time `json:"startedAt,omitempty"` CompletedAt *time.Time `json:"completedAt,omitempty"` ErrorCounts ErrorCounts `json:"errorCounts"` } func (s RoundStats) Validate() error { if err := ValidateID(s.RunID); err != nil { return err } if s.Round < 1 { return invalid("round must be positive") } if err := nonnegative(s.EnqueuedCount, s.ActiveCount, s.NextRoundCount, s.SuccessCount, s.FailedCount, s.UpstreamAttemptCount, s.BatchCount); err != nil { return err } _, err := s.ErrorCounts.Normalized() return err } type RunView struct { Source RunSource `json:"source"` Run RunMeta `json:"run"` Rounds []RoundStats `json:"rounds,omitempty"` }