@@ -82,6 +82,11 @@ const (
8282 // msgQueueFallbackMaxBytes is the byte budget used when the process has no memory
8383 // limit set, where a fraction of it would be meaningless.
8484 msgQueueFallbackMaxBytes = 1 << 30
85+
86+ // statsInterval is how often queue depth and merge counters are reported. Rates are
87+ // reported per interval rather than per event, which keeps the ingest path quiet
88+ // under load where per-event logging would dominate the output.
89+ statsInterval = 30 * time .Second
8590)
8691
8792// queuedMessage is a decoded request paired with its wire size, so the queue's byte
@@ -206,6 +211,19 @@ type P2P struct {
206211 msgQueueBytes atomic.Int64
207212 // msgQueueMaxBytes caps msgQueueBytes. Zero or less disables the byte bound.
208213 msgQueueMaxBytes int64
214+
215+ // stopStats ends the stats reporter. The context outlives Close, so the reporter
216+ // needs a signal of its own.
217+ stopStats chan struct {}
218+
219+ // Counters reported by reportStats and reset on each report, so each line carries
220+ // the rate for that interval rather than a running total.
221+ statDroppedBudget atomic.Int64
222+ statDroppedFull atomic.Int64
223+ statMergedDocs atomic.Int64
224+ statDroppedDocs atomic.Int64
225+ statBatches atomic.Int64
226+ statBatchFailures atomic.Int64
209227}
210228
211229// pushLogCommProcessor implements CommProcessor for push log functionality
@@ -263,12 +281,14 @@ func New(
263281 topicPeerCounts : make (map [string ]int ),
264282 msgQueue : make (chan queuedMessage , msgQueueSize ),
265283 msgQueueMaxBytes : queueByteBudget (),
284+ stopStats : make (chan struct {}),
266285 }
267286
268287 for i := 0 ; i < dagSyncWorkers ; i ++ {
269288 p .msgWorkers .Add (1 )
270289 go p .processMessageWorker ()
271290 }
291+ go p .reportStats ()
272292
273293 p .replicatorProtocol = protocol .NewCommChannel (host , "rep" , & pushLogCommProcessor {p2p : & p })
274294 p .batcher = newPubsubBatcher (host .ID (), func (topic string , data []byte ) error {
@@ -661,6 +681,7 @@ func (p *P2P) docIDsForBlockCID(
661681func (p * P2P ) pubSubMessageHandler (from string , topic string , msg []byte ) ([]byte , error ) {
662682 size := int64 (len (msg ))
663683 if ! p .claimQueueBytes (size ) {
684+ p .statDroppedBudget .Add (1 )
664685 log .Info ("pubsub message queue over byte budget, dropping message" ,
665686 corelog .Any ("topic" , topic ),
666687 corelog .Int64 ("bytes" , size ),
@@ -681,6 +702,7 @@ func (p *P2P) pubSubMessageHandler(from string, topic string, msg []byte) ([]byt
681702 p .releaseQueueBytes (size )
682703 default :
683704 p .releaseQueueBytes (size )
705+ p .statDroppedFull .Add (1 )
684706 log .Info ("pubsub message queue full, dropping message" , corelog .Any ("topic" , topic ))
685707 }
686708 return nil , nil
@@ -707,6 +729,35 @@ func (p *P2P) releaseQueueBytes(size int64) {
707729 p .msgQueueBytes .Add (- size )
708730}
709731
732+ // reportStats periodically logs queue occupancy and merge outcomes. Queue depth and the
733+ // split between merged and dropped documents are otherwise only visible from a heap
734+ // profile, which is too invasive to take routinely.
735+ func (p * P2P ) reportStats () {
736+ ticker := time .NewTicker (statsInterval )
737+ defer ticker .Stop ()
738+ for {
739+ select {
740+ case <- p .ctx .Done ():
741+ return
742+ case <- p .stopStats :
743+ return
744+ case <- ticker .C :
745+ log .Info ("p2p stats" ,
746+ corelog .Int ("queueDepth" , len (p .msgQueue )),
747+ corelog .Int ("queueSlots" , msgQueueSize ),
748+ corelog .Int64 ("queueBytes" , p .msgQueueBytes .Load ()),
749+ corelog .Int64 ("queueBudget" , p .msgQueueMaxBytes ),
750+ corelog .Int64 ("droppedOverBudget" , p .statDroppedBudget .Swap (0 )),
751+ corelog .Int64 ("droppedQueueFull" , p .statDroppedFull .Swap (0 )),
752+ corelog .Int64 ("batches" , p .statBatches .Swap (0 )),
753+ corelog .Int64 ("batchFailures" , p .statBatchFailures .Swap (0 )),
754+ corelog .Int64 ("docsMerged" , p .statMergedDocs .Swap (0 )),
755+ corelog .Int64 ("docsDropped" , p .statDroppedDocs .Swap (0 )),
756+ )
757+ }
758+ }
759+ }
760+
710761// processMessageWorker is a long-lived goroutine that drains p.msgQueue and
711762// calls processPushlogRequest for each message. dagSyncWorkers of these run
712763// concurrently, bounding the goroutine count regardless of inbound message rate.
@@ -772,7 +823,16 @@ func (p *P2P) processPushlogRequest(
772823 merges [i ] = r .merge
773824 }
774825 merged , err := p .db .MergeBatchWithTxn (ctx , merges )
826+ p .statBatches .Add (1 )
827+ for _ , ok := range merged {
828+ if ok {
829+ p .statMergedDocs .Add (1 )
830+ } else {
831+ p .statDroppedDocs .Add (1 )
832+ }
833+ }
775834 if err != nil {
835+ p .statBatchFailures .Add (1 )
776836 log .ErrorE ("Failed to merge documents in batch" , err ,
777837 corelog .String ("PeerID" , req .SenderID ),
778838 corelog .Int ("Documents" , len (merges )))
@@ -877,8 +937,10 @@ func (p *P2P) processPushlogRequest(
877937 CollectionID : req .CollectionID ,
878938 }
879939 if err = p .db .Merge (ctx , mergeEvt ); err != nil {
940+ p .statDroppedDocs .Add (1 )
880941 return err
881942 }
943+ p .statMergedDocs .Add (1 )
882944
883945 // Notify bus subscribers and the network of peers that we have a new document available.
884946 updateEvt := event.Update {
@@ -1144,6 +1206,7 @@ func (pq *processQueue) close() {
11441206// and waits for all worker goroutines to exit.
11451207// It should be called once when the P2P subsystem is shutting down.
11461208func (p * P2P ) Close () {
1209+ close (p .stopStats )
11471210 close (p .msgQueue )
11481211 done := make (chan struct {})
11491212 go func () {
0 commit comments