B4J Question Rate Limit Worker

hatzisn

Expert
Licensed User
Longtime User
Good evening everybody,

this is the code of a worker that is supposed to create requests limits per a given period for a web app. See the code, and what I do is to set a same type variable in Main to the class instance. When though I try to check the rate limit for the IP, as seen in the picture bellow, the code breaks and the reason is that all of the variables in the class are null although they are initialized in worker initialize. What am I doing wrong and I cannot see it right now?

B4X:
Sub Class_Globals
    Private mRequests As Map
    Private tm As Timer
    Private iMaxRequests As Int = 60
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
    mRequests.Initialize
    tm.Initialize("tm", 60000)
    tm.Enabled = True
    Main.wrkLimmiter = Me
    StartMessageLoop
End Sub

Public Sub tm_Tick
    Log(mRequests)
    mRequests.Clear
End Sub

Public Sub GetWorker As wrkRateLimitter
    Return Me
End Sub

Public Sub IsRequestAboveRateLimit(sIP As String) As Boolean
    Dim su As StringUtils
    sIP = su.EncodeUrl(sIP, "UTF8")
    If mRequests.ContainsKey(sIP) Then
        mRequests.Put(sIP, mRequests.Get(sIP).As(Int) + 1)
    Else
        mRequests.Put(sIP, 1)
    End If
    Log(mRequests.Get(sIP))
    Return mRequests.Get(sIP).As(Int) > iMaxRequests
End Sub

Public Sub setMaxRequests(iMax As Int)
    iMaxRequests = iMax
End Sub

Public Sub setTimeIntervalInSeconds(iSecs As Int)
    tm.Interval = iSecs * 1000
    tm.Enabled = False
    tm.Enabled = True
End Sub


 
Last edited:

hatzisn

Expert
Licensed User
Longtime User
Some times I can be completely blind... Here is what was missing:

B4X:
' This was missing from Main.AppStart
srvr.AddBackgroundWorker("wrkRateLimitter")
 
Upvote 0

hatzisn

Expert
Licensed User
Longtime User
This is the final corrected code which is thread-safe for the rate worker.

B4X:
#Event: ContinueAfterCheckingRatetLimit(bRequestExceedsLimit As Boolean)

Sub Class_Globals
    Private mRequests As Map
    Private tm As Timer
    Private iMaxRequests As Int = 60
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
    mRequests.Initialize
    tm.Initialize("tm", 60000)
    tm.Enabled = True
    Main.wrkLimmiter = Me
    StartMessageLoop
End Sub

Public Sub tm_Tick
   ClearRequests
End Sub

Sub ClearRequests
    If Main.bLogShow Then Log(mRequests)
    mRequests.Clear
End Sub

Public Sub GetWorker As wrkRateLimitter
    Return Me
End Sub

Public Sub IsRequestAboveRateLimit(parObj As Object, sSend() As String)
    Dim su As StringUtils
 
    Dim sIP As String
    sIP = sSend(0)
    sIP = su.EncodeUrl(sIP, "UTF8")
    Dim sEv As String
    sEv = sSend(1)
 
    If mRequests.ContainsKey(sIP) Then
        mRequests.Put(sIP, mRequests.Get(sIP).As(Int) + 1)
    Else
        mRequests.Put(sIP, 1)
    End If
    Log(mRequests.Get(sIP))
 
    If SubExists(parObj, sEv & "_ContinueAfterCheckingRatetLimit") Then
        CallSubDelayed2(parObj, sEv & "_ContinueAfterCheckingRatetLimit", mRequests.Get(sIP).As(Int) > iMaxRequests)
    End If
 
End Sub

Public Sub setMaxRequests(iMax As Int)
    iMaxRequests = iMax
End Sub

Public Sub setTimeIntervalInSeconds(iSecs As Int)
    tm.Interval = iSecs * 1000
    tm.Enabled = False
    tm.Enabled = True
End Sub


It is called like this:

B4X:
CallSubDelayed3(Main.wrkLimmiter, "IsRequestAboveRateLimit", Me, Array As String(sIP, "wrkLimit"))
    StartMessageLoop

And callback is:

B4X:
Private Sub wrkLimit_ContinueAfterCheckingRatetLimit(bRequestExceedsLimit As Boolean)
    StopMessageLoop
    If bRequestExceedsLimit Then
        respmem.Status = 430
        Dim result As Map = CreateMap("success":False, "error":"e001", "message":"You have exceeded your quota for this time period.")
        WriteJson(respmem, result)
        Return
    End If
    '
    ' Do something
    '
    '
End Sub
 
Last edited:
Upvote 0

aeric

Expert
Licensed User
Longtime User
I don't understand why you use a background worker to handle rate limit.
Why not use server filter?

Edit: I get it now. It is a hybrid solution to cleanup the entries.
 
Last edited:
Upvote 0

hatzisn

Expert
Licensed User
Longtime User
I don't understand why you use a background worker to handle rate limit.
Why not use server filter?

Edit: I get it now. It is a hybrid solution to cleanup the entries.

In fact this is the only way I thought about to do it. Google Gemini made another proposal but I did my thing because there were some halucinations with keywords that do not exist. How could I do it with a server filter?
 
Upvote 0

aeric

Expert
Licensed User
Longtime User
I had a quick but long chat with Google Chrome AI mode just now.

In short, we use a server filter to block the request if the IP in a cache exceeds the limit count then a timer which act as a worker is use to do the cleanup.

The answer said you only need to add a server filter with timer, you don't need to add a background worker.
 
Last edited:
Upvote 0

aeric

Expert
Licensed User
Longtime User
Ya, Google AI also gave me haluscinate answer.

I copy the code and ask DeepSeek to fix for me.
After several attempts I got a version that has no more invalid keywords like SyncLock, B4XLock, etc.

I haven't tested it but I think you can continue to work from this code with AI.

I created a new extension with the same code and will test further when I am free.

Usage:
Add to main.
B4X:
srvr.AddFilter("/*", "RateLimiter", False)
 

Attachments

  • RateLimiter.bas
    7 KB · Views: 32
Last edited:
Upvote 0

hatzisn

Expert
Licensed User
Longtime User
Here is what I have got from Codex:


A timer inside a filter is generally the wrong design.

Suppose the filter does this:

B4X:
Sub Class_Globals
    Private tmr As Timer
End Sub

Public Sub Initialize
    tmr.Initialize("tmr", 1000)
    tmr.Enabled = True
End Sub

Because each request gets a new filter instance, the practical result is:

Request 1 → filter instance 1 → timer 1
Request 2 → filter instance 2 → timer 2
Request 3 → filter instance 3 → timer 3
...
An enabled timer remains referenced by the message queue, so its filter instance may also remain alive. You can therefore accumulate timers and filter objects instead of having one shared timer.

There is another problem:

  • AddFilter(..., False) executes the filter on a server-pool thread.
  • That request thread normally finishes after Filter returns and does not maintain a message loop.
  • Consequently, Timer_Tick should not be expected to execute reliably there. This commonly works in Debug but fails in Release—a behavior reported and explained in this B4X forum discussion.
Do not solve this by calling StartMessageLoop inside a normal filter: that would hold a server request thread and potentially prevent the HTTP request from completing.

The clean design is one persistent timer outside the filter:

B4X:
'Main module

Sub Process_Globals
    Private CleanupTimer As Timer
End Sub

Sub AppStart(Args() As String)
    CleanupTimer.Initialize("CleanupTimer", 1000)
    CleanupTimer.Enabled = True

    srvr.AddFilter("/*", "MyFilter", False)
    srvr.Start
    StartMessageLoop
End Sub

Private Sub CleanupTimer_Tick
    'Periodic application-wide work
End Sub

Then the lifecycle is:

Main/application → one persistent timer
Each request → temporary filter instance → Filter(...)
If the timer needs its own isolated thread, put it in a jServer background worker and call StartMessageLoop in that worker. Erel recommends background workers for persistent server-side timer work in this server timer discussion.

I suppose you could reform your RateLimiter filter to BackGroundWorker and then run in a filter module the code inside this worker with code like wrk.Filter(.......) inside the Filter sub of the filter.
 
Upvote 0

hatzisn

Expert
Licensed User
Longtime User
Here is an alternative final code (thread-safe) for the rate worker with dynamic maximum requests according to the calling part (send 0 for default value) that returns also the IP detected to the callback.

B4X:
#Event: ContinueAfterCheckingRatetLimit(bRequestExceedsLimit As Boolean, sIP As String)

Sub Class_Globals
    Private mRequests As Map
    Private tm As Timer
    Private iMaxRequests As Int = 30
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
    mRequests.Initialize
    tm.Initialize("tm", 60000)
    tm.Enabled = True
    Main.wrkLimmiter = Me
    StartMessageLoop
End Sub

Public Sub tm_Tick
    ClearRequests
End Sub

Sub ClearRequests
    If Main.bLogShow Then Log(mRequests)
    mRequests.Clear
End Sub

Public Sub GetWorker As wrkRateLimitter
    Return Me
End Sub

Public Sub IsRequestAboveRateLimit(parObj As Object, sSend() As String)
    Dim su As StringUtils
  
    Dim sIP As String
    sIP = sSend(0)
    sIP = su.EncodeUrl(sIP, "UTF8")
    Dim sEv As String
    sEv = sSend(1)
  
    Dim iLocalMaxRequests As Int
    If sSend(2) = "0" Then
        iLocalMaxRequests = iMaxRequests
    Else
        iLocalMaxRequests = sSend(2)
    End If
  
    If mRequests.ContainsKey(sIP) Then
        mRequests.Put(sIP, mRequests.Get(sIP).As(Int) + 1)
    Else
        mRequests.Put(sIP, 1)
    End If
  
    If SubExists(parObj, sEv & "_ContinueAfterCheckingRatetLimit") Then
        CallSubDelayed3(parObj, sEv & "_ContinueAfterCheckingRatetLimit", mRequests.Get(sIP).As(Int) > iLocalMaxRequests, sIP)
    End If
  
End Sub

Public Sub setMaxRequests(iMax As Int)
    iMaxRequests = iMax
End Sub

Public Sub setTimeIntervalInSeconds(iSecs As Int)
    tm.Interval = iSecs * 1000
    tm.Enabled = False
    tm.Enabled = True
End Sub


It is called like this:

B4X:
' The 100 is the maximum requests for this call. Use 0 for the default number of maximum requests
CallSubDelayed3(Main.wrkLimmiter, "IsRequestAboveRateLimit", Me, Array As String(sIP, "wrkLimit", 100))  
    StartMessageLoop

And callback is:

B4X:
Private Sub wrkLimit_ContinueAfterCheckingRatetLimit(bRequestExceedsLimit As Boolean, sIP As String)
    StopMessageLoop
    If bRequestExceedsLimit Then
        respmem.Status = 430
        Dim result As Map = CreateMap("success":False, "error":"e001", "message":"You have exceeded your quota for this time period.")
        WriteJson(respmem, result)
        Return
    End If
    '
    ' Do something
    '
    '
End Sub
 
Last edited:
Upvote 0

aeric

Expert
Licensed User
Longtime User
I also just shared a new library, doesn't use a timer but lazy cleanup on every N requests.
 
Upvote 0
Cookies are required to use this site. You must accept them to continue using the site. Learn more…