@@ -325,9 +325,15 @@ void TInputBuffer::DoAddConnectionOffer(TGuid connectionId, TConnectionOffer off
325325 connectionState.UpdateEpoch = streamState.Epoch ;
326326 }
327327
328- // Just double function amortized complexity.
329- if (--streamState.RecalculateCounter <= 0 ) {
330- RecalculateStreamLimits (streamState);
328+ // Just double function amortized complexity. A manager-side limit change bypasses the
329+ // amortization so the raised limit turns into grants on the first offer, not the Nth;
330+ // the bypass must not advance the connection-GC epoch: the recomputed limit jitters on
331+ // every manage tick, and the GC horizon must stay measured in offer rounds.
332+ const bool offerRoundElapsed = --streamState.RecalculateCounter <= 0 ;
333+ if (offerRoundElapsed ||
334+ streamState.LimitUsageState ->GetLimitBytes () != streamState.LastRecalculatedLimitBytes )
335+ {
336+ RecalculateStreamLimits (streamState, /* collectStaleConnections*/ offerRoundElapsed);
331337 streamState.Usage .PendingInflatedBytes = GetPendingSize (streamState);
332338 streamState.LimitUsageState ->Update (streamState.Usage );
333339 }
@@ -445,6 +451,7 @@ TFuture<std::vector<TInputMessageConstPtr>> TInputBuffer::DoGetInputBatch(THashS
445451
446452 using TPriority = std::pair<TSystemTimestamp, ui64>;
447453 std::vector<std::pair<TMessagesPriorityQueue*, std::function<TPriority ()>>> queues;
454+ std::vector<TStreamState*> extractionStreamStates;
448455 size_t queuedMessageCount = 0 ;
449456 for (auto & [streamId, streamState] : StreamStates_) {
450457 if (!streamState.Messages .empty () && allowedStreams.contains (streamId)) {
@@ -454,6 +461,7 @@ TFuture<std::vector<TInputMessageConstPtr>> TInputBuffer::DoGetInputBatch(THashS
454461 const auto & front = messagesPtr->front ();
455462 return {TSystemTimestamp (front.AlignmentTimestamp .Underlying () + bias), front.SeqNo };
456463 });
464+ extractionStreamStates.push_back (&streamState);
457465 queuedMessageCount += streamState.Messages .size ();
458466 }
459467 }
@@ -480,22 +488,31 @@ TFuture<std::vector<TInputMessageConstPtr>> TInputBuffer::DoGetInputBatch(THashS
480488 LastNotFullBatchInstant_ = TInstant::Now ();
481489 }
482490
491+ // Extraction freed buffer space; regrant connection windows right away so the next
492+ // PushMessages response carries fresh limits instead of waiting out the offer amortization.
493+ if (!batch.empty ()) {
494+ for (auto * streamState : extractionStreamStates) {
495+ RecalculateStreamLimits (*streamState, /* collectStaleConnections*/ false );
496+ streamState->Usage .PendingInflatedBytes = GetPendingSize (*streamState);
497+ }
498+ }
499+
483500 for (auto & [streamId, streamState] : StreamStates_) {
484501 streamState.LimitUsageState ->Update (streamState.Usage );
485502 }
486503
487504 return MakeFuture<std::vector<TInputMessageConstPtr>>(std::move (batch));
488505}
489506
490- void TInputBuffer::RecalculateStreamLimits (TStreamState& streamState)
507+ void TInputBuffer::RecalculateStreamLimits (TStreamState& streamState, bool collectStaleConnections )
491508{
492509 using THeapElement = std::pair<TConnectionState*, i64 >; // (it, currentBucketIndex).
493510
494511 constexpr i64 lastEpochStoreCount = 5 ; // TODO: something better? Configurable?
495512
496513 // Cleanup.
497514 for (auto it = streamState.ConnectionStates .begin (); it != streamState.ConnectionStates .end ();) {
498- if (it->second .UpdateEpoch + lastEpochStoreCount < streamState.Epoch ) {
515+ if (collectStaleConnections && it->second .UpdateEpoch + lastEpochStoreCount < streamState.Epoch ) {
499516 streamState.ConnectionStates .erase (it++);
500517 } else {
501518 it->second .InflatedByteLimit = 0 ;
@@ -520,6 +537,7 @@ void TInputBuffer::RecalculateStreamLimits(TStreamState& streamState)
520537 // Match the manager's accounting: limit is inflated, so used must be inflated too.
521538 i64 inflatedUsedBytes = streamState.Usage .GetInflatedInflightBytes (streamState.LimitUsageState ->GetInflationPerMessage ());
522539 i64 inflatedLimitBytes = streamState.LimitUsageState ->GetLimitBytes ();
540+ streamState.LastRecalculatedLimitBytes = inflatedLimitBytes;
523541 // Do not allocate small share of buffer to reduce retransmits.
524542 i64 inflatedFreeBytes = std::max<i64 >(inflatedLimitBytes * 0.9 - inflatedUsedBytes, 0 );
525543
@@ -547,8 +565,10 @@ void TInputBuffer::RecalculateStreamLimits(TStreamState& streamState)
547565 }
548566 }
549567
550- streamState.Epoch += 1 ;
551- streamState.RecalculateCounter = std::ssize (streamState.ConnectionStates );
568+ if (collectStaleConnections) {
569+ streamState.Epoch += 1 ;
570+ streamState.RecalculateCounter = std::ssize (streamState.ConnectionStates );
571+ }
552572}
553573
554574i64 TInputBuffer::GetPendingSize (const TStreamState& streamState)
0 commit comments