Files
flvx/go-backend/internal/http/handler/jobs.go
T
sagit 5e96a8de72 feat(quota): add tunnel traffic quota with daily/monthly limits (#291) (#308)
Implement per-tunnel traffic quota feature:
- Add TunnelQuota model with daily/monthly usage tracking
- Integrate quota enforcement into flow accumulation path
- Pause forwards and disable tunnel when quota exceeded
- Block new forward creation/resume when tunnel quota disabled
- Auto-reset daily/monthly windows at 00:05 via maintenance job
- Add manual reset API endpoint for admins
- Include quota config in tunnel backup/restore
- Add frontend UI for quota settings and usage display

Entire-Checkpoint: e629b27ca437
2026-03-11 16:09:03 +08:00

217 lines
4.4 KiB
Go

package handler
import (
"context"
"time"
)
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(3)
h.jobsMu.Unlock()
go h.runHourlyStatsLoop(ctx)
go h.runDailyMaintenanceLoop(ctx)
go h.runNodeRenewalCycleLoop(ctx)
}
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) 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.resetTunnelQuotaWindows(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
}