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
4167c437
Commit
4167c437
authored
Mar 11, 2026
by
ius
Browse files
Reduce admin dashboard read amplification
parent
7455476c
Changes
6
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/dashboard_handler.go
View file @
4167c437
...
...
@@ -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 @
4167c437
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 @
4167c437
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 @
4167c437
package
admin
import
(
"context"
"encoding/json"
"errors"
"net/http"
"strconv"
"strings"
...
...
@@ -111,20 +113,45 @@ func (h *DashboardHandler) GetSnapshotV2(c *gin.Context) {
})
cacheKey
:=
string
(
keyRaw
)
if
cached
,
ok
:=
dashboardSnapshotV2Cache
.
Get
(
cacheKey
);
ok
{
if
cached
.
ETag
!=
""
{
c
.
Header
(
"ETag"
,
cached
.
ETag
)
c
.
Header
(
"Vary"
,
"If-None-Match"
)
if
ifNoneMatchMatched
(
c
.
GetHeader
(
"If-None-Match"
),
cached
.
ETag
)
{
c
.
Status
(
http
.
StatusNotModified
)
return
}
}
c
.
Header
(
"X-Snapshot-Cache"
,
"hit"
)
response
.
Success
(
c
,
cached
.
Payload
)
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"
)
if
ifNoneMatchMatched
(
c
.
GetHeader
(
"If-None-Match"
),
cached
.
ETag
)
{
c
.
Status
(
http
.
StatusNotModified
)
return
}
}
c
.
Header
(
"X-Snapshot-Cache"
,
cacheStatusValue
(
hit
))
response
.
Success
(
c
,
cached
.
Payload
)
}
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 @
4167c437
...
...
@@ -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 @
4167c437
...
...
@@ -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