149 lines
4.4 KiB
Go
149 lines
4.4 KiB
Go
package mtprotoedge
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"hash/maphash"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
rpcResultSubscriberMaxGlobal = 1 << 16
|
|
rpcResultSubscriberMaxAuth = 1 << 13
|
|
rpcResultSubscriberMaxSession = 1 << 11
|
|
rpcResultSubscriberMaxPerFlight = 1 << 7
|
|
)
|
|
|
|
type rpcResultSubscriberBudgetShard[K comparable] struct {
|
|
mu sync.Mutex
|
|
usage map[K]int64
|
|
}
|
|
|
|
// rpcResultSubscriberBudget bounds callbacks retained by pending replay
|
|
// flights independently from owner/result bytes. A duplicate does not reserve a
|
|
// new result row, so charging only unique owners would otherwise leave an
|
|
// unbounded same-msg-id reconnect path.
|
|
type rpcResultSubscriberBudget struct {
|
|
seed maphash.Seed
|
|
global rpcResultFlightLimit
|
|
authLimit int64
|
|
sessionLimit int64
|
|
authShards [rpcExecutionBudgetShards]rpcResultSubscriberBudgetShard[[8]byte]
|
|
sessionShards [rpcExecutionBudgetShards]rpcResultSubscriberBudgetShard[rpcExecutionSessionBudgetKey]
|
|
}
|
|
|
|
func newRPCResultSubscriberBudget(
|
|
seed maphash.Seed,
|
|
globalLimit, authLimit, sessionLimit int,
|
|
) *rpcResultSubscriberBudget {
|
|
b := &rpcResultSubscriberBudget{
|
|
seed: seed,
|
|
authLimit: int64(authLimit),
|
|
sessionLimit: int64(sessionLimit),
|
|
}
|
|
b.global.max = int64(globalLimit)
|
|
for i := range b.authShards {
|
|
b.authShards[i].usage = make(map[[8]byte]int64)
|
|
b.sessionShards[i].usage = make(map[rpcExecutionSessionBudgetKey]int64)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (b *rpcResultSubscriberBudget) reserve(key rpcExecutionKey, slots int) bool {
|
|
if b == nil || slots <= 0 {
|
|
return false
|
|
}
|
|
authShard := b.authShard(key.authKeyID)
|
|
sessionKey := rpcExecutionSessionBudgetKey{authKeyID: key.authKeyID, sessionID: key.sessionID}
|
|
sessionShard := b.sessionShard(sessionKey)
|
|
delta := int64(slots)
|
|
authShard.mu.Lock()
|
|
sessionShard.mu.Lock()
|
|
authUsed := authShard.usage[key.authKeyID]
|
|
sessionUsed := sessionShard.usage[sessionKey]
|
|
if !withinRPCExecutionBudget(authUsed, delta, b.authLimit) ||
|
|
!withinRPCExecutionBudget(sessionUsed, delta, b.sessionLimit) ||
|
|
!b.global.reserveN(delta) {
|
|
sessionShard.mu.Unlock()
|
|
authShard.mu.Unlock()
|
|
return false
|
|
}
|
|
authShard.usage[key.authKeyID] = authUsed + delta
|
|
sessionShard.usage[sessionKey] = sessionUsed + delta
|
|
sessionShard.mu.Unlock()
|
|
authShard.mu.Unlock()
|
|
return true
|
|
}
|
|
|
|
func (b *rpcResultSubscriberBudget) release(key rpcExecutionKey, slots int) {
|
|
if b == nil || slots <= 0 {
|
|
panic("mtproto rpc result subscriber release must be positive")
|
|
}
|
|
authShard := b.authShard(key.authKeyID)
|
|
sessionKey := rpcExecutionSessionBudgetKey{authKeyID: key.authKeyID, sessionID: key.sessionID}
|
|
sessionShard := b.sessionShard(sessionKey)
|
|
delta := int64(slots)
|
|
authShard.mu.Lock()
|
|
sessionShard.mu.Lock()
|
|
authUsed, authOK := authShard.usage[key.authKeyID]
|
|
sessionUsed, sessionOK := sessionShard.usage[sessionKey]
|
|
if !authOK || !sessionOK || authUsed < delta || sessionUsed < delta {
|
|
sessionShard.mu.Unlock()
|
|
authShard.mu.Unlock()
|
|
panic("mtproto rpc result subscriber budget underflow")
|
|
}
|
|
authUsed -= delta
|
|
sessionUsed -= delta
|
|
if authUsed == 0 {
|
|
delete(authShard.usage, key.authKeyID)
|
|
} else {
|
|
authShard.usage[key.authKeyID] = authUsed
|
|
}
|
|
if sessionUsed == 0 {
|
|
delete(sessionShard.usage, sessionKey)
|
|
} else {
|
|
sessionShard.usage[sessionKey] = sessionUsed
|
|
}
|
|
b.global.releaseN(delta)
|
|
sessionShard.mu.Unlock()
|
|
authShard.mu.Unlock()
|
|
}
|
|
|
|
func (b *rpcResultSubscriberBudget) authSnapshot(authKeyID [8]byte) int64 {
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
shard := b.authShard(authKeyID)
|
|
shard.mu.Lock()
|
|
used := shard.usage[authKeyID]
|
|
shard.mu.Unlock()
|
|
return used
|
|
}
|
|
|
|
func (b *rpcResultSubscriberBudget) sessionSnapshot(authKeyID [8]byte, sessionID int64) int64 {
|
|
if b == nil {
|
|
return 0
|
|
}
|
|
key := rpcExecutionSessionBudgetKey{authKeyID: authKeyID, sessionID: sessionID}
|
|
shard := b.sessionShard(key)
|
|
shard.mu.Lock()
|
|
used := shard.usage[key]
|
|
shard.mu.Unlock()
|
|
return used
|
|
}
|
|
|
|
func (b *rpcResultSubscriberBudget) authShard(
|
|
authKeyID [8]byte,
|
|
) *rpcResultSubscriberBudgetShard[[8]byte] {
|
|
index := maphash.Bytes(b.seed, authKeyID[:]) & (rpcExecutionBudgetShards - 1)
|
|
return &b.authShards[index]
|
|
}
|
|
|
|
func (b *rpcResultSubscriberBudget) sessionShard(
|
|
key rpcExecutionSessionBudgetKey,
|
|
) *rpcResultSubscriberBudgetShard[rpcExecutionSessionBudgetKey] {
|
|
var raw [16]byte
|
|
copy(raw[:8], key.authKeyID[:])
|
|
binary.LittleEndian.PutUint64(raw[8:], uint64(key.sessionID))
|
|
index := maphash.Bytes(b.seed, raw[:]) & (rpcExecutionBudgetShards - 1)
|
|
return &b.sessionShards[index]
|
|
}
|