Apache Echo Server

Hi!

We are trying to create an APACHE based Echo Server but we are having some problems.

At first we create a WebBroker project for APACHE 2.4, and follow the documentation of TMS Sparkle, but it crash when we retrieve the NodeManager and ask if it’s defined.

We take references from TMS Echo Server demo.

Here is the code.

wuModule:

unit wuModule;

interface

uses  System.SysUtils, System.Classes, Web.HTTPApp,
      Sparkle.WebBroker.Server,
      Sparkle.WebBroker.Adapter,
      Sparkle.HttpServer.Module,
      Echo.Server, Echo.Main, Echo.Entities,
      Aurelius.Drivers.Interfaces,
      Aurelius.Engine.DatabaseManager,
      uConnectionModule,
      Common,
      Sparkle.Sys.Timer,
      XData.Server.Module,
      Aurelius.Json.DataSnap,
      System.JSON,
      Server,
      FireDAC.UI.Intf,
      FireDAC.ConsoleUI.Wait,
      FireDAC.Stan.Intf,
      FireDAC.Comp.UI,
      FireDAC.VCLUI.Wait;

type
  TWebModule1 = class(TWebModule)
    FDGUIxWaitCursor1: TFDGUIxWaitCursor;
    procedure WebModule1DefaultHandlerAction(Sender: TObject;
      Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
    procedure WebModuleException(Sender: TObject; E: Exception;
      var Handled: Boolean);
  private
    { Private declarations }

  public
    { Public declarations }

  end;

procedure AEchoRoute(Log: TEchoLog; Node: TEchoNode; var Route :Boolean);
procedure Log( sLinea : string );

var
  WebModuleClass: TComponentClass = TWebModule1;

implementation

{%CLASSGROUP 'System.Classes.TPersistent'}

{$R *.dfm}

procedure Log( sLinea : string );
var
  A : TextFile;
begin
  AssignFile(A, 'C:\SEGURO\Test.txt');

  if FileExists('C:\SEGURO\Test.txt') then
    Append(A)
  else
    ReWrite(A);

  WriteLn(A, DateTimeToStr(Now) +  ' ' +  sLinea);
  CloseFile(A);
end;

procedure AEchoRoute(Log: TEchoLog; Node: TEchoNode;
  var Route: Boolean);
var
  Deserializer : TDataSnapJsonDeserializer;
  Factura : Tocfacpro;
  DetFact : TocFacProArt;
  JsonValue: TJsonValue;
begin

    Deserializer := TDataSnapJsonDeserializer.Create;
    JSonValue := TJSonValue.Create;
    JSonValue := TJSonObject.ParseJSONValue(Log.Entity);

    if SameText(Log.EntityClass, 'Common.Tocfacpro') then
       Factura := Deserializer.FromJson<Tocfacpro>(JSonValue)     else
    begin
      if SameText(Log.EntityClass, 'Common.Tocfacproart') then
       DetFact := Deserializer.FromJson<Tocfacproart>(JSonValue);
    end;


    if SameText(Log.EntityClass, 'Common.Tocfacpro') then
    begin

      Route := (Pos(Factura.Almacen.ToString, Node.Id)) > 0;
    end
    else if SameText(Log.EntityClass, 'Common.Tocfacproart') then
    begin
      Route := (Pos(DetFact.Almacen.ToString, Node.Id)) > 0;
    end
    else
      Route := True;

end;

procedure TWebModule1.WebModule1DefaultHandlerAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  Adapter: IWebBrokerAdapter;
begin
  Adapter := TWebBrokerAdapter.Create(Request, Response);
  ApacheServer.DispatchRequest(Adapter);
end;

procedure TWebModule1.WebModuleException(Sender: TObject; E: Exception;
  var Handled: Boolean);
begin
  Log(E.Message);
end;

initialization
  StartServer
finalization
  StopServer;

Server unit:

unit Server;

interface

uses
  uConnectionModule,
  Aurelius.Drivers.Interfaces,
  Aurelius.Engine.DatabaseManager,
  XData.Server.Module,
  System.SysUtils,
  Sparkle.HttpSys.Server,
  Sparkle.HttpServer.Context,
  Sparkle.HttpServer.Module,
  Sparkle.Sys.Timer,
  Sparkle.WebBroker.Server,
  Sparkle.WebBroker.Adapter,
  Echo.Server, Echo.Main;

procedure StartServer;
procedure StopServer;

var
  ApacheServer: TWebBrokerServer;
  Echo: TEcho;
  Timer: TSparkleTimer;

implementation

uses
  System.IOUtils,
  wuModule;



procedure StartServer;
var
  EchoModule: TEchoServerModule;
  Pool: IDBConnectionPool;
  NodeManager: IEchoNodeManager;
  Conn: IDBConnection;
  dmConnectionModule : TConnectionModule;
begin
  try
    if Assigned(ApacheServer) then
      Exit;
    Log('Creo el servidor');
    ApacheServer := TWebBrokerServer.Create;
    Log('DataModule creado, creo Pool');
    dmConnectionModule := TConnectionModule.Create(nil);

    Pool := dmConnectionModule.CreatePool(1000);
    Log('Pool creada, creo EchoModule');
    EchoModule := TEchoServerModule.Create(
      'http://localhost:2001/tms/echo', Pool
    );

    Log('EchoModule creado, añado EchoModule al Dispatcher');
    ApacheServer.Dispatcher.AddModule(EchoModule);
    Log('Añadido EchoModule, creo Echo');
    Echo := TEcho.Create(Pool);
     Log('Echo creado, obtengo Conn');
    // Create regular tables and fields of the application
    Conn := Echo.Pool.GetConnection;
    Log('Conn correcta, actualizo estructura BBDD');

   //CRASH! 

    //First CRASH here when apache loads the module
    //If i comment his group then crash down
    TDatabaseManager.Update(Conn);
    Log('Estructura BBDD modificado, Mapping ECHO');
    // Create tables and fields for TMS Echo usage
    TDatabaseManager.Update(Conn, TEcho.Explorer);
    Log('Mapping ECHO OK, Con = nil');
    Conn := nil; // Free reference

    Log('Obtengo NodeManager');
    // Get the NodeManager instance
    NodeManager := Echo.GetNodeManager;

    //Log('Compruebo si ya estoy registrado como UNIT SERVER');

    if not Assigned(NodeManager) then
      Log('No rula NODEMANAGER');
  
   //If i comment the TDatabaseManager, then crash here...
    Log(NodeManager.SelfNode.Name);
    // If SelfNode is already defined, no need to define it again
    if not Assigned(NodeManager.SelfNode) then
    begin
      // SelfNode is not defined, so let's create a Node instance with the name
      // specified by the DBName property ("client1", "client2", "server" in the case
      // of our demo application. It can be any string value, it just needs
      // to be unique among all other databases being replicated.
      Log('Me registro como SERVER');
      NodeManager.CreateNode('server');

      // Now define this newly created node as the SelfNode
      Log('Pongo como SelfNode -> SERVER');
      NodeManager.DefineSelfNode('server');
    end;

    Log('Inicializo TIMER');
    Timer := TSparkleTimer.Create(
      procedure(Echo: TObject)
      begin
        TEcho(Echo).BatchLoad;
        TEcho(Echo).Route(AEchoRoute);
      end,
      Echo, 2000, TTimerType.Periodic);
  except
        on E : Exception do Log(E.Message);
  end;

end;

procedure StopServer;
begin
  FreeAndNil(ApacheServer);
  FreeAndNil(Echo);
  FreeAndNil(Timer);
end;

initialization
  ApacheServer  := nil;
  Echo := nil;
  Timer := nil;
finalization
  StopServer;
end.

uConnectionModule unit:

unit uConnectionModule;

interface

uses
  Aurelius.Drivers.Interfaces,
  Aurelius.SQL.MSSQL,
  Aurelius.Schema.MSSQL,
  Aurelius.Drivers.FireDac,
  System.SysUtils, System.Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def,
  FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.MSSQL,
  FireDAC.Phys.MSSQLDef, FireDAC.FMXUI.Wait, Data.DB, FireDAC.Comp.Client,
  FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt,
  FireDAC.Comp.DataSet, FireDAC.Comp.UI, FireDAC.ConsoleUI.Wait,
  FireDAC.VCLUI.Wait;

type
  TConnectionModule = class(TDataModule)
    oConn2: TFDConnection;
    qyFlag: TFDQuery;
    FDConnection1: TFDConnection;
    FDGUIxWaitCursor1: TFDGUIxWaitCursor;
    procedure DataModuleCreate(Sender: TObject);
  private
    { Private declarations }

  public
    { Public declarations }
    function CreateConnection: IDBConnection;
    function CreateFactory: IDBConnectionFactory;

    function CreatePool(APoolSize: Integer): IDBConnectionPool;
  end;

implementation

{%CLASSGROUP 'FMX.Controls.TControl'}

{$R *.dfm}

uses
  XData.Aurelius.ConnectionPool,
  Aurelius.Drivers.Base;

{ TConnectionModule }

function TConnectionModule.CreateConnection: IDBConnection;
begin
    Result := TFireDacConnectionAdapter.Create(oConn2, False);
end;

function TConnectionModule.CreateFactory: IDBConnectionFactory;
begin
  Result := TDBConnectionFactory.Create(
    function: IDBConnection
    begin
      Result := CreateConnection;
    end
  );
end;

function TConnectionModule.CreatePool(
  APoolSize: Integer): IDBConnectionPool;
begin
  Result := TDBConnectionPool.Create(APoolSize, CreateFactory);
end;

procedure TConnectionModule.DataModuleCreate(Sender: TObject);
begin

    oConn2 := TFDConnection.Create(nil);
    oConn2.LoginPrompt := False;
    oConn2.ResourceOptions.SilentMode := True;
    oConn2.Params.Values['Database'] := 'TIENDA';
    oConn2.Params.Values['DriverID'] := 'MSSQL';
    oConn2.Params.Values['OSAutent'] := 'NO';
    oConn2.Params.Values['User_name'] := 'SA'; 
    oConn2.Params.Values['Password'] := ''; 
    oConn2.Params.Values['Server'] := 'TIENDAS';
end;

Any idea what i'm doing wrong?

What do you have in your unit that has variable dmConnectionModule and also in the dpr of the project?

Hi Wagner,

The variable dmConnectionModule is an instance of TConnectionModule that is declared in the unit uConnectionModule (that I have put you in the previous post).

The .dpr unit is:

library mod_sparkleserver;

uses
  {$IFDEF MSWINDOWS}
  Winapi.ActiveX,
  System.Win.ComObj,
  {$ENDIF }
  Web.WebBroker,
  Web.ApacheApp,
  Web.HTTPD24Impl,
  wuModule in 'wuModule.pas' {WebModule1: TWebModule},
  Common in 'Common.pas',
  uConnectionModule in 'uConnectionModule.pas' {ConnectionModule: TDataModule},
  Server in 'Server.pas';

{$R *.res}

// httpd.conf entries:
//
(*
 LoadModule sparkleserver_module modules/mod_sparkleserver.dll

 <Location /xyz>
    SetHandler mod_sparkleserver-handler
 </Location>
*)
//
// These entries assume that the output directory for this project is the apache/modules directory.
//
// httpd.conf entries should be different if the project is changed in these ways:
//   1. The TApacheModuleData variable name is changed.
//   2. The project is renamed.
//   3. The output directory is not the apache/modules directory.
//   4. The dynamic library extension depends on a platform. Use .dll on Windows and .so on Linux.
//

// Declare exported variable so that Apache can access this module.



//{$E so}
var
  GModuleData: TApacheModuleData;
exports
  GModuleData name 'sparkleserver_module';

begin
{$IFDEF MSWINDOWS}
  CoInitFlags := COINIT_MULTITHREADED;
{$ENDIF}  
  Web.ApacheApp.InitApplication(@GModuleData);
  Application.Initialize;
  Application.WebModuleClass := WebModuleClass;
  Application.Run;
end.

Thanks in advance!!!

Thank you. I overlooked the uConnectionModule. It's not correct. In CreateConnection, you must create a new TFDConnection every time it's called. Something like this:

function TConnectionModule.CreateConnection: IDBConnection;
var
oConn2: TFDConnection;
begin
oConn2 := TFDConnection.Create(nil);
oConn2.LoginPrompt := False;
oConn2.ResourceOptions.SilentMode := True;
oConn2.Params.Values['Database'] := 'TIENDA';
oConn2.Params.Values['DriverID'] := 'MSSQL';
oConn2.Params.Values['OSAutent'] := 'NO';
oConn2.Params.Values['User_name'] := 'SA';
oConn2.Params.Values['Password'] := '';
oConn2.Params.Values['Server'] := 'TIENDAS';
Result := TFireDacConnectionAdapter.Create(oConn2, True);
end;

Also, note that timers don't work in Apache modules.

Hi Wagner!

I've tried what you have indicated and it continues to give an error on the same site :(

That's the new uConnectionModule:

unit uConnectionModule;

interface

uses
  Aurelius.Drivers.Interfaces,
  Aurelius.SQL.MSSQL,
  Aurelius.Schema.MSSQL,
  Aurelius.Drivers.FireDac,
  System.SysUtils, System.Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def,
  FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.MSSQL,
  FireDAC.Phys.MSSQLDef, FireDAC.FMXUI.Wait, Data.DB, FireDAC.Comp.Client,
  FireDAC.Stan.Param, FireDAC.DatS, FireDAC.DApt.Intf, FireDAC.DApt,
  FireDAC.Comp.DataSet, FireDAC.Comp.UI, FireDAC.ConsoleUI.Wait,
  FireDAC.VCLUI.Wait;

type
  TConnectionModule = class(TDataModule)
    oConn2: TFDConnection;
    qyFlag: TFDQuery;
    FDConnection1: TFDConnection;
    FDGUIxWaitCursor1: TFDGUIxWaitCursor;
  private
    { Private declarations }

  public
    { Public declarations }
    function CreateConnection: IDBConnection;
    function CreateFactory: IDBConnectionFactory;

    function CreatePool(APoolSize: Integer): IDBConnectionPool;
  end;

implementation

{%CLASSGROUP 'FMX.Controls.TControl'}

{$R *.dfm}

uses
  XData.Aurelius.ConnectionPool,
  Aurelius.Drivers.Base;

{ TConnectionModule }

function TConnectionModule.CreateConnection: IDBConnection;
var
  oConn2 : TFDConnection;
begin
    oConn2 := TFDConnection.Create(nil);
    oConn2.LoginPrompt := False;
    oConn2.ResourceOptions.SilentMode := True;
    oConn2.Params.Values['Database'] := 'TIENDA';
    oConn2.Params.Values['DriverID'] := 'MSSQL';
    oConn2.Params.Values['OSAutent'] := 'NO';
    oConn2.Params.Values['User_name'] := 'SA'; ;
    oConn2.Params.Values['Password'] := ''; ;
    oConn2.Params.Values['Server'] := 'TIENDAS';
    Result := TFireDacConnectionAdapter.Create(oConn2, True);
end;

function TConnectionModule.CreateFactory: IDBConnectionFactory;
begin
  Result := TDBConnectionFactory.Create(
    function: IDBConnection
    begin
      Result := CreateConnection;
    end
  );
end;

function TConnectionModule.CreatePool(
  APoolSize: Integer): IDBConnectionPool;
begin
  Result := TDBConnectionPool.Create(APoolSize, CreateFactory);
end;

And I've modified the Server unit:

unit Server;

interface

uses
  uConnectionModule,
  Aurelius.Drivers.Interfaces,
  Aurelius.Engine.DatabaseManager,
  XData.Server.Module,
  System.SysUtils,
  Sparkle.HttpSys.Server,
  Sparkle.HttpServer.Context,
  Sparkle.HttpServer.Module,
  Sparkle.Sys.Timer,
  Sparkle.WebBroker.Server,
  Sparkle.WebBroker.Adapter,
  Echo.Server, Echo.Main;

procedure StartServer;
procedure StopServer;

var
  ApacheServer: TWebBrokerServer;
  Echo: TEcho;
  Timer: TSparkleTimer;

implementation

uses
  System.IOUtils,
  wuModule;



procedure StartServer;
var
  EchoModule: TEchoServerModule;
  Pool: IDBConnectionPool;
  NodeManager: IEchoNodeManager;
  Conn: IDBConnection;
  dmConnectionModule : TConnectionModule;
begin
  try
    if Assigned(ApacheServer) then
      Exit;
    Log('Creo el servidor');
    ApacheServer := TWebBrokerServer.Create;
    Log('DataModule creado, creo Pool');
    dmConnectionModule := TConnectionModule.Create(nil);

    Pool := dmConnectionModule.CreatePool(1000);
    Log('Pool creada, creo EchoModule');
    EchoModule := TEchoServerModule.Create(
      'http://localhost:2001/tms/echo', Pool
    );

    Log('EchoModule creado, añado EchoModule al Dispatcher');
    ApacheServer.Dispatcher.AddModule(EchoModule);
    Log('Añadido EchoModule, creo Echo');
    Echo := TEcho.Create(Pool);
     Log('Echo creado, obtengo Conn');
    // Create regular tables and fields of the application
    Conn := Echo.Pool.GetConnection;


    Log('Conn correcta, actualizo estructura BBDD');

    TDatabaseManager.Update(Conn); //<- Continue crashing here


    Log('Estructura BBDD modificado, Mapping ECHO');
    // Create tables and fields for TMS Echo usage
    TDatabaseManager.Update(Conn, TEcho.Explorer);
    Log('Mapping ECHO OK, Con = nil');
    Conn := nil; // Free reference

    Log('Obtengo NodeManager');
    // Get the NodeManager instance
    NodeManager := Echo.GetNodeManager;

    //Log('Compruebo si ya estoy registrado como UNIT SERVER');

    if not Assigned(NodeManager) then
      Log('No rula NODEMANAGER');

    Log(NodeManager.SelfNode.Name);
    // If SelfNode is already defined, no need to define it again
    if not Assigned(NodeManager.SelfNode) then
    begin
      // SelfNode is not defined, so let's create a Node instance with the name
      // specified by the DBName property ("client1", "client2", "server" in the case
      // of our demo application. It can be any string value, it just needs
      // to be unique among all other databases being replicated.
      Log('Me registro como SERVER');
      NodeManager.CreateNode('server');

      // Now define this newly created node as the SelfNode
      Log('Pongo como SelfNode -> SERVER');
      NodeManager.DefineSelfNode('server');
    end;

    TEcho(Echo).BatchLoad;
    TEcho(Echo).Route(AEchoRoute);
    
  except
        on E : Exception do Log(E.Message);
  end;

end;

procedure StopServer;
begin
  FreeAndNil(ApacheServer);
  FreeAndNil(Echo);
  FreeAndNil(Timer);
end;

initialization
  ApacheServer  := nil;
  Echo := nil;
  Timer := nil;
finalization
  StopServer;

Thank you!

Hi Emilio, sorry but then I can't spot what's going on just by reading your code.
What I'd suggest to you is that you first try to run your app in Apache for Windows. If the same error appears, you can then debug it and find out better what's going on.

Hi Wagner!

In the end I was able to run the Server with a new Stand-Alone application.

But when the client try to pull the changes then the server raise this exception "Unsupported Web Broker Adapter"

This is the function of the WebModule1DefaultHandlerAction:

procedure TWebModule1.WebModule1DefaultHandlerAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  Adapter: IWebBrokerAdapter;
begin
  Adapter := TWebBrokerAdapter.Create(Request, Response);
  ApacheServer.DispatchRequest(Adapter); 
end

And the client gets this error:

XData server request error:
Uri: http://212.145.30.28:2001/EchoService/SelfNode/
Status code: 500
Internal Server Error

Thanks in advance!!!!

Yes, TWebBrokerAdapter only works with Apache modules. If you don't want to use Apache, just use Indy-based server: https://download.tmssoftware.com/business/sparkle/doc/web/indy-based-server.html