Handling Concurrency

Handling Concurrency

Even if the new application is the result of a migration from a current VCL application which is already multi-user and client/server, a uniGUI application will serve multiple users within the same process.

A multi-user client/server VCL application usually has multiple instances of the client application running on several computers, each one of them connects to a shared database server. Almost all database servers support multiple connections by implementing row/table locking when needed.

That kind of VCL application is a typical 2-tier application. A similar uniGUI application is a 3-tier application, because the user interface (presentation layer) runs in the client browser, most of the previous application code runs on the server (now serving multiple users/sessions), and it shares the same database server.

The fact that the new non-visual code of the uniGUI application serves multiple users requires handling concurrent access to any shared resources. This concurrent access will occur in a multi-threaded manner where each request comes from a different thread.

ServerModule

Use the ServerModule singleton for global and probably read/only information.

Most web applications require access to a database server. Sometimes it will use data access controls like TFDConnection, but it is also possible to use some middleware. In any case, the information about how to connect to the database server should be available to all user sessions. If using a TFDConnection or similar, the program will need to build the correct connection string or instantiate the corresponding components.

As mentioned previously, if using FireDAC we should add the TFDManager, the driver for the chosen database, etc. If using a TADOConnection it is enough to have the connection string.

After creating the ServerModule, it should only expose read/only properties to the user sessions.

circle-exclamation

If, despite the previous warning, there is some very important reason to expose here a read/write property, it must be protected by making sure that only one session at a time can modify it. The obvious solution for this kind of protection is to use a critical section.

unit ServerModule;

interface

uses
  Classes, SysUtils, uniGUIServer, uniGUIMainModule, uniGUIApplication,
  uIdCustomHTTPServer, uniGUITypes, SyncObjs;

type
  TUniServerModule = class(TUniGUIServerModule)
    procedure UniGUIServerModuleCreate(Sender: TObject);
    procedure UniGUIServerModuleDestroy(Sender: TObject);
  private
    FConnectionString   : string;
    FSharedVariable     : string;
    FLockSharedVariable : TCriticalSection;
    function  GetSharedVariable : string;
    procedure SetSharedVariable(const Value : string);
  protected
    procedure FirstInit; override;
  public
    property ConnectionString : string read FConnectionString;
    property SharedVariable   : string read GetSharedVariable
                                       write SetSharedVariable;
  end;

function UniServerModule: TUniServerModule;

implementation

{$R *.dfm}

uses
  UniGUIVars;

function UniServerModule: TUniServerModule;
begin
  Result := TUniServerModule(UniGUIServerInstance);
end;

procedure TUniServerModule.FirstInit;
begin
  InitServerModule(Self);
end;

procedure TUniServerModule.UniGUIServerModuleCreate(Sender: TObject);
begin
  FConnectionString   := 'Some Connection String';
  FSharedVariable     := 'Shared';
  FLockSharedVariable := TCriticalSection.Create;
end;

procedure TUniServerModule.UniGUIServerModuleDestroy(Sender: TObject);
begin
  FLockSharedVariable.Free;
end;

function TUniServerModule.GetSharedVariable : string;
begin
  FLockSharedVariable.Acquire;
  try
    Result := FSharedVariable;
  finally
    FLockSharedVariable.Release;
  end;
end;

procedure TUniServerModule.SetSharedVariable(const Value : string);
begin
  FLockSharedVariable.Acquire;
  try
    FSharedVariable := Value;
  finally
    FLockSharedVariable.Release;
  end;
end;

initialization
  RegisterServerModuleClass(TUniServerModule);

end.

In this case, the connection string was assigned when creating the ServerModule, and it is a read/only property. However, the SharedVariable is a read/write property, and we must make sure that only one user can write to it. We are enforcing the single access to the variable by using a critical section. Even more, we are hiding this fact in the SetSharedVariable method so that it is not necessary to know, acquire, nor release the TCriticalSection object when accessing the property. Of course, by allowing to modify that property, we also need to control the readings. Any read/write property will slow down the application (depending on how frequently it accesses the variable).

It is important to note that while you can use writable shared variables in ServerModule as defined above, it is not recommended to rely on it as an application-wide global variable. In future each uniGUI application can be divided into many several smaller processes to implement load-balancing and other advanced feature. In this case any shared global variable in ServerModule will be visible to its owner process only, not to the other processes which will serve the same application in a pool of processes. For this, it is recommended to keep such global variables in a database table instead of keeping them in memory.

circle-exclamation

MainModule

Use the MainModule for user session variables.

