Hooking into message loop of dbx DataSnap user session
Asked Answered
P

2

2

Is there a way to hook into the WndProc of a dbx user session?

Background: dbx DataSnap uses Indy components for TCP communication. In its simplest form, a DataSnap server is an Indy TCP server accepting connections. When a connection is established, Indy creates a thread for that connection which handles all requests for that connection.

Each of these user connections consume resources. For a server with a couple hundred simultaneous connections, those resources can be expensive. Many of the resources could be pooled, but I don't want to always acquire and release a resource each time it is needed.

Instead, I'd like to implement a idle timer. After a thread finishes with a resource, the timer would start. If the thread accesses the resource before the timer has elapsed, the resource would still be "assigned" to that thread. But if the timer elapses before the next access, the resource would be released back to the pool. The next time the thread needs the resource, another resource would be acquired from the pool.

I haven't found a way to do this. I've tried using SetTimer but my timer callback never fires. I assume this is because Indy's WndProc for the thread isn't dispatching WM_TIMER. I have no control of the "execution loop" for this thread, so I can't easily check to see if an event has been signaled. In fact, none of my code for this thread executes unless the thread is handling a user request. And in fact, I'm wanting code to execute outside of any user request.

Solutions to the original question or suggestions for alternative approaches would be equally appreciated.

Pragmatic answered 25/1, 2012 at 21:4 Comment(4)
Remy Lebeau stated elsewhere that Indy's thread does not have a message loop. I had previously tried creating a message loop and implementing my own WndProc within the user's thread, but my WndProc never received a message. Neither SetTimer nor a traditional TTimer worked with my message loop. (Thread-safe-ness issues aside, those have already been addressed.) I've created message loops in many threads in many other applications. But never in a DataSnap or Indy TCP server.Pragmatic
I should have mentioned that I'm using Delphi XE and the DSTCP transport. In another forum, Mat DeLong suggested using TDSSessionManager.Instance.AddSessionEvent. Unfortunately, TDSSessionManager doesn't work well for TCP connections in Delphi XE. There are a couple of serious issues that have been fixed in XE2.Pragmatic
I believe your chances of doing anything real here will require XE2, which has some new capabilities which might reduce your need to even worry about mucking about with the low level hacks like this idle timer.Cave
XE2 isn't an option for us for a while. I'm really hoping someone will have a suggestion that can be done in XE. I'm not looking for someone else to create a solution. I just need some suggestions of things to look other than what I've already attempted. I'm having problems thinking outside the box on this one.Pragmatic
C
1

We tried to implement something to share resources across user threads using TCP connections (no HTTP transport, so no SessionManager), but ran into all sorts of problems. In the end we abandoned using individual user threads (set LifeCycle := TDSLifeCycle.Server) and created our own FResourcePool and FUserList (both TThreadList) in ServerContainerUnit. It only took 1 day to implement, and it works very well.

Here's a simplified version of what we did:

TResource = class
  SomeResource: TSomeType;
  UserCount: Integer;
  LastSeen: TDateTime;
end;

When a user connects, we check FResourcePool for the TResource the user needs. If it exists, we increment the resource's UserCount property. When the user is done, we decrement the UserCount property and set LastSeen. We have a TTimer that fires every 60 seconds that frees any resource with a UserCount = 0 and LastSeen greater than 60 seconds.

The FUserList is very similar. If a user hasn't been seen for several hours, we assume that their connection was severed (because our client app does an auto-disconnect if the user has been idle for 90 minutes) so we programmatically disconnect the user on the server-side, which also decrements their use of each resource. Of course, this means that we had to create a session variable ourselves (e.g., CreateGUID();) and pass that to the client when they first connect. The client passes the session id back to the server with each request so we know which FUserList record is theirs. Although this is a drawback to not using user threads, it is easily managed.

Calton answered 2/2, 2012 at 17:30 Comment(1)
Thanks for the suggestion. I'm not marking as an answer, because it doesn't answer my question. But I do appreciate the feedback. In production we've seen 160+ simultaneous users, with a considerable number being active at any given moment. The "one thread per user" model works very well for us and is not something I want to lose. I also don't want to implement my own thread handling when DataSnap already does that so well.Pragmatic
S
1

James L maybe had nailed it. Since Indy thread does not have an message loop, you have to rely in another mechanism - like read-only thread-local properties (like UserCount and / or LastSeem in his' example) - and using main thread of the server to run a TTimer for liberating resources given some rule.

EDIT: another idea is create an common data structure (example below) which is updated each time an thread finishes its' job.

WARNING: coding from mind only... It may not compile... ;-)

Example:

TThreadStatus = (tsDoingMyJob, tsFinished);

TThreadStatusInfo = class
private
  fTStatus : TThreadStatus;
  fDTFinished : TDateTime;
  procedure SetThreadStatus(value: TThreadStatus);
public
  property ThreadStatus: TThreadStatus read fTStatus write SetStatus;
  property FinishedTime: TDateTime read fDTFinished;
  procedure FinishJob ;
  procedure DoJob;
end

procedure TThreadStatusInfo.SetThreadStatus(value : TThreadStatus)
begin
  fTStatus = value;
  case fTStatus of 
    tsDoingMyJob :
       fDTFinished = TDateTime(0);
    tsFinished:
       fDTFinished = Now;
  end;
end;

procedure TThreadStatusInfo.FinishJob;
begin
  ThreadStatus := tsFinished;
end;

procedure TThreadStatusInfo.DoJob;
begin
  ThreadStatus := tsDoingMyJob;
end;

Put it in a list (any list class you like), and make sure each thread is associated with a index in that list. Removing items from the list only when you won't use that number of threads anymore (shrinking the list). Add an item when you create a new thread (example, you have 4 threads and now you need an 5th, you create a new item on main thread).

Since each thread have an index on the list, you don't need to encapsulate this write (the calls on T on a TCriticalSection.

You can read this list without trouble, using an TTimer on main thread to inspect the status of each thread. Since you have the time of each thread's finishing time you can calculate timeouts.

Suwannee answered 2/2, 2012 at 18:55 Comment(5)
My apologies for downvoting, but I'm not sure why you posted this answer. You could have commented on or upvoted James L's answer instead.Pragmatic
I planned to add another idea, but I got a priority shift which made me do an incomplete answer.Suwannee
And you are one of most kind downvoters on SO... Some appear to downvote just for sport...Suwannee
Removing the downvote, since you posted your other idea. I'll take a look at it as soon as I have a moment to spare!Pragmatic
The problem is that even if the main thread identifies the idle condition, the object that needs to be released must be released by the thread that created it. It is a COM object. Although I could marshall the object back and forth between threads, that overhead really defeats the purpose of what I'm hoping to achieve. My main thread would still need a way to signal the owning thread to release the object. If I could solve that challenge, I could do everything within the child thread. Thanks for the suggestion.Pragmatic

© 2022 - 2024 — McMap. All rights reserved.