@@ -174,7 +174,7 @@ defmodule Realtime.RateCounterTest do
174174
175175 log =
176176 capture_log ( fn ->
177- GenCounter . add ( args . id , 50 )
177+ GenCounter . add ( args . id , 6 )
178178 Process . sleep ( 300 )
179179 end )
180180
@@ -185,7 +185,7 @@ defmodule Realtime.RateCounterTest do
185185 # Splitting by the error message returns the error message and the rest of the log only
186186 assert length ( String . split ( log , "ErrorMessage: Reason" ) ) == 2
187187
188- Process . sleep ( 300 )
188+ Process . sleep ( 400 )
189189
190190 assert { :ok , % RateCounter { limit: % { triggered: false } } } = RateCounter . get ( args )
191191 end
@@ -301,6 +301,72 @@ defmodule Realtime.RateCounterTest do
301301 end
302302 end
303303
304+ describe "avg normalization" do
305+ test "avg represents events per second regardless of tick interval" do
306+ # 1-second tick: add 10 events → avg should be ~10 events/second
307+ id_1s = { :domain , :metric , Ecto.UUID . generate ( ) }
308+ args_1s = % Args { id: id_1s , opts: [ tick: 1_000 , max_bucket_len: 1 ] }
309+ { :ok , pid } = RateCounter . new ( args_1s )
310+ # wait for init to complete
311+ :sys . get_state ( pid )
312+
313+ GenCounter . add ( id_1s , 10 )
314+ { :ok , state_1s } = RateCounterHelper . tick! ( args_1s )
315+ assert_in_delta state_1s . avg , 10.0 , 0.01
316+
317+ # 5-second tick: add 50 events (= 10 per second) → avg should also be ~10 events/second
318+ id_5s = { :domain , :metric , Ecto.UUID . generate ( ) }
319+ args_5s = % Args { id: id_5s , opts: [ tick: 5_000 , max_bucket_len: 1 ] }
320+ { :ok , pid } = RateCounter . new ( args_5s )
321+ # wait for init to complete
322+ :sys . get_state ( pid )
323+
324+ GenCounter . add ( id_5s , 50 )
325+ { :ok , state_5s } = RateCounterHelper . tick! ( args_5s )
326+ assert_in_delta state_5s . avg , 10.0 , 0.01
327+ end
328+
329+ test "avg limit triggers and unsets correctly with a non-1-second tick" do
330+ id = { :domain , :metric , Ecto.UUID . generate ( ) }
331+
332+ args = % Args {
333+ id: id ,
334+ opts: [
335+ tick: 5_000 ,
336+ max_bucket_len: 1 ,
337+ limit: [
338+ value: 10 ,
339+ measurement: :avg ,
340+ log_fn: fn ->
341+ Logger . warning ( "RateLimitReached" , external_id: "tenant123" , project: "tenant123" )
342+ end
343+ ]
344+ ]
345+ }
346+
347+ { :ok , pid } = RateCounter . new ( args )
348+ # wait for init to complete
349+ :sys . get_state ( pid )
350+
351+ # 60 events over a 5-second tick = 12 events/second, above the 10/s limit
352+ log =
353+ capture_log ( fn ->
354+ GenCounter . add ( id , 60 )
355+ RateCounterHelper . tick! ( args )
356+ end )
357+
358+ assert { :ok , % RateCounter { avg: avg , limit: % { triggered: true } } } = RateCounter . get ( args )
359+ assert_in_delta avg , 12.0 , 0.01
360+ assert log =~ "RateLimitReached"
361+
362+ # 40 events over a 5-second tick = 8 events/second, below the 10/s limit
363+ GenCounter . add ( id , 40 )
364+ RateCounterHelper . tick! ( args )
365+ assert { :ok , % RateCounter { avg: avg , limit: % { triggered: false } } } = RateCounter . get ( args )
366+ assert_in_delta avg , 8.0 , 0.01
367+ end
368+ end
369+
304370 describe "publish_update/1" do
305371 test "cause shutdown with update message from update topic" do
306372 args = % Args { id: { :domain , :metric , Ecto.UUID . generate ( ) } }
0 commit comments