Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
eb60f670
Unverified
Commit
eb60f670
authored
Mar 12, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 12, 2026
Browse files
Merge pull request #933 from xvhuan/fix/dashboard-read-pressure-20260311
降低 admin/dashboard 读路径压力,避免 snapshot-v2 并发击穿
parents
78193cee
8c2dd7b3
Changes
6
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/dashboard_handler.go
View file @
eb60f670
...
...
@@ -249,11 +249,12 @@ func (h *DashboardHandler) GetUsageTrend(c *gin.Context) {
}
}
trend
,
err
:=
h
.
dashboardService
.
G
etUsageTrend
WithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
requestType
,
stream
,
billingType
)
trend
,
hit
,
err
:=
h
.
g
etUsageTrend
Cached
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
requestType
,
stream
,
billingType
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get usage trend"
)
return
}
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
))
response
.
Success
(
c
,
gin
.
H
{
"trend"
:
trend
,
...
...
@@ -321,11 +322,12 @@ func (h *DashboardHandler) GetModelStats(c *gin.Context) {
}
}
stats
,
err
:=
h
.
dashboardService
.
G
etModelStats
WithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
requestType
,
stream
,
billingType
)
stats
,
hit
,
err
:=
h
.
g
etModelStats
Cached
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
requestType
,
stream
,
billingType
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get model statistics"
)
return
}
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
))
response
.
Success
(
c
,
gin
.
H
{
"models"
:
stats
,
...
...
@@ -391,11 +393,12 @@ func (h *DashboardHandler) GetGroupStats(c *gin.Context) {
}
}
stats
,
err
:=
h
.
dashboardService
.
G
etGroupStats
WithFilters
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
requestType
,
stream
,
billingType
)
stats
,
hit
,
err
:=
h
.
g
etGroupStats
Cached
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
requestType
,
stream
,
billingType
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get group statistics"
)
return
}
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
))
response
.
Success
(
c
,
gin
.
H
{
"groups"
:
stats
,
...
...
@@ -416,11 +419,12 @@ func (h *DashboardHandler) GetAPIKeyUsageTrend(c *gin.Context) {
limit
=
5
}
trend
,
err
:=
h
.
dashboardService
.
G
etAPIKeyUsageTrend
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
limit
)
trend
,
hit
,
err
:=
h
.
g
etAPIKeyUsageTrend
Cached
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
limit
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get API key usage trend"
)
return
}
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
))
response
.
Success
(
c
,
gin
.
H
{
"trend"
:
trend
,
...
...
@@ -442,11 +446,12 @@ func (h *DashboardHandler) GetUserUsageTrend(c *gin.Context) {
limit
=
12
}
trend
,
err
:=
h
.
dashboardService
.
G
etUserUsageTrend
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
limit
)
trend
,
hit
,
err
:=
h
.
g
etUserUsageTrend
Cached
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
limit
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get user usage trend"
)
return
}
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
))
response
.
Success
(
c
,
gin
.
H
{
"trend"
:
trend
,
...
...
backend/internal/handler/admin/dashboard_handler_cache_test.go
0 → 100644
View file @
eb60f670
package
admin
import
(
"context"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
type
dashboardUsageRepoCacheProbe
struct
{
service
.
UsageLogRepository
trendCalls
atomic
.
Int32
usersTrendCalls
atomic
.
Int32
}
func
(
r
*
dashboardUsageRepoCacheProbe
)
GetUsageTrendWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
model
string
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
,
)
([]
usagestats
.
TrendDataPoint
,
error
)
{
r
.
trendCalls
.
Add
(
1
)
return
[]
usagestats
.
TrendDataPoint
{{
Date
:
"2026-03-11"
,
Requests
:
1
,
TotalTokens
:
2
,
Cost
:
3
,
ActualCost
:
4
,
}},
nil
}
func
(
r
*
dashboardUsageRepoCacheProbe
)
GetUserUsageTrend
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
,
)
([]
usagestats
.
UserUsageTrendPoint
,
error
)
{
r
.
usersTrendCalls
.
Add
(
1
)
return
[]
usagestats
.
UserUsageTrendPoint
{{
Date
:
"2026-03-11"
,
UserID
:
1
,
Email
:
"cache@test.dev"
,
Requests
:
2
,
Tokens
:
20
,
Cost
:
2
,
ActualCost
:
1
,
}},
nil
}
func
resetDashboardReadCachesForTest
()
{
dashboardTrendCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardUsersTrendCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardAPIKeysTrendCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardModelStatsCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardGroupStatsCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardSnapshotV2Cache
=
newSnapshotCache
(
30
*
time
.
Second
)
}
func
TestDashboardHandler_GetUsageTrend_UsesCache
(
t
*
testing
.
T
)
{
t
.
Cleanup
(
resetDashboardReadCachesForTest
)
resetDashboardReadCachesForTest
()
gin
.
SetMode
(
gin
.
TestMode
)
repo
:=
&
dashboardUsageRepoCacheProbe
{}
dashboardSvc
:=
service
.
NewDashboardService
(
repo
,
nil
,
nil
,
nil
)
handler
:=
NewDashboardHandler
(
dashboardSvc
,
nil
)
router
:=
gin
.
New
()
router
.
GET
(
"/admin/dashboard/trend"
,
handler
.
GetUsageTrend
)
req1
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/trend?start_date=2026-03-01&end_date=2026-03-07&granularity=day"
,
nil
)
rec1
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec1
,
req1
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec1
.
Code
)
require
.
Equal
(
t
,
"miss"
,
rec1
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
req2
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/trend?start_date=2026-03-01&end_date=2026-03-07&granularity=day"
,
nil
)
rec2
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec2
,
req2
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec2
.
Code
)
require
.
Equal
(
t
,
"hit"
,
rec2
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
require
.
Equal
(
t
,
int32
(
1
),
repo
.
trendCalls
.
Load
())
}
func
TestDashboardHandler_GetUserUsageTrend_UsesCache
(
t
*
testing
.
T
)
{
t
.
Cleanup
(
resetDashboardReadCachesForTest
)
resetDashboardReadCachesForTest
()
gin
.
SetMode
(
gin
.
TestMode
)
repo
:=
&
dashboardUsageRepoCacheProbe
{}
dashboardSvc
:=
service
.
NewDashboardService
(
repo
,
nil
,
nil
,
nil
)
handler
:=
NewDashboardHandler
(
dashboardSvc
,
nil
)
router
:=
gin
.
New
()
router
.
GET
(
"/admin/dashboard/users-trend"
,
handler
.
GetUserUsageTrend
)
req1
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/users-trend?start_date=2026-03-01&end_date=2026-03-07&granularity=day&limit=8"
,
nil
)
rec1
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec1
,
req1
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec1
.
Code
)
require
.
Equal
(
t
,
"miss"
,
rec1
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
req2
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/admin/dashboard/users-trend?start_date=2026-03-01&end_date=2026-03-07&granularity=day&limit=8"
,
nil
)
rec2
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
rec2
,
req2
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec2
.
Code
)
require
.
Equal
(
t
,
"hit"
,
rec2
.
Header
()
.
Get
(
"X-Snapshot-Cache"
))
require
.
Equal
(
t
,
int32
(
1
),
repo
.
usersTrendCalls
.
Load
())
}
backend/internal/handler/admin/dashboard_query_cache.go
0 → 100644
View file @
eb60f670
package
admin
import
(
"context"
"encoding/json"
"fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
)
var
(
dashboardTrendCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardModelStatsCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardGroupStatsCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardUsersTrendCache
=
newSnapshotCache
(
30
*
time
.
Second
)
dashboardAPIKeysTrendCache
=
newSnapshotCache
(
30
*
time
.
Second
)
)
type
dashboardTrendCacheKey
struct
{
StartTime
string
`json:"start_time"`
EndTime
string
`json:"end_time"`
Granularity
string
`json:"granularity"`
UserID
int64
`json:"user_id"`
APIKeyID
int64
`json:"api_key_id"`
AccountID
int64
`json:"account_id"`
GroupID
int64
`json:"group_id"`
Model
string
`json:"model"`
RequestType
*
int16
`json:"request_type"`
Stream
*
bool
`json:"stream"`
BillingType
*
int8
`json:"billing_type"`
}
type
dashboardModelGroupCacheKey
struct
{
StartTime
string
`json:"start_time"`
EndTime
string
`json:"end_time"`
UserID
int64
`json:"user_id"`
APIKeyID
int64
`json:"api_key_id"`
AccountID
int64
`json:"account_id"`
GroupID
int64
`json:"group_id"`
RequestType
*
int16
`json:"request_type"`
Stream
*
bool
`json:"stream"`
BillingType
*
int8
`json:"billing_type"`
}
type
dashboardEntityTrendCacheKey
struct
{
StartTime
string
`json:"start_time"`
EndTime
string
`json:"end_time"`
Granularity
string
`json:"granularity"`
Limit
int
`json:"limit"`
}
func
cacheStatusValue
(
hit
bool
)
string
{
if
hit
{
return
"hit"
}
return
"miss"
}
func
mustMarshalDashboardCacheKey
(
value
any
)
string
{
raw
,
err
:=
json
.
Marshal
(
value
)
if
err
!=
nil
{
return
""
}
return
string
(
raw
)
}
func
snapshotPayloadAs
[
T
any
](
payload
any
)
(
T
,
error
)
{
typed
,
ok
:=
payload
.
(
T
)
if
!
ok
{
var
zero
T
return
zero
,
fmt
.
Errorf
(
"unexpected cache payload type %T"
,
payload
)
}
return
typed
,
nil
}
func
(
h
*
DashboardHandler
)
getUsageTrendCached
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
model
string
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
,
)
([]
usagestats
.
TrendDataPoint
,
bool
,
error
)
{
key
:=
mustMarshalDashboardCacheKey
(
dashboardTrendCacheKey
{
StartTime
:
startTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
EndTime
:
endTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
Granularity
:
granularity
,
UserID
:
userID
,
APIKeyID
:
apiKeyID
,
AccountID
:
accountID
,
GroupID
:
groupID
,
Model
:
model
,
RequestType
:
requestType
,
Stream
:
stream
,
BillingType
:
billingType
,
})
entry
,
hit
,
err
:=
dashboardTrendCache
.
GetOrLoad
(
key
,
func
()
(
any
,
error
)
{
return
h
.
dashboardService
.
GetUsageTrendWithFilters
(
ctx
,
startTime
,
endTime
,
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
requestType
,
stream
,
billingType
)
})
if
err
!=
nil
{
return
nil
,
hit
,
err
}
trend
,
err
:=
snapshotPayloadAs
[[]
usagestats
.
TrendDataPoint
](
entry
.
Payload
)
return
trend
,
hit
,
err
}
func
(
h
*
DashboardHandler
)
getModelStatsCached
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
,
)
([]
usagestats
.
ModelStat
,
bool
,
error
)
{
key
:=
mustMarshalDashboardCacheKey
(
dashboardModelGroupCacheKey
{
StartTime
:
startTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
EndTime
:
endTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
UserID
:
userID
,
APIKeyID
:
apiKeyID
,
AccountID
:
accountID
,
GroupID
:
groupID
,
RequestType
:
requestType
,
Stream
:
stream
,
BillingType
:
billingType
,
})
entry
,
hit
,
err
:=
dashboardModelStatsCache
.
GetOrLoad
(
key
,
func
()
(
any
,
error
)
{
return
h
.
dashboardService
.
GetModelStatsWithFilters
(
ctx
,
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
requestType
,
stream
,
billingType
)
})
if
err
!=
nil
{
return
nil
,
hit
,
err
}
stats
,
err
:=
snapshotPayloadAs
[[]
usagestats
.
ModelStat
](
entry
.
Payload
)
return
stats
,
hit
,
err
}
func
(
h
*
DashboardHandler
)
getGroupStatsCached
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
,
)
([]
usagestats
.
GroupStat
,
bool
,
error
)
{
key
:=
mustMarshalDashboardCacheKey
(
dashboardModelGroupCacheKey
{
StartTime
:
startTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
EndTime
:
endTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
UserID
:
userID
,
APIKeyID
:
apiKeyID
,
AccountID
:
accountID
,
GroupID
:
groupID
,
RequestType
:
requestType
,
Stream
:
stream
,
BillingType
:
billingType
,
})
entry
,
hit
,
err
:=
dashboardGroupStatsCache
.
GetOrLoad
(
key
,
func
()
(
any
,
error
)
{
return
h
.
dashboardService
.
GetGroupStatsWithFilters
(
ctx
,
startTime
,
endTime
,
userID
,
apiKeyID
,
accountID
,
groupID
,
requestType
,
stream
,
billingType
)
})
if
err
!=
nil
{
return
nil
,
hit
,
err
}
stats
,
err
:=
snapshotPayloadAs
[[]
usagestats
.
GroupStat
](
entry
.
Payload
)
return
stats
,
hit
,
err
}
func
(
h
*
DashboardHandler
)
getAPIKeyUsageTrendCached
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
)
([]
usagestats
.
APIKeyUsageTrendPoint
,
bool
,
error
)
{
key
:=
mustMarshalDashboardCacheKey
(
dashboardEntityTrendCacheKey
{
StartTime
:
startTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
EndTime
:
endTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
Granularity
:
granularity
,
Limit
:
limit
,
})
entry
,
hit
,
err
:=
dashboardAPIKeysTrendCache
.
GetOrLoad
(
key
,
func
()
(
any
,
error
)
{
return
h
.
dashboardService
.
GetAPIKeyUsageTrend
(
ctx
,
startTime
,
endTime
,
granularity
,
limit
)
})
if
err
!=
nil
{
return
nil
,
hit
,
err
}
trend
,
err
:=
snapshotPayloadAs
[[]
usagestats
.
APIKeyUsageTrendPoint
](
entry
.
Payload
)
return
trend
,
hit
,
err
}
func
(
h
*
DashboardHandler
)
getUserUsageTrendCached
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
limit
int
)
([]
usagestats
.
UserUsageTrendPoint
,
bool
,
error
)
{
key
:=
mustMarshalDashboardCacheKey
(
dashboardEntityTrendCacheKey
{
StartTime
:
startTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
EndTime
:
endTime
.
UTC
()
.
Format
(
time
.
RFC3339
),
Granularity
:
granularity
,
Limit
:
limit
,
})
entry
,
hit
,
err
:=
dashboardUsersTrendCache
.
GetOrLoad
(
key
,
func
()
(
any
,
error
)
{
return
h
.
dashboardService
.
GetUserUsageTrend
(
ctx
,
startTime
,
endTime
,
granularity
,
limit
)
})
if
err
!=
nil
{
return
nil
,
hit
,
err
}
trend
,
err
:=
snapshotPayloadAs
[[]
usagestats
.
UserUsageTrendPoint
](
entry
.
Payload
)
return
trend
,
hit
,
err
}
backend/internal/handler/admin/dashboard_snapshot_v2_handler.go
View file @
eb60f670
package
admin
import
(
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
...
...
@@ -111,7 +113,25 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
})
cacheKey
:=
string
(
keyRaw
)
if
cached
,
ok
:=
dashboardSnapshotV2Cache
.
Get
(
cacheKey
);
ok
{
cached
,
hit
,
err
:=
dashboardSnapshotV2Cache
.
GetOrLoad
(
cacheKey
,
func
()
(
any
,
error
)
{
return
h
.
buildSnapshotV2Response
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
filters
,
includeStats
,
includeTrend
,
includeModels
,
includeGroups
,
includeUsersTrend
,
usersTrendLimit
,
)
})
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
err
.
Error
())
return
}
if
cached
.
ETag
!=
""
{
c
.
Header
(
"ETag"
,
cached
.
ETag
)
c
.
Header
(
"Vary"
,
"If-None-Match"
)
...
...
@@ -120,11 +140,18 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
return
}
}
c
.
Header
(
"X-Snapshot-Cache"
,
"
hit
"
)
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
)
)
response
.
Success
(
c
,
cached
.
Payload
)
return
}
}
func
(
h
*
DashboardHandler
)
buildSnapshotV2Response
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
filters
*
dashboardSnapshotV2Filters
,
includeStats
,
includeTrend
,
includeModels
,
includeGroups
,
includeUsersTrend
bool
,
usersTrendLimit
int
,
)
(
*
dashboardSnapshotV2Response
,
error
)
{
resp
:=
&
dashboardSnapshotV2Response
{
GeneratedAt
:
time
.
Now
()
.
UTC
()
.
Format
(
time
.
RFC3339
),
StartDate
:
startTime
.
Format
(
"2006-01-02"
),
...
...
@@ -133,10 +160,9 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
}
if
includeStats
{
stats
,
err
:=
h
.
dashboardService
.
GetDashboardStats
(
c
.
Request
.
Context
()
)
stats
,
err
:=
h
.
dashboardService
.
GetDashboardStats
(
c
tx
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get dashboard statistics"
)
return
return
nil
,
errors
.
New
(
"failed to get dashboard statistics"
)
}
resp
.
Stats
=
&
dashboardSnapshotV2Stats
{
DashboardStats
:
*
stats
,
...
...
@@ -145,8 +171,8 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
}
if
includeTrend
{
trend
,
err
:=
h
.
dashboardService
.
G
etUsageTrend
WithFilters
(
c
.
Request
.
Context
()
,
trend
,
_
,
err
:=
h
.
g
etUsageTrend
Cached
(
c
tx
,
startTime
,
endTime
,
granularity
,
...
...
@@ -160,15 +186,14 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
filters
.
BillingType
,
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get usage trend"
)
return
return
nil
,
errors
.
New
(
"failed to get usage trend"
)
}
resp
.
Trend
=
trend
}
if
includeModels
{
models
,
err
:=
h
.
dashboardService
.
G
etModelStats
WithFilters
(
c
.
Request
.
Context
()
,
models
,
_
,
err
:=
h
.
g
etModelStats
Cached
(
c
tx
,
startTime
,
endTime
,
filters
.
UserID
,
...
...
@@ -180,15 +205,14 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
filters
.
BillingType
,
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get model statistics"
)
return
return
nil
,
errors
.
New
(
"failed to get model statistics"
)
}
resp
.
Models
=
models
}
if
includeGroups
{
groups
,
err
:=
h
.
dashboardService
.
G
etGroupStats
WithFilters
(
c
.
Request
.
Context
()
,
groups
,
_
,
err
:=
h
.
g
etGroupStats
Cached
(
c
tx
,
startTime
,
endTime
,
filters
.
UserID
,
...
...
@@ -200,34 +224,20 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
filters
.
BillingType
,
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get group statistics"
)
return
return
nil
,
errors
.
New
(
"failed to get group statistics"
)
}
resp
.
Groups
=
groups
}
if
includeUsersTrend
{
usersTrend
,
err
:=
h
.
dashboardService
.
GetUserUsageTrend
(
c
.
Request
.
Context
(),
startTime
,
endTime
,
granularity
,
usersTrendLimit
,
)
usersTrend
,
_
,
err
:=
h
.
getUserUsageTrendCached
(
ctx
,
startTime
,
endTime
,
granularity
,
usersTrendLimit
)
if
err
!=
nil
{
response
.
Error
(
c
,
500
,
"Failed to get user usage trend"
)
return
return
nil
,
errors
.
New
(
"failed to get user usage trend"
)
}
resp
.
UsersTrend
=
usersTrend
}
cached
:=
dashboardSnapshotV2Cache
.
Set
(
cacheKey
,
resp
)
if
cached
.
ETag
!=
""
{
c
.
Header
(
"ETag"
,
cached
.
ETag
)
c
.
Header
(
"Vary"
,
"If-None-Match"
)
}
c
.
Header
(
"X-Snapshot-Cache"
,
"miss"
)
response
.
Success
(
c
,
resp
)
return
resp
,
nil
}
func
parseDashboardSnapshotV2Filters
(
c
*
gin
.
Context
)
(
*
dashboardSnapshotV2Filters
,
error
)
{
...
...
backend/internal/handler/admin/snapshot_cache.go
View file @
eb60f670
...
...
@@ -7,6 +7,8 @@ import (
"strings"
"sync"
"time"
"golang.org/x/sync/singleflight"
)
type
snapshotCacheEntry
struct
{
...
...
@@ -19,6 +21,12 @@ type snapshotCache struct {
mu
sync
.
RWMutex
ttl
time
.
Duration
items
map
[
string
]
snapshotCacheEntry
sf
singleflight
.
Group
}
type
snapshotCacheLoadResult
struct
{
Entry
snapshotCacheEntry
Hit
bool
}
func
newSnapshotCache
(
ttl
time
.
Duration
)
*
snapshotCache
{
...
...
@@ -70,6 +78,41 @@ func (c *snapshotCache) Set(key string, payload any) snapshotCacheEntry {
return
entry
}
func
(
c
*
snapshotCache
)
GetOrLoad
(
key
string
,
load
func
()
(
any
,
error
))
(
snapshotCacheEntry
,
bool
,
error
)
{
if
load
==
nil
{
return
snapshotCacheEntry
{},
false
,
nil
}
if
entry
,
ok
:=
c
.
Get
(
key
);
ok
{
return
entry
,
true
,
nil
}
if
c
==
nil
||
key
==
""
{
payload
,
err
:=
load
()
if
err
!=
nil
{
return
snapshotCacheEntry
{},
false
,
err
}
return
c
.
Set
(
key
,
payload
),
false
,
nil
}
value
,
err
,
_
:=
c
.
sf
.
Do
(
key
,
func
()
(
any
,
error
)
{
if
entry
,
ok
:=
c
.
Get
(
key
);
ok
{
return
snapshotCacheLoadResult
{
Entry
:
entry
,
Hit
:
true
},
nil
}
payload
,
err
:=
load
()
if
err
!=
nil
{
return
nil
,
err
}
return
snapshotCacheLoadResult
{
Entry
:
c
.
Set
(
key
,
payload
),
Hit
:
false
},
nil
})
if
err
!=
nil
{
return
snapshotCacheEntry
{},
false
,
err
}
result
,
ok
:=
value
.
(
snapshotCacheLoadResult
)
if
!
ok
{
return
snapshotCacheEntry
{},
false
,
nil
}
return
result
.
Entry
,
result
.
Hit
,
nil
}
func
buildETagFromAny
(
payload
any
)
string
{
raw
,
err
:=
json
.
Marshal
(
payload
)
if
err
!=
nil
{
...
...
backend/internal/handler/admin/snapshot_cache_test.go
View file @
eb60f670
...
...
@@ -3,6 +3,8 @@
package
admin
import
(
"sync"
"sync/atomic"
"testing"
"time"
...
...
@@ -95,6 +97,61 @@ func TestBuildETagFromAny_UnmarshalablePayload(t *testing.T) {
require
.
Empty
(
t
,
etag
)
}
func
TestSnapshotCache_GetOrLoad_MissThenHit
(
t
*
testing
.
T
)
{
c
:=
newSnapshotCache
(
5
*
time
.
Second
)
var
loads
atomic
.
Int32
entry
,
hit
,
err
:=
c
.
GetOrLoad
(
"key1"
,
func
()
(
any
,
error
)
{
loads
.
Add
(
1
)
return
map
[
string
]
string
{
"hello"
:
"world"
},
nil
})
require
.
NoError
(
t
,
err
)
require
.
False
(
t
,
hit
)
require
.
NotEmpty
(
t
,
entry
.
ETag
)
require
.
Equal
(
t
,
int32
(
1
),
loads
.
Load
())
entry2
,
hit
,
err
:=
c
.
GetOrLoad
(
"key1"
,
func
()
(
any
,
error
)
{
loads
.
Add
(
1
)
return
map
[
string
]
string
{
"unexpected"
:
"value"
},
nil
})
require
.
NoError
(
t
,
err
)
require
.
True
(
t
,
hit
)
require
.
Equal
(
t
,
entry
.
ETag
,
entry2
.
ETag
)
require
.
Equal
(
t
,
int32
(
1
),
loads
.
Load
())
}
func
TestSnapshotCache_GetOrLoad_ConcurrentSingleflight
(
t
*
testing
.
T
)
{
c
:=
newSnapshotCache
(
5
*
time
.
Second
)
var
loads
atomic
.
Int32
start
:=
make
(
chan
struct
{})
const
callers
=
8
errCh
:=
make
(
chan
error
,
callers
)
var
wg
sync
.
WaitGroup
wg
.
Add
(
callers
)
for
range
callers
{
go
func
()
{
defer
wg
.
Done
()
<-
start
_
,
_
,
err
:=
c
.
GetOrLoad
(
"shared"
,
func
()
(
any
,
error
)
{
loads
.
Add
(
1
)
time
.
Sleep
(
20
*
time
.
Millisecond
)
return
"value"
,
nil
})
errCh
<-
err
}()
}
close
(
start
)
wg
.
Wait
()
close
(
errCh
)
for
err
:=
range
errCh
{
require
.
NoError
(
t
,
err
)
}
require
.
Equal
(
t
,
int32
(
1
),
loads
.
Load
())
}
func
TestParseBoolQueryWithDefault
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment