1
+ """
2
+ Generic object pooling utilities for LiteLLM.
3
+
4
+ This module provides a flexible object pooling system that can be used
5
+ to pool any type of object, reducing memory allocation overhead and
6
+ improving performance for frequently created/destroyed objects.
7
+
8
+ Memory Management Strategy:
9
+ - Balanced eviction-based memory control to optimize reuse ratio
10
+ - Moderate eviction frequency (300s) to maintain high object reuse
11
+ - Conservative eviction weight (0.3) to avoid destroying useful objects
12
+ - Lower pre-warm count (5) to reduce initial memory footprint
13
+ - Always keeps at least one object available for high availability
14
+ - Unlimited pools when maxsize is not specified (eviction controls actual usage)
15
+ """
16
+
17
+ from typing import Type , TypeVar , Optional , Callable
18
+ from pond import Pond , PooledObjectFactory , PooledObject
19
+
20
+ T = TypeVar ('T' )
21
+
22
+ class GenericPooledObjectFactory (PooledObjectFactory ):
23
+ """Generic factory class for creating pooled objects of any type."""
24
+
25
+ def __init__ (
26
+ self ,
27
+ object_class : Type [T ],
28
+ pooled_maxsize : Optional [int ] = None , # None = unlimited pool with eviction-based memory control
29
+ least_one : bool = True , # Always keep at least one for high concurrency
30
+ initializer : Optional [Callable [[T ], None ]] = None
31
+ ):
32
+ # Only pass maxsize to Pond if user specified it - otherwise let Pond handle unlimited pools
33
+ if pooled_maxsize is not None :
34
+ super ().__init__ (pooled_maxsize = pooled_maxsize , least_one = least_one )
35
+ else :
36
+ super ().__init__ (least_one = least_one )
37
+ self .object_class = object_class
38
+ self .initializer = initializer
39
+ self ._user_maxsize = pooled_maxsize # Store original user preference
40
+
41
+ def createInstance (self ) -> PooledObject :
42
+ """Create a new instance wrapped in a PooledObject."""
43
+ # Create a properly initialized instance
44
+ obj = self .object_class ()
45
+ return PooledObject (obj )
46
+
47
+ def destroy (self , pooled_object : PooledObject ):
48
+ """Destroy the pooled object."""
49
+ if hasattr (pooled_object .keeped_object , '__dict__' ):
50
+ pooled_object .keeped_object .__dict__ .clear ()
51
+ del pooled_object
52
+
53
+ def reset (self , pooled_object : PooledObject ) -> PooledObject :
54
+ """Reset the pooled object to a clean state."""
55
+ obj = pooled_object .keeped_object
56
+ # Reset the object by calling its reset method if it exists
57
+ if hasattr (obj , 'reset' ) and callable (getattr (obj , 'reset' )):
58
+ obj .reset ()
59
+ else :
60
+ # Fallback: clear all attributes to reset the object
61
+ if hasattr (obj , '__dict__' ):
62
+ obj .__dict__ .clear ()
63
+ return pooled_object
64
+
65
+ def validate (self , pooled_object : PooledObject ) -> bool :
66
+ """Validate if the pooled object is still usable."""
67
+ return pooled_object .keeped_object is not None
68
+
69
+ # Global pond instances
70
+ _pools : dict [str , Pond ] = {}
71
+
72
+ def get_object_pool (
73
+ pool_name : str ,
74
+ object_class : Type [T ],
75
+ pooled_maxsize : Optional [int ] = None , # None = unlimited pool with eviction-based memory control
76
+ least_one : bool = True , # Always keep at least one
77
+ borrowed_timeout : int = 10 , # Longer timeout for high concurrency
78
+ time_between_eviction_runs : int = 300 , # Less frequent eviction to maintain high reuse ratio
79
+ eviction_weight : float = 0.3 , # Less aggressive eviction for better reuse
80
+ prewarm_count : int = 5 # Lower pre-warm count to reduce initial memory usage
81
+ ) -> Pond :
82
+ """Get or create a global object pool instance with balanced eviction-based memory control.
83
+
84
+ Memory is controlled through moderate eviction to balance reuse ratio and memory usage:
85
+ - Moderate eviction frequency (300s) to maintain high object reuse ratio
86
+ - Conservative eviction weight (0.3) to avoid destroying useful objects
87
+ - Lower pre-warm count (5) to reduce initial memory footprint
88
+
89
+ Args:
90
+ pool_name: Unique name for the pool
91
+ object_class: The class type to pool
92
+ pooled_maxsize: Maximum number of objects in the pool (None = truly unlimited)
93
+ least_one: Whether to keep at least one object in the pool (default: True)
94
+ borrowed_timeout: Timeout for borrowing objects (seconds, default: 10)
95
+ time_between_eviction_runs: Time between eviction runs (seconds, default: 300)
96
+ eviction_weight: Weight for eviction algorithm (default: 0.3, conservative)
97
+ prewarm_count: Number of objects to pre-warm the pool with (default: 5)
98
+
99
+ Returns:
100
+ Pond instance for the specified object type
101
+ """
102
+
103
+ if pool_name in _pools :
104
+ return _pools [pool_name ]
105
+
106
+ # Create new pond
107
+ pond = Pond (
108
+ borrowed_timeout = borrowed_timeout ,
109
+ time_between_eviction_runs = time_between_eviction_runs ,
110
+ thread_daemon = True ,
111
+ eviction_weight = eviction_weight
112
+ )
113
+
114
+ # Register the factory with user's maxsize preference
115
+ factory = GenericPooledObjectFactory (
116
+ object_class = object_class ,
117
+ pooled_maxsize = pooled_maxsize ,
118
+ least_one = least_one
119
+ )
120
+ pond .register (factory , name = f"{ pool_name } Factory" )
121
+
122
+ # Pre-warm the pool
123
+ _prewarm_pool (pond , pool_name , prewarm_count )
124
+
125
+ _pools [pool_name ] = pond
126
+ return pond
127
+
128
+ def _prewarm_pool (pond : Pond , pool_name : str , prewarm_count : int = 20 ) -> None :
129
+ """Pre-warm the pool with initial objects for high concurrency."""
130
+ for _ in range (prewarm_count ):
131
+ try :
132
+ pooled_obj = pond .borrow (name = f"{ pool_name } Factory" )
133
+ pond .recycle (pooled_obj , name = f"{ pool_name } Factory" )
134
+ except Exception :
135
+ # If pre-warming fails, just continue
136
+ break
0 commit comments