feat: sync multilayer td integration
This commit is contained in:
parent
20a310f6ca
commit
766c5db992
491 changed files with 26235 additions and 35340 deletions
|
|
@ -3,17 +3,39 @@ package mtprotoedge
|
|||
import (
|
||||
"container/list"
|
||||
"encoding/binary"
|
||||
"hash/maphash"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
rpcResultCacheTTL = 3 * time.Minute
|
||||
rpcResultCacheMaxEntries = 4096
|
||||
rpcResultCacheMaxBytes = 64 << 20
|
||||
// rpcResultCacheShards 把缓存按 (auth_key_id, session_id) 分片:每条 RPC 都要
|
||||
// Get(重复检测)+ Put(结果缓存),单把全局锁会让所有连接的 RPC 热路径在
|
||||
// 一个 mutex 上汇聚(同 P0-5 的 SessionManager 教训)。分片数为 2 的幂。
|
||||
// Telegram accepts client msg_id values up to five minutes old and up to
|
||||
// thirty seconds in the future. Retain the result across that complete
|
||||
// replay horizon, plus one second for boundary/scheduler jitter, so a valid
|
||||
// duplicate cannot rerun its handler merely because our cache expired first.
|
||||
rpcResultCacheTTL = 331 * time.Second
|
||||
// Completed results cover the complete replay horizon under explicit global,
|
||||
// auth and session hard ceilings. At the default 331-second TTL, the 1<<18
|
||||
// global entries permit about 792 unique RPC/s process-wide before bounded
|
||||
// backpressure; lower scopes provide noisy-neighbor isolation.
|
||||
rpcResultCacheMaxEntries = 1 << 18
|
||||
rpcResultCacheMaxBytes = 64 << 20
|
||||
rpcResultCacheAuthMaxEntries = 1 << 15
|
||||
rpcResultCacheAuthMaxBytes = 32 << 20
|
||||
rpcResultCacheSessionMaxEntries = 1 << 14
|
||||
rpcResultCacheSessionMaxBytes = 16 << 20
|
||||
rpcResultFlightMaxPendingPerAuth = 1 << 11
|
||||
// Keep every transport-legal rpc_result cacheable. Converting the constant
|
||||
// difference to uint64 intentionally fails compilation if a future transport
|
||||
// limit grows beyond the completed-result budget.
|
||||
_ = uint64(rpcResultCacheMaxBytes - maxOutboundBodyBytes)
|
||||
_ = uint64(rpcResultCacheAuthMaxBytes - maxOutboundBodyBytes)
|
||||
_ = uint64(rpcResultCacheSessionMaxBytes - maxOutboundBodyBytes)
|
||||
// rpcResultCacheShards hashes the complete replay identity with a random
|
||||
// per-instance maphash seed. Including req_msg_id spreads one hot session's
|
||||
// independent requests instead of forcing them through one mutex. The shard
|
||||
// count is a power of two.
|
||||
rpcResultCacheShards = 16
|
||||
)
|
||||
|
||||
|
|
@ -24,10 +46,28 @@ type rpcResultCacheKey struct {
|
|||
}
|
||||
|
||||
type rpcResultCacheEntry struct {
|
||||
key rpcResultCacheKey
|
||||
encoded *encodedOutboundMessage
|
||||
size int
|
||||
expiresAt time.Time
|
||||
key rpcResultCacheKey
|
||||
encoded *encodedOutboundMessage
|
||||
size int
|
||||
expiresAt time.Time
|
||||
identity rpcResultRequestIdentity
|
||||
admissionSeq uint64
|
||||
executionKnown bool
|
||||
executionOK bool
|
||||
// capacity marks a bounded replay tombstone. The original owner and its
|
||||
// already-joined waiters received encoded, but the byte budget could not
|
||||
// retain that body. Keeping the immutable identity until TTL prevents a
|
||||
// duplicate from rerunning business; Acquire returns a capacity error.
|
||||
capacity bool
|
||||
// reservation is the same global+auth+session ownership acquired before the
|
||||
// handler ran. Put transfers it from the pending flight; TTL returns it.
|
||||
reservation *rpcResultBudgetReservation
|
||||
}
|
||||
|
||||
type rpcResultDependency struct {
|
||||
waiter *rpcResultWaiter
|
||||
completed bool
|
||||
success bool
|
||||
}
|
||||
|
||||
// rpcResultCache 缓存已有交付证明的 rpc_result(按 auth_key+session+req_msg_id),
|
||||
|
|
@ -36,16 +76,35 @@ type rpcResultCacheEntry struct {
|
|||
// encodedOutboundMessage 构造后不可变(push fan-out 与 pending resend 均依赖该契约),
|
||||
// 因此 Get/Put 直接共享指针,不做防御性拷贝。
|
||||
type rpcResultCache struct {
|
||||
shards [rpcResultCacheShards]rpcResultCacheShard
|
||||
flightLimit rpcResultFlightLimit
|
||||
shards [rpcResultCacheShards]rpcResultCacheShard
|
||||
hashSeed maphash.Seed
|
||||
completedBytes rpcResultCacheByteBudget
|
||||
completedEntries rpcResultFlightLimit
|
||||
fairBudget *rpcResultFairBudget
|
||||
flightLimit rpcResultFlightLimit
|
||||
subscriberBudget *rpcResultSubscriberBudget
|
||||
subscriberPerFlight int
|
||||
// nextAdmissionSeq is the process-wide ordering authority for auth-key
|
||||
// shared Layer defaults. Exact owners allocate once; joins/replays retain the
|
||||
// owner's value from their flight/completed descriptor.
|
||||
nextAdmissionSeq atomic.Uint64
|
||||
activeAdmissions rpcAdmissionTracker
|
||||
}
|
||||
|
||||
func (c *rpcResultCache) stableAdmissionSafeFloor() uint64 {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
return c.activeAdmissions.stableSafeFloor(&c.nextAdmissionSeq)
|
||||
}
|
||||
|
||||
type rpcResultCacheShard struct {
|
||||
mu sync.Mutex
|
||||
now func() time.Time
|
||||
ttl time.Duration
|
||||
mu sync.Mutex
|
||||
now func() time.Time
|
||||
ttl time.Duration
|
||||
// maxEntries is a focused-test seam for one physical shard. Production leaves
|
||||
// it zero and uses the explicit global/auth/session fair-budget hierarchy.
|
||||
maxEntries int
|
||||
maxBytes int
|
||||
bytes int
|
||||
order *list.List
|
||||
byKey map[rpcResultCacheKey]*list.Element
|
||||
|
|
@ -56,20 +115,129 @@ type rpcResultCacheShard struct {
|
|||
}
|
||||
|
||||
func newRPCResultCacheWithFlightLimit(now func() time.Time, maxPending int) *rpcResultCache {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
if maxPending <= 0 {
|
||||
maxPending = rpcResultFlightDefaultMaxPending
|
||||
}
|
||||
c := &rpcResultCache{}
|
||||
c.flightLimit.max = int64(maxPending)
|
||||
pendingPerAuth := rpcResultFlightMaxPendingPerAuth
|
||||
if pendingPerAuth > maxPending {
|
||||
pendingPerAuth = maxPending
|
||||
}
|
||||
return newRPCResultCacheWithFairCapacity(now, rpcResultCacheCapacity{
|
||||
maxPending: maxPending,
|
||||
maxPendingPerAuth: pendingPerAuth,
|
||||
globalMaxBytes: rpcResultCacheMaxBytes,
|
||||
globalMaxEntries: rpcResultCacheMaxEntries,
|
||||
authMaxBytes: rpcResultCacheAuthMaxBytes,
|
||||
authMaxEntries: rpcResultCacheAuthMaxEntries,
|
||||
sessionMaxBytes: rpcResultCacheSessionMaxBytes,
|
||||
sessionMaxEntries: rpcResultCacheSessionMaxEntries,
|
||||
})
|
||||
}
|
||||
|
||||
func newRPCResultCacheWithLimits(now func() time.Time, maxPending, maxCompletedBytes int) *rpcResultCache {
|
||||
return newRPCResultCacheWithCapacity(now, maxPending, int64(maxCompletedBytes), rpcResultCacheMaxEntries)
|
||||
}
|
||||
|
||||
func newRPCResultCacheWithCapacity(
|
||||
now func() time.Time,
|
||||
maxPending int,
|
||||
maxCompletedBytes int64,
|
||||
maxCompletedEntries int,
|
||||
) *rpcResultCache {
|
||||
// Compatibility/test constructor: the caller supplied only global limits, so
|
||||
// keep every fairness scope equal to that global ceiling. Production always
|
||||
// calls newRPCResultCacheWithFairCapacity with explicit auth/session limits.
|
||||
return newRPCResultCacheWithFairCapacity(now, rpcResultCacheCapacity{
|
||||
maxPending: maxPending,
|
||||
maxPendingPerAuth: maxPending,
|
||||
globalMaxBytes: maxCompletedBytes,
|
||||
globalMaxEntries: maxCompletedEntries,
|
||||
authMaxBytes: maxCompletedBytes,
|
||||
authMaxEntries: maxCompletedEntries,
|
||||
sessionMaxBytes: maxCompletedBytes,
|
||||
sessionMaxEntries: maxCompletedEntries,
|
||||
})
|
||||
}
|
||||
|
||||
type rpcResultCacheCapacity struct {
|
||||
maxPending int
|
||||
maxPendingPerAuth int
|
||||
globalMaxBytes int64
|
||||
globalMaxEntries int
|
||||
authMaxBytes int64
|
||||
authMaxEntries int
|
||||
sessionMaxBytes int64
|
||||
sessionMaxEntries int
|
||||
subscriberMaxGlobal int
|
||||
subscriberMaxAuth int
|
||||
subscriberMaxSession int
|
||||
subscriberMaxPerFlight int
|
||||
}
|
||||
|
||||
func newRPCResultCacheWithFairCapacity(now func() time.Time, capacity rpcResultCacheCapacity) *rpcResultCache {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
if capacity.maxPending <= 0 {
|
||||
capacity.maxPending = rpcResultFlightDefaultMaxPending
|
||||
}
|
||||
if capacity.maxPendingPerAuth <= 0 {
|
||||
capacity.maxPendingPerAuth = capacity.maxPending
|
||||
}
|
||||
if capacity.globalMaxBytes <= 0 {
|
||||
capacity.globalMaxBytes = rpcResultCacheMaxBytes
|
||||
}
|
||||
if capacity.globalMaxEntries <= 0 {
|
||||
capacity.globalMaxEntries = rpcResultCacheMaxEntries
|
||||
}
|
||||
if capacity.authMaxBytes <= 0 {
|
||||
capacity.authMaxBytes = capacity.globalMaxBytes
|
||||
}
|
||||
if capacity.authMaxEntries <= 0 {
|
||||
capacity.authMaxEntries = capacity.globalMaxEntries
|
||||
}
|
||||
if capacity.sessionMaxBytes <= 0 {
|
||||
capacity.sessionMaxBytes = capacity.authMaxBytes
|
||||
}
|
||||
if capacity.sessionMaxEntries <= 0 {
|
||||
capacity.sessionMaxEntries = capacity.authMaxEntries
|
||||
}
|
||||
if capacity.subscriberMaxGlobal <= 0 {
|
||||
capacity.subscriberMaxGlobal = rpcResultSubscriberMaxGlobal
|
||||
}
|
||||
if capacity.subscriberMaxAuth <= 0 {
|
||||
capacity.subscriberMaxAuth = rpcResultSubscriberMaxAuth
|
||||
}
|
||||
if capacity.subscriberMaxSession <= 0 {
|
||||
capacity.subscriberMaxSession = rpcResultSubscriberMaxSession
|
||||
}
|
||||
if capacity.subscriberMaxPerFlight <= 0 {
|
||||
capacity.subscriberMaxPerFlight = rpcResultSubscriberMaxPerFlight
|
||||
}
|
||||
c := &rpcResultCache{hashSeed: maphash.MakeSeed()}
|
||||
c.completedBytes.max = capacity.globalMaxBytes
|
||||
c.completedEntries.max = int64(capacity.globalMaxEntries)
|
||||
c.flightLimit.max = int64(capacity.maxPending)
|
||||
c.fairBudget = newRPCResultFairBudget(
|
||||
c.hashSeed,
|
||||
&c.completedEntries,
|
||||
&c.completedBytes,
|
||||
rpcResultBudgetLimit{entries: int64(capacity.authMaxEntries), bytes: capacity.authMaxBytes},
|
||||
rpcResultBudgetLimit{entries: int64(capacity.sessionMaxEntries), bytes: capacity.sessionMaxBytes},
|
||||
capacity.maxPendingPerAuth,
|
||||
)
|
||||
c.subscriberBudget = newRPCResultSubscriberBudget(
|
||||
c.hashSeed,
|
||||
capacity.subscriberMaxGlobal,
|
||||
capacity.subscriberMaxAuth,
|
||||
capacity.subscriberMaxSession,
|
||||
)
|
||||
c.subscriberPerFlight = capacity.subscriberMaxPerFlight
|
||||
for i := range c.shards {
|
||||
s := &c.shards[i]
|
||||
s.now = now
|
||||
s.ttl = rpcResultCacheTTL
|
||||
s.maxEntries = rpcResultCacheMaxEntries / rpcResultCacheShards
|
||||
s.maxBytes = rpcResultCacheMaxBytes / rpcResultCacheShards
|
||||
s.maxEntries = 0
|
||||
s.order = list.New()
|
||||
s.byKey = make(map[rpcResultCacheKey]*list.Element)
|
||||
s.pending = make(map[rpcResultCacheKey]*rpcResultFlight)
|
||||
|
|
@ -78,10 +246,16 @@ func newRPCResultCacheWithFlightLimit(now func() time.Time, maxPending int) *rpc
|
|||
}
|
||||
|
||||
func (c *rpcResultCache) shard(key rpcResultCacheKey) *rpcResultCacheShard {
|
||||
// auth_key_id 与 session_id 都是均匀随机的 64-bit 值,异或折叠后取低位即可。
|
||||
h := binary.LittleEndian.Uint64(key.authKeyID[:]) ^ uint64(key.sessionID)
|
||||
h ^= h >> 32
|
||||
return &c.shards[h&(rpcResultCacheShards-1)]
|
||||
return &c.shards[c.shardIndex(key)]
|
||||
|
||||
}
|
||||
|
||||
func (c *rpcResultCache) shardIndex(key rpcResultCacheKey) uint64 {
|
||||
var raw [24]byte
|
||||
copy(raw[:8], key.authKeyID[:])
|
||||
binary.LittleEndian.PutUint64(raw[8:16], uint64(key.sessionID))
|
||||
binary.LittleEndian.PutUint64(raw[16:24], uint64(key.reqMsgID))
|
||||
return maphash.Bytes(c.hashSeed, raw[:]) & (rpcResultCacheShards - 1)
|
||||
}
|
||||
|
||||
func (c *rpcResultCache) Get(authKeyID [8]byte, sessionID, reqMsgID int64) (*encodedOutboundMessage, bool) {
|
||||
|
|
@ -104,45 +278,210 @@ func (c *rpcResultCache) Get(authKeyID [8]byte, sessionID, reqMsgID int64) (*enc
|
|||
s.removeElement(elem)
|
||||
return nil, false
|
||||
}
|
||||
if entry.capacity || entry.encoded == nil {
|
||||
return nil, false
|
||||
}
|
||||
return entry.encoded, true
|
||||
}
|
||||
|
||||
// ObserveDependency returns a waiter for an admitted in-flight dependency, a
|
||||
// nil waiter for an already completed dependency, or ok=false when the
|
||||
// referenced message never established API-RPC ownership. It never creates a
|
||||
// flight and therefore cannot turn a forged invokeAfterMsg into authority to
|
||||
// run another request.
|
||||
func (c *rpcResultCache) ObserveDependency(authKeyID [8]byte, sessionID, reqMsgID int64) (rpcResultDependency, bool) {
|
||||
if c == nil || reqMsgID == 0 {
|
||||
return rpcResultDependency{}, false
|
||||
}
|
||||
key := rpcResultCacheKey{authKeyID: authKeyID, sessionID: sessionID, reqMsgID: reqMsgID}
|
||||
s := c.shard(key)
|
||||
now := s.now()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if elem, exists := s.byKey[key]; exists {
|
||||
entry := elem.Value.(*rpcResultCacheEntry)
|
||||
if entry.expiresAt.After(now) {
|
||||
if !entry.executionKnown {
|
||||
return rpcResultDependency{}, false
|
||||
}
|
||||
return rpcResultDependency{completed: true, success: entry.executionOK}, true
|
||||
}
|
||||
s.removeElement(elem)
|
||||
}
|
||||
if flight := s.pending[key]; flight != nil {
|
||||
if flight.executionDone {
|
||||
return rpcResultDependency{completed: true, success: flight.executionOK}, true
|
||||
}
|
||||
return rpcResultDependency{waiter: &rpcResultWaiter{cache: c, key: key, flight: flight}}, true
|
||||
}
|
||||
return rpcResultDependency{}, false
|
||||
}
|
||||
|
||||
func (c *rpcResultCache) Put(authKeyID [8]byte, sessionID, reqMsgID int64, encoded *encodedOutboundMessage) {
|
||||
if c == nil || reqMsgID == 0 || encoded == nil {
|
||||
return
|
||||
}
|
||||
if c.putOnce(authKeyID, sessionID, reqMsgID, encoded) {
|
||||
return
|
||||
}
|
||||
// A direct Put has no pre-reserved owner slot. Expired entries in another
|
||||
// shard may be its only blocker; reap once without holding a shard and retry.
|
||||
// Production owner publication already carries both reservations and never
|
||||
// needs this cold path.
|
||||
c.expireCompletedResults()
|
||||
_ = c.putOnce(authKeyID, sessionID, reqMsgID, encoded)
|
||||
}
|
||||
|
||||
// putOnce returns false only when a cross-shard expiry reap may release the
|
||||
// process-wide entry/body capacity needed by a defensive direct Put.
|
||||
func (c *rpcResultCache) putOnce(authKeyID [8]byte, sessionID, reqMsgID int64, encoded *encodedOutboundMessage) bool {
|
||||
key := rpcResultCacheKey{authKeyID: authKeyID, sessionID: sessionID, reqMsgID: reqMsgID}
|
||||
s := c.shard(key)
|
||||
size := len(encoded.body)
|
||||
cacheable := s.maxBytes <= 0 || size <= s.maxBytes
|
||||
now := s.now()
|
||||
|
||||
s.mu.Lock()
|
||||
|
||||
if cacheable {
|
||||
s.expireLocked(now)
|
||||
if elem, ok := s.byKey[key]; ok {
|
||||
s.removeElement(elem)
|
||||
}
|
||||
entry := &rpcResultCacheEntry{
|
||||
key: key,
|
||||
encoded: encoded,
|
||||
size: size,
|
||||
expiresAt: now.Add(s.ttl),
|
||||
}
|
||||
elem := s.order.PushBack(entry)
|
||||
s.byKey[key] = elem
|
||||
s.bytes += size
|
||||
s.trimLocked()
|
||||
accountedSize := len(encoded.body)
|
||||
if accountedSize < 1 {
|
||||
// Every owner reserves one byte at admission. Keeping zero-length results
|
||||
// at the same minimum makes entry and byte capacity linearizable.
|
||||
accountedSize = 1
|
||||
}
|
||||
// Resolve the independent in-flight entry only after the completed cache has
|
||||
// been published. Waiters awakened by this close can therefore immediately
|
||||
// observe either the shared encoded result or the completed Get entry.
|
||||
subscribers := c.completeRPCResultFlightLocked(s, key, encoded)
|
||||
|
||||
// Publication never evicts another unexpired result. A production owner has
|
||||
// already reserved its entry slot and one byte. If its actual result cannot
|
||||
// expand that reservation, publish a one-byte identity tombstone: the owner
|
||||
// and current waiters still receive the immutable result, while later
|
||||
// duplicates fail admission instead of rerunning the handler.
|
||||
s.mu.Lock()
|
||||
now := s.now()
|
||||
s.expireLocked(now)
|
||||
old := s.byKey[key]
|
||||
flight := s.pending[key]
|
||||
if old == nil && flight == nil && s.maxEntries > 0 && len(s.byKey) >= s.maxEntries {
|
||||
// Defensive direct Put callers do not own a reserved admission slot.
|
||||
// Preserve every existing unexpired result and decline the new cache row.
|
||||
s.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
identity, admissionSeq, executionKnown, executionOK := rpcResultFlightMetadataLocked(s, key)
|
||||
var oldEntry *rpcResultCacheEntry
|
||||
if old != nil {
|
||||
oldEntry = old.Value.(*rpcResultCacheEntry)
|
||||
if flight == nil {
|
||||
// A defensive duplicate terminal publication must never downgrade
|
||||
// completed dependency/identity metadata after its flight disappeared.
|
||||
identity = oldEntry.identity
|
||||
admissionSeq = oldEntry.admissionSeq
|
||||
executionKnown = oldEntry.executionKnown
|
||||
executionOK = oldEntry.executionOK
|
||||
}
|
||||
}
|
||||
|
||||
var reservation *rpcResultBudgetReservation
|
||||
switch {
|
||||
case flight != nil:
|
||||
reservation = flight.reservation
|
||||
if reservation == nil {
|
||||
s.mu.Unlock()
|
||||
panic("mtprotoedge: pending rpc result has no fair-budget reservation")
|
||||
}
|
||||
case oldEntry != nil && oldEntry.reservation != nil:
|
||||
reservation = oldEntry.reservation
|
||||
default:
|
||||
reservation = c.fairBudget.reserveCompleted(key, accountedSize)
|
||||
if reservation == nil {
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
retainedSize := accountedSize
|
||||
retained := encoded
|
||||
capacity := false
|
||||
if !reservation.resizeBytes(accountedSize) {
|
||||
if flight == nil {
|
||||
// A direct replacement cannot discard the prior replay body. Leave it
|
||||
// untouched and let Put perform one cross-shard expiry reap before its
|
||||
// final bounded failure.
|
||||
if oldEntry == nil {
|
||||
reservation.release()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
// Owner admission already reserved one byte at all three scopes. When the
|
||||
// actual body cannot expand, transfer that reservation to an identity
|
||||
// tombstone so a duplicate never reruns business.
|
||||
const tombstoneSize = 1
|
||||
if !reservation.resizeBytes(tombstoneSize) {
|
||||
s.mu.Unlock()
|
||||
panic("mtprotoedge: rpc result owner lost its one-byte tombstone reservation")
|
||||
}
|
||||
retainedSize = tombstoneSize
|
||||
retained = nil
|
||||
capacity = true
|
||||
}
|
||||
|
||||
if old != nil {
|
||||
s.unlinkElement(old)
|
||||
if oldEntry.reservation != nil && oldEntry.reservation != reservation {
|
||||
oldEntry.reservation.release()
|
||||
oldEntry.reservation = nil
|
||||
}
|
||||
}
|
||||
entry := &rpcResultCacheEntry{
|
||||
key: key,
|
||||
encoded: retained,
|
||||
size: retainedSize,
|
||||
expiresAt: now.Add(s.ttl),
|
||||
identity: identity,
|
||||
admissionSeq: admissionSeq,
|
||||
executionKnown: executionKnown,
|
||||
executionOK: executionOK,
|
||||
capacity: capacity,
|
||||
reservation: reservation,
|
||||
}
|
||||
elem := s.order.PushBack(entry)
|
||||
s.byKey[key] = elem
|
||||
s.bytes += retainedSize
|
||||
|
||||
// Resolve the independent in-flight entry only after either the completed
|
||||
// result or its replay tombstone is published under the same shard lock.
|
||||
subscribers, executionSubscribers, executionOK := c.completeRPCResultFlightLocked(s, key, encoded)
|
||||
s.mu.Unlock()
|
||||
for _, subscriber := range subscribers {
|
||||
subscriber(encoded, true)
|
||||
}
|
||||
for _, subscriber := range executionSubscribers {
|
||||
subscriber(executionOK)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func rpcResultFlightMetadataLocked(s *rpcResultCacheShard, key rpcResultCacheKey) (
|
||||
rpcResultRequestIdentity,
|
||||
uint64,
|
||||
bool,
|
||||
bool,
|
||||
) {
|
||||
if flight := s.pending[key]; flight != nil {
|
||||
return flight.identity, flight.admissionSeq, flight.executionDone, flight.executionOK
|
||||
}
|
||||
return rpcResultRequestIdentity{}, 0, false, false
|
||||
}
|
||||
|
||||
// expireCompletedResults performs the cold-path cross-shard reap used only
|
||||
// after a one-byte admission reservation fails. The caller must hold no shard
|
||||
// lock. Each shard is reaped independently so ordinary result publication on
|
||||
// the other shards remains parallel.
|
||||
func (c *rpcResultCache) expireCompletedResults() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for i := range c.shards {
|
||||
s := &c.shards[i]
|
||||
s.mu.Lock()
|
||||
s.expireLocked(s.now())
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcResultCacheShard) expireLocked(now time.Time) {
|
||||
|
|
@ -157,20 +496,17 @@ func (s *rpcResultCacheShard) expireLocked(now time.Time) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *rpcResultCacheShard) trimLocked() {
|
||||
for s.order.Len() > 0 {
|
||||
tooManyEntries := s.maxEntries > 0 && s.order.Len() > s.maxEntries
|
||||
tooManyBytes := s.maxBytes > 0 && s.bytes > s.maxBytes
|
||||
if !tooManyEntries && !tooManyBytes {
|
||||
return
|
||||
}
|
||||
s.removeElement(s.order.Front())
|
||||
func (s *rpcResultCacheShard) removeElement(elem *list.Element) {
|
||||
entry := s.unlinkElement(elem)
|
||||
if entry != nil && entry.reservation != nil {
|
||||
entry.reservation.release()
|
||||
entry.reservation = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *rpcResultCacheShard) removeElement(elem *list.Element) {
|
||||
func (s *rpcResultCacheShard) unlinkElement(elem *list.Element) *rpcResultCacheEntry {
|
||||
if elem == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
entry := elem.Value.(*rpcResultCacheEntry)
|
||||
delete(s.byKey, entry.key)
|
||||
|
|
@ -179,4 +515,45 @@ func (s *rpcResultCacheShard) removeElement(elem *list.Element) {
|
|||
s.bytes = 0
|
||||
}
|
||||
s.order.Remove(elem)
|
||||
return entry
|
||||
}
|
||||
|
||||
type rpcResultCacheByteBudget struct {
|
||||
max int64
|
||||
used atomic.Int64
|
||||
}
|
||||
|
||||
func (b *rpcResultCacheByteBudget) reserve(n int) bool {
|
||||
if n <= 0 {
|
||||
return true
|
||||
}
|
||||
bytes := int64(n)
|
||||
if b == nil || bytes > b.max {
|
||||
return false
|
||||
}
|
||||
for {
|
||||
used := b.used.Load()
|
||||
if used > b.max-bytes {
|
||||
return false
|
||||
}
|
||||
if b.used.CompareAndSwap(used, used+bytes) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *rpcResultCacheByteBudget) release(n int) {
|
||||
if b == nil || n <= 0 {
|
||||
return
|
||||
}
|
||||
if remaining := b.used.Add(-int64(n)); remaining < 0 {
|
||||
panic("mtprotoedge: rpc result completed-byte budget underflow")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *rpcResultCacheByteBudget) snapshot() int64 {
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return b.used.Load()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue