ObjectManager-Performance question in `XDataModule.Events.OnEntityXXX`-Events

Hey guys,
we are investigating currently a problem with slowing down performance in our token-check system. While the system is running 24/7 the query-performance with TObjectManager.Find<TToken>(TokenPk) gets slower and slower.

Performance-Checks with the TStopWatch, code:

// var lSW: TStopWatch;
  LogManager.GetLogger.Trace('CheckToken ' + sTokenId + ' ' + lSW.ElapsedMilliseconds.ToString + 'ms');
  lToken := Self.ObjectManager.Find<TToken>(sTokenId);
  if not Assigned(lToken) then
  begin
    raise EXDataHttpUnauthorized.Create('Token is revoked');
  end;
  LogManager.GetLogger.Trace('> CheckToken in DB ' + sTokenId + ' ' + lSW.ElapsedMilliseconds.ToString + 'ms');

resulting in these:

[03.02.2025 16:47:49][3220][4736][Memory usage: 9,14 MB][Trace][Value: > CheckToken in DB 869DF881-B2E8-4EB2-B3B6-0BFE53C4797A 6ms][Type: string]
// snip
[04.02.2025 10:48:38][3220][9384][Memory usage: 45,17 MB][Trace][Value: > CheckToken in DB E7451A53-70F3-4A7B-AF3E-774AE4ACEACE 64ms][Type: string]

Maybe we are using the OnEntity-Events in the wrong way and the approch of checking the Token values against the DB is wrong implemented?

We have implemented all token and access checks for Aurelius CRUD entities in the XDataModule.Events.OnEntityXXX.Subscribe (e.g. OnEntityGet) Events like this:

XDataModule.Events.OnEntityGet.Subscribe(
  procedure(Args: TEntityGetArgs)
  begin
    lUser := TXDataOperationContext.Current.Request.User;
    if lUser = nil then
    begin
      raise EXDataHttpUnauthorized.Create(UserNotAuthenticated);
    end;

    lTokenChecker := TTokenChecker.Create(TXDataOperationContext.Current.CreateManager(), lUser);
    lTokenChecker.CheckToken;
  end;

Can we use the TXDataOperationContext.Current context in these events and the ObjectManager? Or should we use the Args.Handler and cast ist to sowemthing like this:

XDataModule.Events.OnEntityGet.Subscribe(
  procedure(Args: TEntityGetArgs)
  begin
    lHandler := (Args.Handler as TXDataRequestHandler);
    lUser := lHandler.Request.User;
    if lUser = nil then
    begin
      raise EXDataHttpUnauthorized.Create(UserNotAuthenticated);
    end;

    lObjManager := TObjectManager.Create(lHandler.ConnectionPool.GetConnection);
    lHandler.AddManager(lObjManager);

    lTokenChecker := TTokenChecker.Create(lObjManager, lUser);
    lTokenChecker.CheckToken;
  end;

If we restart the service all performance values are nice again. If we run the query direct on the DB, the query always took only ~2ms.

The Token-Checker is also called from serivce-endpoints, the performance values there are always below < 10ms. Only in these entity-events get the performance bad. Our guess is the ObjectManager in these Events will be not cleared and gets full of objects.

Every help is appreciated. Thanks in advance!

Yes.

It won't make a difference. Although I agree that using the Handler is "better code", because you are using the parameter in Args, instead of using a global context. But currently, in the end, it's just the same.

I'm afraid you might have a memory leak somewhere? Are you maybe keeping something in memory as a cache?

The code you provided is not the whole history, as for example, I can't tell lTokenChecker type because it isn't even declared anywhere.

Hi, thank you for your reply!

I'm afraid you might have a memory leak somewhere? Are you maybe keeping something in memory as a cache?

We use FastMM4 to find memory-leaks, nothing reported so far. Of course we have a caching-system for our access-control-lists. But there are not involved here. All tokens check happen before we do the access-checks. The caching is loaded by startup and gets cleared and reloaded when something changes in the access-rights.

The code you provided is not the whole history, as for example, I can't tell lTokenChecker type because it isn't even declared anywhere.

Here is the full source of this unit with the token-checker:

unit Security.TokenChecker;

interface

uses
  System.SysUtils
  , Aurelius.Engine.ObjectManager
  , Sparkle.Security
  ;

type
  ITokenChecker = interface
  ['{A9418211-0F7A-4658-AB30-37E3316BCD96}']
    procedure CheckToken;
  end;

  TTokenChecker = class(TInterfacedObject, ITokenChecker)
  private
    FObjectManager: TObjectManager;
    FUserIdentity: IUserIdentity;
  protected
    property ObjectManager: TObjectManager read FObjectManager;
  public
    property UserIdentity: IUserIdentity read FUserIdentity;
  public
    procedure CheckToken;
  public
    constructor Create(const ObjectManager: TObjectManager; const UserIdentity: IUserIdentity);
  end;

implementation

uses
  System.Diagnostics
  , Bcl.Logging
  , XData.Sys.Exceptions
  , MyAureliusModel
  ;

{ TTokenChecker }

procedure TTokenChecker.CheckToken;
var
  lClaimTokenId: TUserClaim;
  lClaimTokenKind: TUserClaim;
  lToken: TSecurity_Token;
  lSW: TStopwatch;
  sTokenId: String;
begin
  lSW := TStopwatch.StartNew;
  lClaimTokenId := FUserIdentity.Claims.Find('tokenid');
  if not Assigned(lClaimTokenId) then
  begin
    raise EXDataHttpUnauthorized.Create('tokenid inavalid');
  end;

  sTokenId := lClaimTokenId.AsString;
  LogManager.GetLogger.Trace('CheckToken ' + sTokenId + ' ' + lSW.ElapsedMilliseconds.ToString + 'ms');
  lToken := Self.ObjectManager.Find<TSecurity_Token>(sTokenId);
  if not Assigned(lToken) then
  begin
    raise EXDataHttpUnauthorized.Create('Token is revoked');
  end;
  LogManager.GetLogger.Trace('> CheckToken in DB ' + sTokenId + ' ' + lSW.ElapsedMilliseconds.ToString + 'ms');

  if lToken.Expiration.HasValue then
  begin
    if lToken.Expiration < Now then
    begin
      raise EXDataHttpUnauthorized.Create('Token is expired');
    end;
  end
  else
  begin
    raise EXDataHttpUnauthorized.Create('Token is expired');
  end;
  LogManager.GetLogger.Trace('> CheckToken Expiration ' + sTokenId + ' ' + lSW.ElapsedMilliseconds.ToString + 'ms');

  if lToken.TokenKind <> TTokenKind.ApplicationToken then
  begin
    raise EXDataHttpUnauthorized.Create('Invalid Tokenkind');
  end;
  LogManager.GetLogger.Trace('CheckToken Kind ' + sTokenId + ' ' + lSW.ElapsedMilliseconds.ToString + 'ms');
  lSW.Stop;

end;

constructor TTokenChecker.Create(const ObjectManager: TObjectManager;
  const UserIdentity: IUserIdentity);
begin
  inherited Create;
  FObjectManager := ObjectManager;
  FUserIdentity := UserIdentity;
end;

end.

My favorite solution would be to implement the token-checker as middleware as a derived class from the default JWT token check class and add the checking of the token with the ObjectManager there. But i don't know how to implement or get a DB connection in the middleware to retrieve the token information there. The TXDataOperationContect.Current is not available in the middleware.

Thank you for your investigation and best regards!

That looks like a better solution indeed.

You simply create (and later destroy) a TObjectManager instance, and the connection you can get directly from the pool. The pool is thread-safe and is global, so just grab a reference to it from the middleware and use it, that's all.

Regarding your code, I don't see any issue.
Where is lTokenChecker variable declared? I don't see it in your call to XDataModule.Events.OnEntityGet.Subscribe.

You can get a hacked instance of object manager and check how many objects you have in there. Inspect it for each token check to see if the number of objects in the manager is increasing somehow. That's for now the main explanation I'd have for the decreasing in performance over time.

Ok, thank you. I will try this.

It's only a local variable in the event handler:

  XDataModule.Events.OnEntityDeleting.Subscribe(
    procedure(Args: TEntityDeletingArgs)
    var
      lTokenChecker: ITokenChecker;
    begin
...

I've already something implemented like this. Now I'm waiting for the results :slight_smile:.

Thank you!