package handler import ( "context" "log" "time" "go-backend/internal/license" ) var nftablesTrafficCollectInterval = 30 * time.Second func (h *Handler) StartBackgroundJobs() { if h == nil || h.repo == nil { return } h.jobsMu.Lock() if h.jobsStarted { h.jobsMu.Unlock() return } ctx, cancel := context.WithCancel(context.Background()) h.jobsCancel = cancel h.jobsStarted = true h.jobsWG.Add(8) h.jobsMu.Unlock() go h.runHourlyStatsLoop(ctx) go h.runDailyMaintenanceLoop(ctx) go h.runNodeRenewalCycleLoop(ctx) go h.runMetricsIngestion(ctx) go h.runHealthChecks(ctx) go h.runTunnelQualityProber(ctx) go h.runValidateLicenseJob(ctx) go h.runNftablesTrafficCollectLoop(ctx) } func (h *Handler) runValidateLicenseJob(ctx context.Context) { defer h.jobsWG.Done() ticker := time.NewTicker(12 * time.Hour) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: h.validateLicenseJob() } } } func (h *Handler) validateLicenseJob() { if h == nil || h.repo == nil { return } accountID := "1bc96cac-09de-4cf4-af34-26afdad63a90" key, _ := h.repo.GetViteConfigValue("license_key") isCommercial, _ := h.repo.GetViteConfigValue("is_commercial") if key == "" || isCommercial != "true" { return // Nothing to validate } fingerprint, _ := h.repo.GetViteConfigValue("machine_fingerprint") client := license.NewKeygenClient(accountID, "") valResp, err := client.ValidateKeyWithFingerprint(key, fingerprint) if err != nil { // Network error or timeout. Grace period by not revoking immediately here. return } if !valResp.Meta.Valid { // License is invalid (e.g., revoked, suspended, expired). Downgrade the system. now := time.Now().UnixMilli() _ = h.repo.UpsertConfig("is_commercial", "false", now) } else { now := time.Now().UnixMilli() expiry := valResp.Data.Attributes.Expiry if expiry == "" { expiry = "never" } _ = h.repo.UpsertConfig("license_expiry", expiry, now) } } func (h *Handler) StopBackgroundJobs() { if h == nil { return } h.jobsMu.Lock() if !h.jobsStarted { h.jobsMu.Unlock() return } cancel := h.jobsCancel h.jobsCancel = nil h.jobsStarted = false h.jobsMu.Unlock() if cancel != nil { cancel() } h.jobsWG.Wait() } func (h *Handler) runMetricsIngestion(ctx context.Context) { defer h.jobsWG.Done() if h.metrics != nil { h.metrics.Start(ctx) } } func (h *Handler) runHealthChecks(ctx context.Context) { defer h.jobsWG.Done() if h.healthCheck != nil { h.healthCheck.Start(ctx) } } func (h *Handler) runTunnelQualityProber(ctx context.Context) { defer h.jobsWG.Done() if h == nil || h.qualityProber == nil { return } h.qualityProber.Start(ctx) } func (h *Handler) runNftablesTrafficCollectLoop(ctx context.Context) { defer h.jobsWG.Done() h.runNftablesStartupReconcile(ctx) select { case <-ctx.Done(): return default: h.runNftablesTrafficCollectJob(time.Now()) } interval := nftablesTrafficCollectInterval if interval <= 0 { interval = 30 * time.Second } ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: h.runNftablesTrafficCollectJob(time.Now()) } } } func (h *Handler) runNftablesStartupReconcile(ctx context.Context) { if h == nil || h.repo == nil { return } nodes, err := h.repo.ListNftablesNodesForCollection() if err != nil { log.Printf("nftables startup reconcile failed op=list_nodes err=%v", err) return } for _, node := range nodes { select { case <-ctx.Done(): return default: } if err := h.syncNftablesNode(node.NodeID); err != nil { log.Printf("nftables startup reconcile failed node_id=%d err=%v", node.NodeID, err) } } } func (h *Handler) runHourlyStatsLoop(ctx context.Context) { defer h.jobsWG.Done() for { wait := durationUntilNextHour(time.Now()) timer := time.NewTimer(wait) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } return case <-timer.C: h.runStatisticsFlowJob(time.Now()) } } } func (h *Handler) runDailyMaintenanceLoop(ctx context.Context) { defer h.jobsWG.Done() for { wait := durationUntilNextDailyMaintenance(time.Now()) timer := time.NewTimer(wait) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } return case <-timer.C: h.runResetAndExpiryJob(time.Now()) } } } func durationUntilNextHour(now time.Time) time.Duration { next := now.Truncate(time.Hour).Add(time.Hour) return next.Sub(now) } func durationUntilNextDailyMaintenance(now time.Time) time.Duration { next := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 5, 0, now.Location()) if !next.After(now) { next = next.Add(24 * time.Hour) } return next.Sub(now) } func (h *Handler) runStatisticsFlowJob(now time.Time) { if h == nil || h.repo == nil { return } nowMs := now.UnixMilli() cutoffMs := nowMs - int64((48*time.Hour)/time.Millisecond) _ = h.repo.PurgeOldStatisticsFlows(cutoffMs) hourMark := now.Truncate(time.Hour) hourText := hourMark.Format("15:04") createdTime := hourMark.UnixMilli() users, err := h.repo.ListAllUserFlowSnapshots() if err != nil { return } for _, user := range users { currentTotal := user.InFlow + user.OutFlow increment := currentTotal lastTotal, err := h.repo.GetLastStatisticsFlowTotal(user.UserID) if err == nil && lastTotal.Valid { increment = currentTotal - lastTotal.Int64 if increment < 0 { increment = currentTotal } } _ = h.repo.CreateStatisticsFlow(user.UserID, increment, currentTotal, hourText, createdTime) } } func (h *Handler) runResetAndExpiryJob(now time.Time) { if h == nil || h.repo == nil { return } h.resetMonthlyFlow(now) h.resetUserQuotaWindows(now) h.disableExpiredUsers(now.UnixMilli()) h.disableExpiredUserTunnels(now.UnixMilli()) } func (h *Handler) resetMonthlyFlow(now time.Time) { currentDay := now.Day() lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day() _ = h.repo.ResetUserMonthlyFlow(currentDay, lastDay) _ = h.repo.ResetUserTunnelMonthlyFlow(currentDay, lastDay) } func (h *Handler) disableExpiredUsers(nowMs int64) { userIDs, err := h.repo.ListExpiredActiveUserIDs(nowMs) if err != nil { return } for _, userID := range userIDs { forwards, err := h.listActiveForwardsByUser(userID) if err == nil { h.pauseForwardRecords(forwards, nowMs) } _ = h.repo.DisableUser(userID) } } func (h *Handler) disableExpiredUserTunnels(nowMs int64) { items, err := h.repo.ListExpiredActiveUserTunnels(nowMs) if err != nil { return } for _, item := range items { forwards, err := h.listActiveForwardsByUserTunnel(item.UserID, item.TunnelID) if err == nil { h.pauseForwardRecords(forwards, nowMs) } _ = h.repo.DisableUserTunnel(item.ID) } } func (h *Handler) runNodeRenewalCycleLoop(ctx context.Context) { defer h.jobsWG.Done() for { wait := durationUntilNextNodeRenewalCycle(time.Now()) timer := time.NewTimer(wait) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } return case <-timer.C: h.runNodeRenewalCycleJob(time.Now()) } } } func durationUntilNextNodeRenewalCycle(now time.Time) time.Duration { next := now.Truncate(6 * time.Hour).Add(6 * time.Hour) return next.Sub(now) } func (h *Handler) runNodeRenewalCycleJob(now time.Time) { if h == nil || h.repo == nil { return } advanced, err := h.repo.AdvanceNodeRenewalCycles(now.UnixMilli()) if err != nil { return } _ = advanced }