@@ -9,15 +9,20 @@ use fhevm_engine_common::types::AllowEvents;
99use fhevm_engine_common:: types:: SupportedFheOperations ;
1010use fhevm_engine_common:: utils:: DatabaseURL ;
1111use fhevm_engine_common:: utils:: { to_hex, HeartBeat } ;
12+ use prometheus:: { register_int_counter_vec, IntCounterVec } ;
1213use sqlx:: postgres:: PgConnectOptions ;
1314use sqlx:: postgres:: PgPoolOptions ;
1415use sqlx:: types:: Uuid ;
1516use sqlx:: Error as SqlxError ;
1617use sqlx:: { PgPool , Postgres } ;
18+ use std:: collections:: HashMap ;
1719use std:: ops:: DerefMut ;
1820use std:: sync:: Arc ;
21+ use std:: sync:: LazyLock ;
1922use std:: time:: Duration ;
23+ use std:: time:: Instant ;
2024use time:: { Duration as TimeDuration , PrimitiveDateTime } ;
25+ use tokio:: sync:: Mutex ;
2126use tokio:: sync:: RwLock ;
2227use tracing:: error;
2328use tracing:: info;
@@ -39,6 +44,24 @@ pub type ScalarByte = FixedBytes<1>;
3944pub type ClearConst = Uint < 256 , 4 > ;
4045pub type ChainHash = TransactionHash ;
4146
47+ static DEPENDENT_OPS_ALLOWED : LazyLock < IntCounterVec > = LazyLock :: new ( || {
48+ register_int_counter_vec ! (
49+ "host_listener_dependent_ops_allowed" ,
50+ "Number of dependent ops allowed by the limiter" ,
51+ & [ "chain_id" ]
52+ )
53+ . unwrap ( )
54+ } ) ;
55+
56+ static DEPENDENT_OPS_THROTTLED : LazyLock < IntCounterVec > = LazyLock :: new ( || {
57+ register_int_counter_vec ! (
58+ "host_listener_dependent_ops_throttled" ,
59+ "Number of dependent ops deferred by the limiter" ,
60+ & [ "chain_id" ]
61+ )
62+ . unwrap ( )
63+ } ) ;
64+
4265#[ derive( Clone , Debug ) ]
4366pub struct Chain {
4467 pub hash : ChainHash ,
@@ -92,8 +115,11 @@ pub struct Database {
92115 pub pool : Arc < RwLock < sqlx:: Pool < Postgres > > > ,
93116 pub tenant_id : TenantId ,
94117 pub chain_id : ChainId ,
118+ chain_id_label : String ,
119+ last_scheduled_by_chain : Arc < Mutex < HashMap < Vec < u8 > , PrimitiveDateTime > > > ,
95120 pub dependence_chain : ChainCache ,
96121 pub tick : HeartBeat ,
122+ dependent_ops_limiter : Option < Arc < Mutex < DependentOpsLimiter > > > ,
97123}
98124
99125#[ derive( Debug ) ]
@@ -107,6 +133,82 @@ pub struct LogTfhe {
107133 pub dependence_chain : TransactionHash ,
108134}
109135
136+ #[ derive( Debug ) ]
137+ struct DependentOpsLimiter {
138+ rate_per_min : u32 ,
139+ burst : u32 ,
140+ tokens : f64 ,
141+ last_refill : Instant ,
142+ }
143+
144+ impl DependentOpsLimiter {
145+ fn new ( rate_per_min : u32 , burst : u32 ) -> Option < Self > {
146+ if rate_per_min == 0 {
147+ return None ;
148+ }
149+ let burst = if burst == 0 {
150+ rate_per_min. max ( 1 )
151+ } else {
152+ burst
153+ } ;
154+ Some ( Self {
155+ rate_per_min,
156+ burst,
157+ tokens : burst as f64 ,
158+ last_refill : Instant :: now ( ) ,
159+ } )
160+ }
161+
162+ fn defer_duration ( & mut self ) -> Duration {
163+ let now = Instant :: now ( ) ;
164+ self . refill ( now) ;
165+ let rate_per_sec = self . rate_per_min as f64 / 60.0 ;
166+ if rate_per_sec <= 0.0 {
167+ return Duration :: ZERO ;
168+ }
169+ self . tokens -= 1.0 ;
170+ if self . tokens >= 0.0 {
171+ Duration :: ZERO
172+ } else {
173+ let deficit = -self . tokens ;
174+ let wait_secs = deficit / rate_per_sec;
175+ if wait_secs <= 0.0 {
176+ Duration :: ZERO
177+ } else {
178+ Duration :: from_secs_f64 ( wait_secs)
179+ }
180+ }
181+ }
182+
183+ fn refill ( & mut self , now : Instant ) {
184+ let elapsed = now. duration_since ( self . last_refill ) . as_secs_f64 ( ) ;
185+ if elapsed <= 0.0 {
186+ return ;
187+ }
188+ let rate_per_sec = self . rate_per_min as f64 / 60.0 ;
189+ let added = elapsed * rate_per_sec;
190+ if added > 0.0 {
191+ self . tokens = ( self . tokens + added) . min ( self . burst as f64 ) ;
192+ self . last_refill = now;
193+ }
194+ }
195+ }
196+
197+ fn clamp_schedule_order (
198+ last_scheduled_by_chain : & mut HashMap < Vec < u8 > , PrimitiveDateTime > ,
199+ chain_id : Vec < u8 > ,
200+ schedule_order : PrimitiveDateTime ,
201+ ) -> PrimitiveDateTime {
202+ let next = match last_scheduled_by_chain. get ( & chain_id) {
203+ Some ( last) if * last >= schedule_order => {
204+ last. saturating_add ( TimeDuration :: microseconds ( 1 ) )
205+ }
206+ _ => schedule_order,
207+ } ;
208+ last_scheduled_by_chain. insert ( chain_id, next) ;
209+ next
210+ }
211+
110212pub type Transaction < ' l > = sqlx:: Transaction < ' l , Postgres > ;
111213
112214impl Database {
@@ -129,12 +231,21 @@ impl Database {
129231 url : url. clone ( ) ,
130232 tenant_id,
131233 chain_id,
234+ chain_id_label : chain_id. to_string ( ) ,
235+ last_scheduled_by_chain : Arc :: new ( Mutex :: new ( HashMap :: new ( ) ) ) ,
132236 pool : Arc :: new ( RwLock :: new ( pool) ) ,
133237 dependence_chain : bucket_cache,
134238 tick : HeartBeat :: default ( ) ,
239+ dependent_ops_limiter : None ,
135240 } )
136241 }
137242
243+ pub fn set_dependent_ops_limiter ( & mut self , rate_per_min : u32 , burst : u32 ) {
244+ self . dependent_ops_limiter =
245+ DependentOpsLimiter :: new ( rate_per_min, burst)
246+ . map ( |limiter| Arc :: new ( Mutex :: new ( limiter) ) ) ;
247+ }
248+
138249 async fn new_pool ( url : & DatabaseURL ) -> PgPool {
139250 let options: PgConnectOptions = url. parse ( ) . expect ( "bad url" ) ;
140251 let options = options. options ( [
@@ -280,6 +391,41 @@ impl Database {
280391 ) -> Result < bool , SqlxError > {
281392 let is_scalar = !scalar_byte. is_zero ( ) ;
282393 let output_handle = result. to_vec ( ) ;
394+ let dependence_chain_id = log. dependence_chain . to_vec ( ) ;
395+ let mut schedule_order =
396+ log. block_timestamp
397+ . saturating_add ( TimeDuration :: microseconds (
398+ log. tx_depth_size as i64 ,
399+ ) ) ;
400+ if log. is_allowed && !dependencies. is_empty ( ) {
401+ if let Some ( limiter) = & self . dependent_ops_limiter {
402+ let defer_for = limiter. lock ( ) . await . defer_duration ( ) ;
403+ if defer_for. is_zero ( ) {
404+ DEPENDENT_OPS_ALLOWED
405+ . with_label_values ( & [ self . chain_id_label . as_str ( ) ] )
406+ . inc ( ) ;
407+ } else {
408+ // Defer by writing a future schedule_order; worker still
409+ // pulls earliest schedule_order, so this smooths load without blocking ingest.
410+ let defer_micros =
411+ defer_for. as_micros ( ) . min ( i64:: MAX as u128 ) as i64 ;
412+ schedule_order = schedule_order. saturating_add (
413+ TimeDuration :: microseconds ( defer_micros) ,
414+ ) ;
415+ DEPENDENT_OPS_THROTTLED
416+ . with_label_values ( & [ self . chain_id_label . as_str ( ) ] )
417+ . inc ( ) ;
418+ }
419+ }
420+ }
421+ if self . dependent_ops_limiter . is_some ( ) {
422+ let mut last_scheduled = self . last_scheduled_by_chain . lock ( ) . await ;
423+ schedule_order = clamp_schedule_order (
424+ & mut last_scheduled,
425+ dependence_chain_id. clone ( ) ,
426+ schedule_order,
427+ ) ;
428+ }
283429 let query = sqlx:: query!(
284430 r#"
285431 INSERT INTO computations (
@@ -303,13 +449,10 @@ impl Database {
303449 & dependencies,
304450 fhe_operation as i16 ,
305451 is_scalar,
306- log . dependence_chain . to_vec ( ) ,
452+ dependence_chain_id ,
307453 log. transaction_hash. map( |txh| txh. to_vec( ) ) ,
308454 log. is_allowed,
309- log. block_timestamp
310- . saturating_add( time:: Duration :: microseconds(
311- log. tx_depth_size as i64
312- ) ) ,
455+ schedule_order,
313456 !log. is_allowed,
314457 ) ;
315458 query
@@ -1094,3 +1237,47 @@ pub fn tfhe_inputs_handle(op: &TfheContractEvents) -> Vec<Handle> {
10941237 E :: Initialized ( _) | E :: Upgraded ( _) | E :: VerifyInput ( _) => vec ! [ ] ,
10951238 }
10961239}
1240+
1241+ #[ cfg( test) ]
1242+ mod tests {
1243+ use super :: * ;
1244+
1245+ #[ test]
1246+ fn dependent_ops_limiter_defers_after_burst ( ) {
1247+ let mut limiter = DependentOpsLimiter :: new ( 60 , 2 ) . unwrap ( ) ;
1248+ assert ! ( limiter. defer_duration( ) . is_zero( ) ) ;
1249+ assert ! ( limiter. defer_duration( ) . is_zero( ) ) ;
1250+ let deferred = limiter. defer_duration ( ) ;
1251+ assert ! ( deferred > Duration :: ZERO ) ;
1252+ }
1253+
1254+ #[ test]
1255+ fn dependent_ops_limiter_refills_over_time ( ) {
1256+ let mut limiter = DependentOpsLimiter :: new ( 60 , 1 ) . unwrap ( ) ;
1257+ assert ! ( limiter. defer_duration( ) . is_zero( ) ) ;
1258+ let deferred = limiter. defer_duration ( ) ;
1259+ assert ! ( deferred > Duration :: ZERO ) ;
1260+ limiter. last_refill =
1261+ Instant :: now ( ) . checked_sub ( Duration :: from_secs ( 2 ) ) . unwrap ( ) ;
1262+ let allowed_after_refill = limiter. defer_duration ( ) ;
1263+ assert ! ( allowed_after_refill. is_zero( ) ) ;
1264+ }
1265+
1266+ #[ test]
1267+ fn dependent_ops_limiter_disabled_when_rate_zero ( ) {
1268+ assert ! ( DependentOpsLimiter :: new( 0 , 10 ) . is_none( ) ) ;
1269+ }
1270+
1271+ #[ test]
1272+ fn clamp_schedule_order_is_monotonic_per_chain ( ) {
1273+ let mut last_scheduled = HashMap :: new ( ) ;
1274+ let chain_id = vec ! [ 1 , 2 , 3 ] ;
1275+ let base = PrimitiveDateTime :: MIN ;
1276+ let first =
1277+ clamp_schedule_order ( & mut last_scheduled, chain_id. clone ( ) , base) ;
1278+ assert_eq ! ( first, base) ;
1279+ let second =
1280+ clamp_schedule_order ( & mut last_scheduled, chain_id. clone ( ) , base) ;
1281+ assert ! ( second > base) ;
1282+ }
1283+ }
0 commit comments