Files
geo/server/internal/tenant/app/monitoring_projection_rebuild_queue.go
T
root 6066f43a7d Add monitoring service and database schema
- Implement monitoring service with heartbeat, lease tasks, resume tasks, and task result handling.
- Create monitoring time utilities for business date calculations.
- Add unit tests for date window resolution and business day handling.
- Define database schema for monitoring-related tables including quotas, daily reports, and task management.
- Establish migration scripts for creating and dropping monitoring tables.
2026-04-12 09:56:18 +08:00

195 lines
6.1 KiB
Go

package app
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
"github.com/geo-platform/tenant-api/internal/shared/response"
"go.uber.org/zap"
)
const (
monitoringProjectionQueueIOTimeout = 5 * time.Second
monitoringProjectionFallbackTimeout = 2 * time.Minute
monitoringProjectionQueueMessageVer = "v1"
)
type monitoringProjectionRebuildEnvelope struct {
Version string `json:"version"`
TenantID int64 `json:"tenant_id"`
BrandID int64 `json:"brand_id"`
BusinessDate string `json:"business_date"`
Source string `json:"source"`
}
func encodeMonitoringProjectionRebuildEnvelope(tenantID, brandID int64, businessDay time.Time, source string) ([]byte, error) {
businessDay = monitoringCanonicalBusinessDay(businessDay)
return json.Marshal(monitoringProjectionRebuildEnvelope{
Version: monitoringProjectionQueueMessageVer,
TenantID: tenantID,
BrandID: brandID,
BusinessDate: businessDay.Format("2006-01-02"),
Source: source,
})
}
func decodeMonitoringProjectionRebuildEnvelope(payload []byte) (*monitoringProjectionRebuildEnvelope, error) {
if len(payload) == 0 {
return nil, fmt.Errorf("monitoring projection rebuild payload is empty")
}
var envelope monitoringProjectionRebuildEnvelope
if err := json.Unmarshal(payload, &envelope); err != nil {
return nil, err
}
if envelope.TenantID <= 0 {
return nil, fmt.Errorf("monitoring projection rebuild payload missing tenant_id")
}
if envelope.BrandID <= 0 {
return nil, fmt.Errorf("monitoring projection rebuild payload missing brand_id")
}
if _, err := parseMonitoringBusinessDateInLocation(envelope.BusinessDate, monitoringBusinessLocation()); err != nil {
return nil, fmt.Errorf("monitoring projection rebuild payload has invalid business_date")
}
return &envelope, nil
}
func (s *MonitoringCallbackService) publishProjectionRebuildEnvelope(ctx context.Context, payload []byte) error {
if s == nil || s.rabbitMQ == nil {
return response.ErrServiceUnavailable(50341, "monitoring_projection_queue_unavailable", "rabbitmq client is not configured")
}
if err := s.rabbitMQ.PublishMonitorProjectionRebuild(ctx, payload); err != nil {
return response.ErrServiceUnavailable(50341, "monitoring_projection_queue_unavailable", err.Error())
}
return nil
}
func (s *MonitoringCallbackService) dispatchMonitoringProjectionRebuilds(tenantID int64, businessDay time.Time, brandIDs map[int64]struct{}, source string) {
if s == nil || len(brandIDs) == 0 {
return
}
sortedBrandIDs := sortedMonitoringProjectionBrandIDs(brandIDs)
failedBrandIDs := make([]int64, 0)
for _, brandID := range sortedBrandIDs {
payload, err := encodeMonitoringProjectionRebuildEnvelope(tenantID, brandID, businessDay, source)
if err != nil {
failedBrandIDs = append(failedBrandIDs, brandID)
if s.logger != nil {
s.logger.Warn("monitoring projection rebuild payload encode failed", zap.Int64("tenant_id", tenantID), zap.Int64("brand_id", brandID), zap.Error(err))
}
continue
}
ctx, cancel := context.WithTimeout(context.Background(), monitoringProjectionQueueIOTimeout)
err = s.publishProjectionRebuildEnvelope(ctx, payload)
cancel()
if err != nil {
failedBrandIDs = append(failedBrandIDs, brandID)
if s.logger != nil {
s.logger.Warn("monitoring projection rebuild enqueue failed", zap.Int64("tenant_id", tenantID), zap.Int64("brand_id", brandID), zap.Error(err))
}
}
}
if len(failedBrandIDs) == 0 {
return
}
ctx, cancel := context.WithTimeout(context.Background(), monitoringProjectionFallbackTimeout)
defer cancel()
if err := s.rebuildBrandDailyProjectionBatch(ctx, tenantID, businessDay, failedBrandIDs); err != nil {
if s.logger != nil {
s.logger.Warn("monitoring projection rebuild fallback failed",
zap.Int64("tenant_id", tenantID),
zap.Int("brand_count", len(failedBrandIDs)),
zap.Error(err),
)
}
return
}
if s.logger != nil {
s.logger.Info("monitoring projection rebuild fallback completed",
zap.Int64("tenant_id", tenantID),
zap.Int("brand_count", len(failedBrandIDs)),
)
}
}
func (s *MonitoringCallbackService) ProcessProjectionRebuildEnvelope(ctx context.Context, envelope monitoringProjectionRebuildEnvelope) error {
businessDay, err := parseMonitoringBusinessDateInLocation(envelope.BusinessDate, monitoringBusinessLocation())
if err != nil {
return response.ErrBadRequest(40041, "invalid_business_date", "projection rebuild business_date is invalid")
}
return s.rebuildBrandDailyProjectionBatch(ctx, envelope.TenantID, businessDay, []int64{envelope.BrandID})
}
func (s *MonitoringCallbackService) rebuildBrandDailyProjectionBatch(ctx context.Context, tenantID int64, businessDay time.Time, brandIDs []int64) error {
if s == nil || s.monitoringPool == nil || len(brandIDs) == 0 {
return nil
}
businessDay = monitoringCanonicalBusinessDay(businessDay)
tx, err := s.monitoringPool.Begin(ctx)
if err != nil {
return response.ErrInternal(50041, "begin_failed", "failed to start monitoring projection rebuild")
}
defer tx.Rollback(ctx)
for _, brandID := range uniqueSortedMonitoringProjectionBrandIDs(brandIDs) {
if err := s.rebuildBrandDailyProjection(ctx, tx, tenantID, brandID, businessDay); err != nil {
return err
}
}
if err := tx.Commit(ctx); err != nil {
return response.ErrInternal(50041, "commit_failed", "failed to commit monitoring projection rebuild")
}
return nil
}
func sortedMonitoringProjectionBrandIDs(items map[int64]struct{}) []int64 {
result := make([]int64, 0, len(items))
for brandID := range items {
result = append(result, brandID)
}
sort.Slice(result, func(i, j int) bool {
return result[i] < result[j]
})
return result
}
func uniqueSortedMonitoringProjectionBrandIDs(items []int64) []int64 {
if len(items) == 0 {
return nil
}
seen := make(map[int64]struct{}, len(items))
result := make([]int64, 0, len(items))
for _, brandID := range items {
if brandID <= 0 {
continue
}
if _, ok := seen[brandID]; ok {
continue
}
seen[brandID] = struct{}{}
result = append(result, brandID)
}
sort.Slice(result, func(i, j int) bool {
return result[i] < result[j]
})
return result
}