package mtprotoedge import ( "encoding/binary" "hash/maphash" "sync" ) const rpcExecutionBudgetShards = 64 type rpcExecutionBudgetUsage struct { entries int64 pending int64 } type rpcExecutionSessionBudgetKey struct { authKeyID [8]byte sessionID int64 } type rpcExecutionAuthBudgetShard struct { mu sync.Mutex usage map[[8]byte]rpcExecutionBudgetUsage } type rpcExecutionSessionBudgetShard struct { mu sync.Mutex usage map[rpcExecutionSessionBudgetKey]rpcExecutionBudgetUsage } // rpcExecutionFairBudget accounts one bounded ledger slot at global, auth and // session scopes. Pending owner and completed receipt are the same ownership: // Complete transfers the reservation; Abort/ACK/TTL return it. type rpcExecutionFairBudget struct { seed maphash.Seed globalEntries *rpcResultFlightLimit authLimit int64 sessionLimit int64 pendingPerAuth int64 authShards [rpcExecutionBudgetShards]rpcExecutionAuthBudgetShard sessionShards [rpcExecutionBudgetShards]rpcExecutionSessionBudgetShard } type rpcExecutionBudgetReservation struct { budget *rpcExecutionFairBudget key rpcExecutionKey pending bool released bool } func newRPCExecutionFairBudget( seed maphash.Seed, globalEntries *rpcResultFlightLimit, authLimit int64, sessionLimit int64, pendingPerAuth int, ) *rpcExecutionFairBudget { b := &rpcExecutionFairBudget{ seed: seed, globalEntries: globalEntries, authLimit: authLimit, sessionLimit: sessionLimit, pendingPerAuth: int64(pendingPerAuth), } for i := range b.authShards { b.authShards[i].usage = make(map[[8]byte]rpcExecutionBudgetUsage) b.sessionShards[i].usage = make(map[rpcExecutionSessionBudgetKey]rpcExecutionBudgetUsage) } return b } func (b *rpcExecutionFairBudget) reserveOwner(key rpcExecutionKey) *rpcExecutionBudgetReservation { return b.reserve(key, true) } func (b *rpcExecutionFairBudget) reserveCompleted(key rpcExecutionKey) *rpcExecutionBudgetReservation { return b.reserve(key, false) } func (b *rpcExecutionFairBudget) reserve(key rpcExecutionKey, pending bool) *rpcExecutionBudgetReservation { if b == nil || b.globalEntries == nil { return nil } authShard := b.authShard(key.authKeyID) sessionKey := rpcExecutionSessionBudgetKey{authKeyID: key.authKeyID, sessionID: key.sessionID} sessionShard := b.sessionShard(sessionKey) authShard.mu.Lock() sessionShard.mu.Lock() authUsage := authShard.usage[key.authKeyID] sessionUsage := sessionShard.usage[sessionKey] allowed := withinRPCExecutionBudget(authUsage.entries, 1, b.authLimit) && withinRPCExecutionBudget(sessionUsage.entries, 1, b.sessionLimit) if pending { allowed = allowed && withinRPCExecutionBudget(authUsage.pending, 1, b.pendingPerAuth) } if !allowed || !b.globalEntries.reserve() { sessionShard.mu.Unlock() authShard.mu.Unlock() return nil } authUsage.entries++ sessionUsage.entries++ if pending { authUsage.pending++ } authShard.usage[key.authKeyID] = authUsage sessionShard.usage[sessionKey] = sessionUsage sessionShard.mu.Unlock() authShard.mu.Unlock() return &rpcExecutionBudgetReservation{budget: b, key: key, pending: pending} } func withinRPCExecutionBudget(used, delta, limit int64) bool { return delta >= 0 && limit > 0 && used >= 0 && used <= limit-delta } func (r *rpcExecutionBudgetReservation) releasePending() { if r == nil || r.budget == nil || r.released || !r.pending { return } b := r.budget shard := b.authShard(r.key.authKeyID) shard.mu.Lock() usage, ok := shard.usage[r.key.authKeyID] if !ok || usage.pending < 1 { shard.mu.Unlock() panic("mtprotoedge: rpc execution per-auth pending budget underflow") } usage.pending-- shard.usage[r.key.authKeyID] = usage r.pending = false shard.mu.Unlock() } func (r *rpcExecutionBudgetReservation) release() { if r == nil || r.budget == nil || r.released { return } b := r.budget authShard := b.authShard(r.key.authKeyID) sessionKey := rpcExecutionSessionBudgetKey{authKeyID: r.key.authKeyID, sessionID: r.key.sessionID} sessionShard := b.sessionShard(sessionKey) authShard.mu.Lock() sessionShard.mu.Lock() authUsage, authOK := authShard.usage[r.key.authKeyID] sessionUsage, sessionOK := sessionShard.usage[sessionKey] if !authOK || !sessionOK || authUsage.entries < 1 || sessionUsage.entries < 1 || (r.pending && authUsage.pending < 1) { sessionShard.mu.Unlock() authShard.mu.Unlock() panic("mtprotoedge: rpc execution fair budget underflow") } authUsage.entries-- sessionUsage.entries-- if r.pending { authUsage.pending-- } if authUsage == (rpcExecutionBudgetUsage{}) { delete(authShard.usage, r.key.authKeyID) } else { authShard.usage[r.key.authKeyID] = authUsage } if sessionUsage == (rpcExecutionBudgetUsage{}) { delete(sessionShard.usage, sessionKey) } else { sessionShard.usage[sessionKey] = sessionUsage } r.released = true r.pending = false b.globalEntries.release() sessionShard.mu.Unlock() authShard.mu.Unlock() } func (b *rpcExecutionFairBudget) authSnapshot(authKeyID [8]byte) rpcExecutionBudgetUsage { if b == nil { return rpcExecutionBudgetUsage{} } shard := b.authShard(authKeyID) shard.mu.Lock() usage := shard.usage[authKeyID] shard.mu.Unlock() return usage } func (b *rpcExecutionFairBudget) sessionSnapshot(authKeyID [8]byte, sessionID int64) rpcExecutionBudgetUsage { if b == nil { return rpcExecutionBudgetUsage{} } key := rpcExecutionSessionBudgetKey{authKeyID: authKeyID, sessionID: sessionID} shard := b.sessionShard(key) shard.mu.Lock() usage := shard.usage[key] shard.mu.Unlock() return usage } func (b *rpcExecutionFairBudget) authShard(authKeyID [8]byte) *rpcExecutionAuthBudgetShard { index := maphash.Bytes(b.seed, authKeyID[:]) & (rpcExecutionBudgetShards - 1) return &b.authShards[index] } func (b *rpcExecutionFairBudget) sessionShard(key rpcExecutionSessionBudgetKey) *rpcExecutionSessionBudgetShard { 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] }