130 lines
4.6 KiB
Go
130 lines
4.6 KiB
Go
// Package mictime runs room mic outbox consumers and exposes mic duration queries.
|
|
package mictime
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"hyapp/pkg/logx"
|
|
"hyapp/pkg/xerr"
|
|
mictimedomain "hyapp/services/user-service/internal/domain/mictime"
|
|
)
|
|
|
|
// Repository applies room mic events and returns user duration aggregates.
|
|
type Repository interface {
|
|
ProcessRoomMicEvent(ctx context.Context, event mictimedomain.RoomMicEvent) (mictimedomain.ApplyResult, error)
|
|
CompensateOpenSessions(ctx context.Context, options mictimedomain.CompensationOptions) (mictimedomain.CompensationResult, error)
|
|
GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error)
|
|
}
|
|
|
|
// RoomOutboxReader scans room-service RoomMicChanged facts in creation order.
|
|
type RoomOutboxReader interface {
|
|
ListRoomMicEventsAfter(ctx context.Context, cursor mictimedomain.RoomOutboxCursor, batchSize int) ([]mictimedomain.RoomMicEvent, error)
|
|
}
|
|
|
|
// Service owns the user mic duration read model.
|
|
type Service struct {
|
|
repository Repository
|
|
reader RoomOutboxReader
|
|
}
|
|
|
|
// WorkerOptions controls local room outbox polling.
|
|
type WorkerOptions struct {
|
|
WorkerID string
|
|
PollInterval time.Duration
|
|
BatchSize int
|
|
}
|
|
|
|
// CompensationWorkerOptions controls abnormal open session closure.
|
|
type CompensationWorkerOptions struct {
|
|
WorkerID string
|
|
BatchSize int
|
|
PendingPublishMaxAge time.Duration
|
|
PublishingSessionMaxAge time.Duration
|
|
}
|
|
|
|
// New creates a mic time service with persistence and optional room outbox reader.
|
|
func New(repository Repository, reader RoomOutboxReader) *Service {
|
|
return &Service{repository: repository, reader: reader}
|
|
}
|
|
|
|
// GetLifetimeStats returns user-wide accumulated mic duration.
|
|
func (s *Service) GetLifetimeStats(ctx context.Context, appCode string, userID int64) (mictimedomain.LifetimeStats, error) {
|
|
if s == nil || s.repository == nil {
|
|
return mictimedomain.LifetimeStats{}, xerr.New(xerr.Unavailable, "mic time repository is not configured")
|
|
}
|
|
return s.repository.GetLifetimeStats(ctx, appCode, userID)
|
|
}
|
|
|
|
// RunRoomMicWorker consumes RoomMicChanged facts from room-service outbox.
|
|
func (s *Service) RunRoomMicWorker(ctx context.Context, options WorkerOptions) {
|
|
if s == nil || s.repository == nil || s.reader == nil {
|
|
logx.Warn(ctx, "worker_disabled", slog.String("worker", "room_mic"), slog.String("component", "user_mic_time"), slog.String("reason", "missing_dependency"))
|
|
return
|
|
}
|
|
if options.PollInterval <= 0 {
|
|
options.PollInterval = 5 * time.Second
|
|
}
|
|
if options.BatchSize <= 0 {
|
|
options.BatchSize = 100
|
|
}
|
|
|
|
var cursor mictimedomain.RoomOutboxCursor
|
|
for {
|
|
processed, err := s.processBatch(ctx, &cursor, options.BatchSize)
|
|
if err != nil {
|
|
logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "room_mic"), slog.String("component", "user_mic_time"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize))
|
|
}
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
if processed >= options.BatchSize {
|
|
// Backlog is still present; continue without sleeping until the cursor catches up.
|
|
continue
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(options.PollInterval):
|
|
}
|
|
}
|
|
}
|
|
|
|
// ProcessOpenSessionCompensationBatch exposes one bounded compensation scan for cron-service.
|
|
func (s *Service) ProcessOpenSessionCompensationBatch(ctx context.Context, options CompensationWorkerOptions) (mictimedomain.CompensationResult, error) {
|
|
if s == nil || s.repository == nil {
|
|
return mictimedomain.CompensationResult{}, xerr.New(xerr.Unavailable, "mic time repository is not configured")
|
|
}
|
|
if options.BatchSize <= 0 {
|
|
options.BatchSize = 100
|
|
}
|
|
if options.PendingPublishMaxAge <= 0 {
|
|
options.PendingPublishMaxAge = 2 * time.Minute
|
|
}
|
|
if options.PublishingSessionMaxAge <= 0 {
|
|
options.PublishingSessionMaxAge = 12 * time.Hour
|
|
}
|
|
return s.repository.CompensateOpenSessions(ctx, mictimedomain.CompensationOptions{
|
|
NowMs: time.Now().UnixMilli(),
|
|
BatchSize: options.BatchSize,
|
|
PendingPublishMaxAgeMs: options.PendingPublishMaxAge.Milliseconds(),
|
|
PublishingSessionMaxAgeMs: options.PublishingSessionMaxAge.Milliseconds(),
|
|
})
|
|
}
|
|
|
|
func (s *Service) processBatch(ctx context.Context, cursor *mictimedomain.RoomOutboxCursor, batchSize int) (int, error) {
|
|
events, err := s.reader.ListRoomMicEventsAfter(ctx, *cursor, batchSize)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for _, event := range events {
|
|
if _, err := s.repository.ProcessRoomMicEvent(ctx, event); err != nil {
|
|
return 0, err
|
|
}
|
|
cursor.CreatedAtMs = event.OutboxCreatedAtMs
|
|
cursor.EventID = event.EventID
|
|
}
|
|
return len(events), nil
|
|
}
|