Using RAD Studio for Delphi development generates correct code for a single-user VCL application, but some of that code will create issues in a multi-user or multithreaded environment. Below are examples showing why it will fail and how to fix it.

1

Problematic auto-generated form/data module globals

After asking to create a new VCL form we get this code:

Then another unit might use that global variable like this:

Form1 is accessible to any user session, and several users could be using it. Imagine what will happen if two users create the same variable (causing a memory leak of one form and an invalid reference when releasing the surviving form).

2

Corrected usage (no global variables)

A simple fix avoids that situation:

In uniGUI we don't need to keep an instance of the form in a global variable. Delete all automatically generated global variables and create your own private instance. This is a good habit for VCL applications and it requires fixing forms and data modules.

Example of an auto-generated data module with a global variable (don't use):

uniGUI is more than just a set of components for rendering the presentation layer; it is also a framework supporting the best practices for web application development. Among the supported best practices is the absence of automatically generated global variables and the automatic memory management of forms and data modules attached to a user session.

Instead of the global variable Delphi creates for forms and data modules, uniGUI creates functions which return the session instance based on the previously registered type. Let's examine MainModule as a typical example.

Notice the call to RegisterMainModuleClass during initialization. When needing access to the session's MainModule, it is enough to use the function UniMainModule. Another session function, UniApplication, will return the instance of the registered MainModule corresponding to the current session.

The same kind of solution applies to session forms and modules. If we use the uniGUI Wizard for creating a new form, and select Application Form, we will get this:

uniGUI also manages the lifetime of the form. Example of usage from a main form:

In the previous VCL code we needed to create the temporary instance of the form, open it using ShowModal, and release it (we used try ... finally to avoid a memory leak). This code only needs a call: UniForm1.ShowModal. Why?

UniForm1 creates a temporary form which is automatically released upon reaching the end of its lifetime, but it will be available until that moment.

What about showing a non-modal form? uniGUI forms are automatically released when closed.

If you need just a few data modules, you can use the MainModule for sharing your connection and other session variables, and create these data modules as "Application Data Modules". They will be attached to the session and managed by uniGUI. The code is very similar to the previous form.

If you have many forms or the data modules consume many resources, any form could handle its own data module manually by creating it on FormCreate and releasing it on FormDestroy. Still, by creating the data module as a uniGUI data module, no global variable will be created, it won't be necessary to create it in FormCreate, and if the developer forgets to release the data module in FormDestroy, it will be automatically released when closing the user session.

As the MainModule is automatically created for each new user, all variables attached to a session should be accessible through it. Never use global variables.

Free Forms and DataModules

In uniGUI you can also create free forms and datamodules. They are referred as free because their lifetime is managed by the developer, not the framework.

Links:

  • https://unigui.com/doc/online_help/free-form.htm

  • https://unigui.com/doc/online_help/free-datamodule.htm

Login Form

Once the MainModule exists, uniGUI runtime will check if a login form was registered. You can create the login form using the uniGUI Wizard for creating forms and selecting "Login Form". The only difference with any other application form is that it inherits from TUniLoginForm and it will be automatically displayed when a new session is started.

When implementing the functionality of this form, it is important to remember that it can only use the ServerModule, DataModule, and any other temporary form using them, but not the MainForm. In fact, no form should ever access the MainForm, but the MainModule.

Using Global Form and DataModule references

In uniGUI Application Forms, Application DataModules and MainModule are referenced with global functions which return an instance of that object for the current session.

For forms, this function replaces the standard variable created in VCL applications. It allows keeping the same syntax for accessing the forms:

UniForm1.Show;

Calling the UniForm1 function will create an instance of TUniForm1 or return the existing instance (making it a singleton per session). It can be used to easily create and display forms. One important detail is that these functions should not be used as global references for those forms or data modules. It is not recommended to call a form's global function from other forms or data modules for any purpose other than displaying that form.

The uniGUI framework manages the lifetime of all previous "application" objects (forms and datamodules). Each uniGUI session/MainModule keeps a list of managed forms and datamodules. A call like UniMainModule.GetFormInstance(TUniForm1) asks for a singleton of that specific form (TUniForm1). If that instance doesn't exist, it will be created. If the instance was already created, it will be returned.

In addition to the singleton factories, uniGUI also tries to optimize memory usage. The application datamodules will be automatically released when closing the session (because there is no way to know if they are in use), but forms are released as soon as they are not in use. For example, after exiting from a call to ShowModal, and receiving the result, that form can be safely released.

Using functions for accessing a form instead of global variables provides two advantages:

  • Each session has access to its own form, and only one (like a singleton per session).

  • Application forms are managed by the uniGUI framework, becoming on-demand resources.