99import inspect
1010import logging
1111import uuid
12+ from collections import deque
1213from typing import Any
1314
1415import pandas as pd
@@ -196,6 +197,8 @@ def __init__(self):
196197 self ._handle_manager = get_handle_manager ()
197198 self ._job_manager = get_job_manager ()
198199 self ._data_handles : dict [str , Any ] = {}
200+ # Tombstones for data handles evicted under the cap (see _cleanup_oldest_data).
201+ self ._evicted_data : deque [str ] = deque (maxlen = 1024 )
199202 from sktime_mcp .config import settings
200203
201204 self ._max_data_handles = settings .max_data_handles
@@ -205,7 +208,23 @@ def _cleanup_oldest_data(self, count: int = 10) -> None:
205208 to_remove = list (self ._data_handles .keys ())[:count ]
206209 for handle_id in to_remove :
207210 del self ._data_handles [handle_id ]
208- logger .debug ("Evicted data handle %s (limit=%d)" , handle_id , self ._max_data_handles )
211+ self ._evicted_data .append (handle_id )
212+ logger .info ("Evicted data handle %s (limit %d reached)" , handle_id , self ._max_data_handles )
213+
214+ def data_handle_missing (self , handle_id : str ) -> dict [str , Any ]:
215+ """Error body for a missing data handle — distinguishes evicted from unknown.
216+
217+ Returns the ``error`` string plus the capped available-handles summary,
218+ so callers can splat it into a not-found response.
219+ """
220+ if handle_id in self ._evicted_data :
221+ error = (
222+ f"Data handle '{ handle_id } ' was evicted (handle limit "
223+ f"{ self ._max_data_handles } reached); reload the source."
224+ )
225+ else :
226+ error = f"Data handle '{ handle_id } ' not found"
227+ return {"error" : error , ** self .summarize_available_handles ()}
209228
210229 def _register_data_handle (self , handle_id : str , data : dict [str , Any ]) -> None :
211230 if len (self ._data_handles ) >= self ._max_data_handles :
@@ -380,7 +399,7 @@ def fit(
380399 handle_info = self ._handle_manager .get_info (handle_id )
381400 instance = handle_info .instance
382401 except KeyError :
383- return {"success" : False , "error" : f"Handle not found: { handle_id } " }
402+ return {"success" : False , "error" : self . _handle_manager . describe_missing ( handle_id ) }
384403
385404 obj_type = getattr (instance , "get_class_tag" , lambda x , y : "" )("object_type" , "" )
386405 if not hasattr (instance , "fit" ):
@@ -448,7 +467,7 @@ def predict(
448467 try :
449468 instance = self ._handle_manager .get_instance (handle_id )
450469 except KeyError :
451- return {"success" : False , "error" : f"Handle not found: { handle_id } " }
470+ return {"success" : False , "error" : self . _handle_manager . describe_missing ( handle_id ) }
452471
453472 obj_type = getattr (instance , "get_class_tag" , lambda x , y : "" )("object_type" , "" )
454473 if (
@@ -667,7 +686,7 @@ def call_method(
667686 try :
668687 instance = self ._handle_manager .get_instance (handle_id )
669688 except KeyError :
670- return {"success" : False , "error" : f"Handle not found: { handle_id } " }
689+ return {"success" : False , "error" : self . _handle_manager . describe_missing ( handle_id ) }
671690
672691 if not hasattr (instance , method_name ):
673692 obj_type = getattr (instance , "get_class_tag" , lambda x , y : "" )("object_type" , "" )
@@ -748,7 +767,7 @@ def update(
748767 try :
749768 instance = self ._handle_manager .get_instance (handle_id )
750769 except KeyError :
751- return {"success" : False , "error" : f"Handle not found: { handle_id } " }
770+ return {"success" : False , "error" : self . _handle_manager . describe_missing ( handle_id ) }
752771
753772 if not self ._handle_manager .is_fitted (handle_id ):
754773 return {"success" : False , "error" : "Estimator not fitted" }
@@ -788,7 +807,7 @@ def get_fitted_params(self, handle_id: str) -> dict[str, Any]:
788807 try :
789808 instance = self ._handle_manager .get_instance (handle_id )
790809 except KeyError :
791- return {"success" : False , "error" : f"Handle not found: { handle_id } " }
810+ return {"success" : False , "error" : self . _handle_manager . describe_missing ( handle_id ) }
792811
793812 if not self ._handle_manager .is_fitted (handle_id ):
794813 return {"success" : False , "error" : "Estimator not fitted" }
@@ -932,7 +951,7 @@ async def evaluate_async(
932951 try :
933952 instance = self ._handle_manager .get_instance (handle_id )
934953 except KeyError as err :
935- raise ValueError (f"Handle not found: { handle_id } " ) from err
954+ raise ValueError (self . _handle_manager . describe_missing ( handle_id ) ) from err
936955
937956 y_res = self ._resolve_source (y )
938957 if not y_res ["success" ]:
@@ -1259,7 +1278,7 @@ def format_data_handle(
12591278 was never exposed to the caller.
12601279 """
12611280 if data_handle not in self ._data_handles :
1262- return {"success" : False , "error" : f"Data handle ' { data_handle } ' not found" }
1281+ return {"success" : False , ** self . data_handle_missing ( data_handle ) }
12631282
12641283 data_info = self ._data_handles [data_handle ]
12651284 y = data_info ["y" ].copy ()
@@ -1424,7 +1443,7 @@ def release_data_handle(self, data_handle: str) -> dict[str, Any]:
14241443 else :
14251444 return {
14261445 "success" : False ,
1427- "error" : f"Data handle ' { data_handle } ' not found" ,
1446+ "error" : self . data_handle_missing ( data_handle )[ "error" ] ,
14281447 }
14291448
14301449
0 commit comments