/// minimal REST server for a list of Persons stored in SQlite3
program RESTBenchmark;

{
  run the Server executable then e.g.
  - ab -n 1000 -c 100 http://localhost:8888/root/abc
      for latency measure (return the current timestamp as a few bytes)
  - ab -n 1000 -c 100 http://localhost:8888/root/xyz
      for bandwidth measure (returns some ORM query as 77KB of JSON)
}
{$ifndef UNIX}
{$APPTYPE CONSOLE}
{$endif}

{$I Synopse.inc} // LINUXNOTBSD

uses
  {$I SynDprUses.inc}  // use FastMM4 on older Delphi, or set FPC threads
  SysUtils,
  SynCommons,          // framework core
  SynTable,
  SynCrtSock,          // direct access to HTTP server
  SynLog,              // logging features
  mORMot,              // RESTful server & ORM
  mORMotSQLite3,       // SQLite3 engine as ORM core
  SynSQLite3Static,    // staticaly linked SQLite3 engine
  {$ifdef UNIX}
  BaseUnix,
  {$endif}
  {$ifdef LINUXNOTBSD}
  SynFPCLinux,
  mORMotService,
  {$endif}
  mORMotHttpServer;    // HTTP server for RESTful server

type
  TSQLPerson = class(TSQLRecordNoCase)
  private
    fFirstName: RawUTF8;
    fName: RawUTF8;
    fBirth: TDateTime;
  published
    property Name: RawUTF8 read fName write fName;
    property FirstName: RawUTF8 read fFirstName write fFirstName;
    property Birth: TDateTime read fBirth write fBirth;
  end;

  TMyServices = class(TSynPersistent)
  protected
    fDb: TSQLRestServer;
    procedure PopulateWithRandom;
  public
    constructor Create(aDB: TSQLRestServer); reintroduce;
  published
    procedure abc(ctxt: TSQLRestServerURIContext);
    procedure xyz(ctxt: TSQLRestServerURIContext);
  end;

{ TMyServices }

constructor TMyServices.Create(aDB: TSQLRestServer);
begin
  inherited Create;
  fDB := aDB;
  fDB.ServiceMethodRegisterPublishedMethods('', self); // root/abc and root/xyz
  if not fDb.TableHasRows(TSQLPerson) then
    PopulateWithRandom;
end;

procedure TMyServices.PopulateWithRandom;
var
  aBatch: TSQLRestBatch;
  aPerson: TSQLPerson;
  aTimer: TPrecisionTimer;
  i: integer;
begin
  aTimer.Start;
  aPerson := TSQLPerson.Create;
  try
    aBatch := TSQLRestBatch.Create(fDB, TSQLPerson, 10000);
    try
      for i := 1 to 1000 do begin
        aPerson.Name := FormatUTF8('% name', [CardinalToHexShort(i * 777777)]);
        aPerson.FirstName := FormatUTF8('first %', [i]);
        aPerson.Birth := i + 40000;
        aBatch.Add(aPerson, true);
      end;
      fDb.BatchSend(aBatch);
    finally
      aBatch.Free;
    end;
  finally
    aPerson.Free;
  end;
  writeln('Created 1000 entries in ', aTimer.Stop);
end;

procedure TMyServices.xyz(ctxt: TSQLRestServerURIContext);
var
  s: RawUTF8;
begin
  FormatUTF8('xyz %', [NowUTCToString], s);
  ctxt.Results([s]);
end;

procedure TMyServices.abc(ctxt: TSQLRestServerURIContext);
var
  s: RawUTF8;
begin
  s := fDB.RetrieveListJSON(TSQLPerson, '', '', true);
  ctxt.Returns(s);
end;

var
  url: AnsiString;
  keepAlive: boolean;
  aRestServer: TSQLRestServer;
  lastReadCount: TSynMonitorCount64;

procedure DoTest(const url: AnsiString; keepAlive: boolean);
var
  aHttpServer: TSQLHttpServer;
  aServices: TMyServices;
begin
  // create the main mORMot server
  aRestServer := TSQLRestServerDB.CreateWithOwnModel([TSQLPerson],'test.db',False,'root'); // authentication=false
  try
    // create tables or fields if missing
    aRestServer.CreateMissingTables;
    // initialize the services implementation class
    aServices := TMyServices.Create(aRestServer);
    try
      // serve aRestServer data over HTTP
      aHttpServer := TSQLHttpServer.Create(url,[aRestServer]);
      if not keepAlive and (aHttpServer.HttpServer is THttpServer) then
        THttpServer(aHttpServer.HttpServer).ServerKeepAliveTimeOut := 0;
      try
        aHttpServer.AccessControlAllowOrigin := '*'; // allow cross-site AJAX queries
        write('Background server is running on ', url, ' keepAlive ');
        if (keepAlive) then
          writeLn('is enabled') else
          writeLn('is disabled');
        {$ifdef LINUX}
        SynDaemonIntercept(nil);
        writeln('Press [Ctrl+C] or send SIGINT/SIGTERM to close the server.');
        fpPause;
        {$else}
        write('Press [Enter] to close the server.');
        readln;
        {$endif}
      finally
        aHttpServer.Free;
      end;
    finally
      aServices.Free;
    end;
  finally
    FreeAndNil(aRestServer);
  end;
end;

const
  UNIX_SOCK_PATH = '/tmp/rest-bench.socket';

{$ifdef LINUX}
/// killa process after X second without Get requests
function inactivityWatchdog(p: pointer): ptrint;
var currentRC: TSynMonitorCount64;
begin
  repeat
    sleep(10000); // once per 10 second
    if aRestServer = nil then // not initialized
      continue;
    currentRC := aRestServer.Stats.Read;
    if (currentRC - lastReadCount) <= 0 then begin
      SQLite3Log.Add.Log(sllServer, 'Terminating due to inactivity..');
      FpKill(GetProcessID, SIGTERM);
      break;
    end;
    lastReadCount := currentRC;
  until false;
  Result := 0;
end;
{$endif}

begin
  // set logging abilities
  SQLite3Log.Family.Level := LOG_VERBOSE;
  //SQLite3Log.Family.EchoToConsole := LOG_VERBOSE;
  SQLite3Log.Family.PerThreadLog := ptIdentifiedInOnFile;
  SQLite3Log.Family.NoFile := true; // do not create log files for benchmark
  {$ifdef UNIX}
  {$ifdef LINUXNOTBSD}
  if ProcessIsStartedBySystemd then begin
    SQLite3Log.Family.EchoToConsole := SQLite3Log.Family.Level;
    SQLite3Log.Family.EchoToConsoleUseJournal := true;
    if ExternalLibraries.sd_listen_fds(0) = 1 then
      url := '' // force to use socket passed by systemd
    else
      url := '8888';
    // set a wachdog to kill our process after 10 sec of inactivity
    // just for demo - in real life verifiing only read operations is not enought
    lastReadCount := 0;
    BeginThread(@inactivityWatchdog, nil);
  end else
  {$endif}
  if (ParamCount>0) and (ParamStr(1)='unix') then begin
    url := 'unix:' + UNIX_SOCK_PATH;
    if FileExists(UNIX_SOCK_PATH) then
      DeleteFile(UNIX_SOCK_PATH); // remove socket file
  end else
  {$endif}
    url := '8888';
  if (ParamCount>1) and (ParamStr(2)='false') then
    keepAlive := false else
    keepAlive := true;
  DoTest(url, keepAlive);
end.

