2525 MessageResponseType ,
2626)
2727from pypnm .api .routes .common .service .status_codes import ServiceStatusCode
28+ from pypnm .lib .memory import ProcessMemory
2829from pypnm .lib .types import GroupId , OperationId , TimeStamp
2930from pypnm .lib .utils import Generate
3031
@@ -129,6 +130,7 @@ async def start(self) -> tuple[GroupId, OperationId]:
129130 "duration" : self .duration ,
130131 "interval" : self .interval ,
131132 "time_remaining" : self .time_remaining ,
133+ "collected" : 0 ,
132134 "samples" : [],
133135 "task" : None ,
134136 }
@@ -162,15 +164,14 @@ async def _runner() -> None:
162164 if isinstance (msg_rsp .payload , list ):
163165 samples = self ._process_captures (msg_rsp )
164166 for sample in samples :
165- self ._ops [operation_id ]["samples" ].append (sample )
166- if sample .transaction_id :
167- self ._cap_group .add_transaction (sample .transaction_id )
167+ self ._append_operation_sample (operation_id , sample )
168168 self .logger .debug (f"[{ operation_id } ] Captured sample txn={ sample .transaction_id } " )
169169 else :
170170 status_name = msg_rsp .status .name if isinstance (msg_rsp .status , ServiceStatusCode ) else str (msg_rsp .status )
171171 err = f"Capture response returned no transaction payload (status={ status_name } )"
172172 self .logger .warning (f"[{ operation_id } ] { err } " )
173- self ._ops [operation_id ]["samples" ].append (
173+ self ._append_operation_sample (
174+ operation_id ,
174175 CaptureSample (
175176 timestamp = cast (TimeStamp , iteration_ts ),
176177 transaction_id = "" ,
@@ -182,10 +183,10 @@ async def _runner() -> None:
182183 except Exception as exc :
183184 error_msg = str (exc )
184185 self .logger .error (f"[{ operation_id } ] Capture error: { error_msg } " , exc_info = True )
185- self ._ops [ operation_id ][ "samples" ]. append ( CaptureSample ( timestamp = cast ( TimeStamp , iteration_ts ),
186- transaction_id = "" ,
187- filename = "" ,
188- error = error_msg ) )
186+ self ._append_operation_sample (
187+ operation_id ,
188+ CaptureSample ( timestamp = cast ( TimeStamp , iteration_ts ), transaction_id = "" , filename = "" , error = error_msg ) ,
189+ )
189190
190191 # Complete if still running
191192 if self ._ops [operation_id ]["state" ] == OperationState .RUNNING :
@@ -209,15 +210,14 @@ async def _runner() -> None:
209210 if isinstance (msg_rsp .payload , list ):
210211 samples = self ._process_captures (msg_rsp )
211212 for sample in samples :
212- self ._ops [operation_id ]["samples" ].append (sample )
213- if sample .transaction_id :
214- self ._cap_group .add_transaction (sample .transaction_id )
213+ self ._append_operation_sample (operation_id , sample )
215214 self .logger .info (f"[{ operation_id } ] Captured sample txn={ sample .transaction_id } " )
216215 else :
217216 status_name = msg_rsp .status .name if isinstance (msg_rsp .status , ServiceStatusCode ) else str (msg_rsp .status )
218217 err = f"Final capture response returned no transaction payload (status={ status_name } )"
219218 self .logger .warning (f"[{ operation_id } ] { err } " )
220- self ._ops [operation_id ]["samples" ].append (
219+ self ._append_operation_sample (
220+ operation_id ,
221221 CaptureSample (
222222 timestamp = cast (TimeStamp , iteration_ts ),
223223 transaction_id = "" ,
@@ -229,11 +229,10 @@ async def _runner() -> None:
229229 except Exception as exc :
230230 error_msg = str (exc )
231231 self .logger .error (f"[{ operation_id } ] Capture error: { error_msg } " , exc_info = True )
232- self ._ops [operation_id ]["samples" ].append (
233- CaptureSample (timestamp = cast (TimeStamp , iteration_ts ),
234- transaction_id = "" ,
235- filename = "" ,
236- error = error_msg ))
232+ self ._append_operation_sample (
233+ operation_id ,
234+ CaptureSample (timestamp = cast (TimeStamp , iteration_ts ), transaction_id = "" , filename = "" , error = error_msg ),
235+ )
237236 except asyncio .CancelledError :
238237 if operation_id in self ._ops and self ._ops [operation_id ]["state" ] == OperationState .RUNNING :
239238 self ._ops [operation_id ]["state" ] = OperationState .CANCELLED
@@ -278,6 +277,20 @@ def getOperationID(self) -> OperationId:
278277 def getOperation (self , operation_id :OperationId ) -> dict [str , dict [str , Any ]]:
279278 return self ._ops [operation_id ]
280279
280+ def _append_operation_sample (self , operation_id : OperationId , sample : CaptureSample ) -> None :
281+ """
282+ Append a capture sample and update operation-level counters.
283+
284+ Args:
285+ operation_id: Operation receiving the sample.
286+ sample: Parsed capture sample to retain.
287+ """
288+ op = self ._ops [operation_id ]
289+ op ["samples" ].append (sample )
290+ op ["collected" ] = int (op .get ("collected" , 0 )) + 1
291+ if sample .transaction_id :
292+ self ._cap_group .add_transaction (sample .transaction_id )
293+
281294 def getOperationState (self ,operation_id :OperationId ) -> OperationState :
282295 return self ._ops [operation_id ]["state" ]
283296
@@ -314,7 +327,7 @@ def status(self, operation_id: OperationId) -> dict[str, Any]:
314327
315328 return {
316329 "state" : op ["state" ],
317- "collected" : len (op [ "samples" ] ),
330+ "collected" : int (op . get ( "collected" , 0 ) ),
318331 "time_remaining" : op .get ("time_remaining" , 0 )
319332 }
320333
@@ -331,6 +344,26 @@ def results(self, operation_id: OperationId) -> list[CaptureSample]:
331344 op = self ._ops .get (operation_id )
332345 return op ["samples" ] if op else []
333346
347+ def release_operation_memory (self , operation_id : OperationId ) -> None :
348+ """
349+ Drop retained in-memory samples and task references for an operation.
350+
351+ Args:
352+ operation_id: Operation whose transient in-memory state should be released.
353+ """
354+ op = self ._ops .get (operation_id )
355+ if not op :
356+ return
357+
358+ released_samples = len (op ["samples" ])
359+ op ["samples" ] = []
360+ op ["task" ] = None
361+
362+ if released_samples > 0 :
363+ self .logger .info ("[%s] Released %d retained samples from memory" , operation_id , released_samples )
364+
365+ ProcessMemory .release_unused_memory ()
366+
334367 def stop (self , operation_id : OperationId ) -> None :
335368 """
336369 Signal the background runner to stop after the current iteration.
0 commit comments