Net-Base Časopis

06.06.2026

Visokoučinkoviti REST server u Delphi: ograničenja zahtjeva, pool dretvi i uredno ponašanje pri preopterećenju (isječak izvornog koda)

High Performance REST Server u Delphi nije brz samo zbog „brzog JSON‑a“, već zbog kontrolirane paralelnosti, strogih timeouta i urednog ponašanja pri preopterećenju. Ovaj članak pokazuje praktično primjenjiv Concurrency-Gate s semaforom i odgovorima 429/503...

06.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Zašto „High Performance“ kod REST u Delphi često zakaže zbog paralelnosti

Jedan server visokih performansi REST Delphi u praksi rijetko je ograničen samo čistim CPU-vremenom po requestu, već nekontroliranom paralelnošću: previše istovremenih zahtjeva, previše istovremenih upita bazi podataka ili blokirajući I/O (datoteka, mreža, baza podataka). Posljedica ne izgleda kao „malo sporije“, već kao lančana reakcija: više dretvi, više redova čekanja, krah connection poola, rastuće latencije, timeouti na strani klijenta i na kraju server koji i dalje „živi“, ali više ne daje stabilne odgovore.

Protivmjera nije pojedinačni trik, već svjesno ponašanje pri preopterećenju: kad server dosegne svoje granice, mora rano i deterministički odbijati (tipično HTTP 429 ili 503), umjesto da pusti zahtjeve u beskonačni red čekanja. Upravo za to služi ovaj izvorni isječak koda: lagani Concurrency-Gate (semafor) plus timeouti, koji se može integrirati u postojeće REST-endpointe – bez obzira koristite li Indy, WebBroker, Horse ili vlastiti HTTP-sloj.

Arhitektonska ideja: Concurrency-Gate ispred „resursno intenzivnog dijela“

Osnovna ideja je jednostavna: prije resursno intenzivnog dijela (pristup bazi podataka, kompleksna izvješća, veliki JSON-odgovori) rezervira se token iz semafora. Ako nema slobodnog tokena, odmah se daje kontrolirani odgovor. Važno je: ovaj gate mora biti pouzdano otpušten (try/finally), i mora biti u kodnom putu koji je stvarno skup – ne samo na samom početku request-handlera, kad ionako nakon toga slijede parser/router/autentikacija.

Na taj način se opterećenje ne „optimizira van“, već kanalizira: server odgovara na manje zahtjeva istovremeno, ali s stabilnijim latencijama. U individualnim poslovnim aplikacijama to je najčešće vrijednije od sporadičnih najboljih rezultata u sintetičkim benchmark-ovima.

Izvorni isječak: Request-limiter s timeoutom, 429/503 i telemetrijskim hookovima

Sljedeći Delphi-kod implementira Concurrency-Gate kao klasu TRestRequestGate. Temelji se na TSemaphore (iz System.SyncObjs; semafor je brojač za ograničene istovremene pristupe). Poziv gate-a vraća ili „Lease“ objekt (slično RAII: otpuštanje u destruktoru) ili se odlučuje za trenutni odgovor zbog preopterećenja. Dodatno postoje hookovi za logging/monitoring, kako biste u radu mogli vidjeti, zašto su zahtjevi bili odbijeni.

Delphi
unit RESTRequestGate;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs,
  System.Diagnostics;

type
  // Minimaler Kontext für Logging/Tracing; kann z.B. um User/Route erweitert werden.
  TRESTGateContext = record
    RequestId: string;
    Route: string;
    RemoteIp: string;
  end;

  TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

  // Hook für Betriebstelemetrie (z.B. in Datei, Syslog, Prometheus-Exporter, etc.)
  TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
                                         Decision: TRESTOverloadDecision;
                                         WaitedMs: Integer;
                                         InFlight: Integer);

  // Lease-Objekt: Freigabe des Tokens im Destructor.
  TRESTGateLease = class
  private
    FSemaphore: TSemaphore;
    FInFlightCounter: PInteger;
    FReleased: Boolean;
  public
    constructor Create(ASem: TSemaphore; ACounter: PInteger);
    destructor Destroy; override;
    procedure Release;
  end;

  TRESTRequestGate = class
  private
    FSem: TSemaphore;
    FMaxInFlight: Integer;
    FInFlight: Integer;
    FOnEvent: TRESTGateEvent;
  public
    constructor Create(AMaxInFlight: Integer);
    destructor Destroy; override;

    // TimeoutMs = 0: keine Wartezeit, sofort 429/503
    function TryAcquire(const Ctx: TRESTGateContext; TimeoutMs: Cardinal;
                        out Lease: TRESTGateLease;
                        out WaitedMs: Integer;
                        out Decision: TRESTOverloadDecision): Boolean;

    property OnEvent: TRESTGateEvent read FOnEvent write FOnEvent;
    property MaxInFlight: Integer read FMaxInFlight;
    function InFlight: Integer;
  end;

implementation

uses
  System.Math;

{ TRESTGateLease }

constructor TRESTGateLease.Create(ASem: TSemaphore; ACounter: PInteger);
begin
  inherited Create;
  FSemaphore := ASem;
  FInFlightCounter := ACounter;
  FReleased := False;
end;

destructor TRESTGateLease.Destroy;
begin
  Release;
  inherited;
end;

procedure TRESTGateLease.Release;
begin
  if FReleased then
    Exit;
  FReleased := True;

  // Erst Counter runter, dann Semaphore freigeben.
  TInterlocked.Decrement(FInFlightCounter^);
  FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
  inherited Create;
  if AMaxInFlight <= 0 then
    raise EArgumentException.Create('AMaxInFlight muss > 0 sein');

  FMaxInFlight := AMaxInFlight;
  FInFlight := 0;

  // InitialCount = MaxCount = AMaxInFlight
  FSem := TSemaphore.Create(nil, AMaxInFlight, AMaxInFlight, '');
end;

destructor TRESTRequestGate.Destroy;
begin
  FSem.Free;
  inherited;
end;

function TRESTRequestGate.InFlight: Integer;
begin
  Result := TInterlocked.CompareExchange(FInFlight, 0, 0);
end;

function TRESTRequestGate.TryAcquire(const Ctx: TRESTGateContext; TimeoutMs: Cardinal;
  out Lease: TRESTGateLease; out WaitedMs: Integer; out Decision: TRESTOverloadDecision): Boolean;
var
  Sw: TStopwatch;
  WaitRes: TWaitResult;
  CurrentInFlight: Integer;
begin
  Lease := nil;
  WaitedMs := 0;
  Decision := odRejectedBusy;

  Sw := TStopwatch.StartNew;
  if TimeoutMs = 0 then
    WaitRes := FSem.WaitFor(0)
  else
    WaitRes := FSem.WaitFor(TimeoutMs);

  WaitedMs := Integer(Min(Sw.ElapsedMilliseconds, High(Integer)));

  case WaitRes of
    wrSignaled:
      begin
        CurrentInFlight := TInterlocked.Increment(FInFlight);
        Lease := TRESTGateLease.Create(FSem, @FInFlight);
        Decision := odAccepted;
        Result := True;

        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, CurrentInFlight);
      end;

    wrTimeout:
      begin
        // wrTimeout bei TimeoutMs > 0: gezielt warten, aber begrenzen.
        Decision := odRejectedTimeout;
        Result := False;
        if Assigned(FOnEvent) then
          FOnEvent(Ctx, Decision, WaitedMs, InFlight);
      end;
  else
    begin
      // wrAbandoned/Fehlerfälle: konservativ zurückweisen
      Decision := odRejectedBusy;
      Result := False;
      if Assigned(FOnEvent) then
        FOnEvent(Ctx, Decision, WaitedMs, InFlight);
    end;
  end;
end;

end.

Svrha: stabilnost pod opterećenjem umjesto „sve istovremeno“

S MaxInFlight definirate koliko zahtjeva istovremeno smije ući u „skupi dio“. Namjerno to nije „broj CPU-jezgri“, nego operativna veličina. Kod endpoints koji su opterećeni bazom podataka često je smisleno postaviti MaxInFlight u odnosu na DB-Connection-Pool (na primjer Pool = 20, MaxInFlight = 12 do 16), kako svaki zahtjev ne bi blokirao vezu i potom povukao dodatne threadove.

Uvjeti i zamke

  • Try/Finally je obavezno: Lease se mora zajamčeno osloboditi. Ako imate iznimke u endpointu, inače će gate postati nepouzdan i server će ostati trajno „zauzet“.
  • Timeout odabrati razumno: TimeoutMs=0 je strogo ograničenje (odmah odbija). Kratak timeout (tipično 50 do 150 ms) izravnava vrhove bez stvaranja pravih redova čekanja.
  • Gate ne prerano: Autentifikacija (na primjer Bearer/JWT) ili routing može biti jeftin; semafor bi trebao zahvatiti prije stvarno skupog dijela. Suprotno: ako autentifikacija postane skupa (npr. prema vanjskom Identity-sustavu), i to treba ograničiti.
  • 429 vs 503: HTTP 429 („Too Many Requests“) odgovara kad klijenti trebaju ciljano ponovo pokušati. 503 („Service Unavailable“) odgovara kad servis privremeno općenito nije u stanju smisleno primati zahtjeve. U oba slučaja preporučuje se Retry-After-header.

Integracija u REST-Handler: Indy/WebBroker/Horse pragmatski

Snippet je namjerno framework-neutralan. Trebate samo jedno mjesto kroz koje zahtjevi „prolaze“. Tipično je globalni singleton ili gate po grupi ruta (na primjer „/reports“ manji, „/health“ bez gate). Primjer integracije kao uzor:

  • Ispuniti kontekst (RequestId, Route, RemoteIp)
  • TryAcquire s kratkim timeoutom
  • Ako se odbije, odmah poslati response (429/503) i završiti
  • Lease ostaje u scopeu do završetka skupog dijela

U Horse (middleware) gate je blizu grupe ruta. U WebBrokeru možete raditi u odgovarajućem action-handleru. Kod Indy-a ovisi o tome imate li po zahtjevu thread; gate i dalje djeluje sve dok su skupi dijelovi jasno ograničeni.

High Performance REST Server Delphi: Overload-Antworten, die Clients nicht „vergiften“

Odgovori na preopterećenje su više od status kodova. Ako klijenti na 429/503 agresivno odmah ponovno šalju, dobit ćete val ponovnih pokušaja. U heterogenim sustavnim krajolicima (mobilne aplikacije, C# Services, legacy-klijenti) pomaže dosljedno ponašanje:

  • Retry-After: na primjer 1 do 3 sekunde, ovisno o endpointu. To je jasan takt.
  • Kratko tijelo: Malo JSON kao {"error":"server_busy","requestId":"..."} je dovoljno. Veliki error-objekti opet troše CPU i propusnost.
  • Health-endpoint neograničen: Monitoring treba i pod opterećenjem i dalje davati podatke (eventualno s „degraded“-flagom).

Ako koristite reverse proxy poput nginx ispred: uskladite timeoute i buffering tamo. Proxy može rasteretiti (TLS-terminacija, Keep-Alive), ali i premjestiti opterećenje (na primjer međuspremanje velikih request-bodyja). U radu je važno da su limiti konzistentni: Proxy-Timeout > App-Timeout, inače klijenti vide „Gateway Timeout“, iako bi aplikacija uredno odbila.

Threading, DB-Pools und Keep-Alive: Wo es in der Praxis kippt

Das Gate löst das „zu viele gleichzeitig“-Problem, aber es verhindert nicht automatisch, dass ein einzelner Request übermäßig viele Ressourcen bindet. Drei typische Kipp-Punkte aus Delphi-Projekten entstehen genau an den Schnittstellen zwischen Threading, Datenbank und HTTP-Verbindungen:

  • Ein Request blockiert mehrere knappe Ressourcen: Erst eine DB-Verbindung, dann ein externer HTTP-Call, dann ein Dateizugriff. Wenn das alles im selben Request-Thread passiert, multipliziert sich die Blockadezeit. Das Gate begrenzt dann zwar die Parallelität, aber die Durchsatzleistung sinkt drastisch. Hier lohnt es sich, die Abhängigkeiten zu entkoppeln (z.B. externe Calls asynchron, Vorberechnung per Job-Queue).
  • BDE-Ablosung mit nativer Anbindung-Pooling und Transaktionen: BDE-Ablosung mit nativer Anbindung kann Connections poolen, aber eine „lange“ Transaktion (z.B. weil JSON-Erstellung oder Business-Checks zwischen StartTransaction und Commit liegen) hält die Verbindung unnötig. Eine saubere Praxis ist, die Transaktion so eng wie möglich um die eigentlichen Statements zu legen und außerhalb der Transaktion zu serialisieren oder zu validieren, wenn es fachlich geht.
  • HTTP Keep-Alive als versteckter Speicherfresser: Keep-Alive reduziert Handshakes, kann aber bei vielen Leerlauf-Clients zu vielen offenen Sockets führen. Gerade bei Windows- und Linux-Services sieht man dann nicht „CPU hoch“, sondern „Handles/FDs voll“ oder RAM durch Puffer. Hier helfen klare Idle-Timeouts im Server und am Reverse Proxy sowie ein Limit pro Client-IP, wenn es die Umgebung erlaubt.

Die Konsequenz: MaxInFlight ist kein statischer Wert. Er hängt von Ihrer langsamsten, knappsten Ressource ab (DB, externe Systeme, Storage) und davon, wie gut ein Request diese Ressourcen „zusammenhält“.

Performance-Hebel neben dem Gate: JSON, DB und I/O nicht vermischen

Das Gate stabilisiert, aber es ersetzt keine saubere Endpoint-Ökonomie. Drei Bremsen in Delphi REST-Servern tauchen wiederholt auf:

  • JSON-Building mit unnötigen Zwischenstrings: Häufig entsteht Last durch viele temporäre Unicode-Strings. Wo möglich, streaming-orientiert bauen (Writer/Stream) statt riesige Zwischenobjekte, besonders bei Listen-Endpunkten.
  • Datenbankzugriff „pro Item“: N+1-Queries und per-Row Lookups sind der Klassiker. Besser: gezielte Joins, Batch-Queries, serverseitige Aggregation. Bei sehr großen Ergebnissen lohnt sich zusätzlich Pagination mit stabiler Sortierung (damit Seiten nicht „springen“).
  • Blockierende I/O im Request-Thread: Dateizugriffe oder externe HTTP-Calls sollten entweder strikt begrenzt oder in eine asynchrone Pipeline verlagert werden. Sonst blockieren Sie teure Threads für „Warten“.

Für gewachsene digitale Unternehmenslösungen ist das oft der Knackpunkt: Ein Endpoint wurde „mal schnell“ ergänzt und funktioniert, bis reale Last und Datenvolumina kommen. Dann zeigt sich, ob Architekturgrenzen sauber gezogen wurden (Datenzugriffsschicht, Caching, Bulk-Strategien, klare Timeouts).

Debugging und Betrieb: Was Sie messen sollten

Der Hook OnEvent ist bewusst simpel. In der Praxis sollten Sie mindestens folgende Werte erfassen:

  • InFlight (aktuelle Parallelität am Gate)
  • WaitedMs (wie viel „Queueing“ Sie zulassen)
  • Decision (accepted/busy/timeout)
  • Route/RemoteIp (gruba analiza uzroka, pri čemu se zaštita podataka ne zanemaruje)

Time dobivate signal je li limit postavljen presnažno (previše 429) ili preslabo (visoki WaitedMs, rastuće latencije). I vidite dominiraju li pojedine rute. Za Windows- i Linux-Services je to u svakodnevnom radu presudno: bez telemetrije problem s performansama brzo postane pogađanje između mreže, baze podataka, proxyja i aplikacije.

Ungewöhnlich, aber extrem hilfreich: „WaitedMs“ als Frühwarnindikator

Mnogi timovi promatraju samo vrijeme odziva i CPU. WaitedMs često je bolji indikator jer pokazuje da requests već prije stvarnog rada čekaju. Ako WaitedMs raste dok CPU ostaje umjeren, ograničavajući resurs često nije CPU, nego pool (DB-veze), zaključavanje u poslovnoj logici ili vanjski downstream-service. To štedi vrijeme pri analizi uzroka jer tražite ciljano prema „Pool/Lock/I/O“ umjesto prema „optimizaciji kompajlera“.

Varianten: Pro-Route-Gates, Prioritäten und „Fast Lane“

Jedan gate za sve je jednostavan, ali ne uvijek idealan. Smislene varijante:

  • Gate po grupi ruta: „/reports“ strogo, „/api/orders“ umjereno, „/health“ otvoreno. Tako sprječavate da skupi zahtjevi za izvješća potisnu ključne procese.
  • Fast Lane za Admin/Monitoring: Odvojen gate s malom paralelnošću, kako bi operativne radnje bile moguće i pod opterećenjem.
  • Ograničenja temeljena na budžetu: Ako se veličine odgovora znatno razlikuju, može pomoći i byte-budžet (npr. maksimalno X MB istovremeno pri generiranju). To je složenije, ali realno za velike preuzimanja.

Važno: prioritetizacija brzo postane politička („moj endpoint je važniji“). Tehnički stabilno ostaje ako se prioriteti vežu uz procese (npr. unos narudžbi prije izvještavanja), a ne uz uloge ili odjele.

Fazit: Lohnt sich das Gate – und wo kippt der Ansatz?

Concurrency-Gate je pragmatični građevni blok za High Performance REST server u Delphi, jer čini preopterećenje kontroliranim i održava vaše sustave stabilnima pri vršnom opterećenju. Posebno se isplati ako imate endpoint-e vezane uz bazu podataka, ako ispred stoji reverse proxy ili ako više klijenata (legacy, portali, servisi) u valovima generira opterećenje.

Granice su jasne: ako je stvarni rad po zahtjevu preskup (neefikasni upiti, veliki JSON-objekti, blokirajući vanjski sustavi), gate samo prikriva simptome. Tada se moraju poboljšati pristupi pristupu podacima, strategije cacheiranja, timeouti i eventualno asinkrono procesiranje (Queue/Job-System). Kao sigurnosni pojas u radu, gate često čini razliku između „samo malo trom“ i „potpuno neupotrebljiv“.

Ako želite uvesti ponašanje pri preopterećenju u postojeću Delphi REST-API i REST-Server ili uredno izbalansirati limite s vremenskim ograničenjima baze podataka i proxyja: razgovarajte o projektu ili modernizaciji s Net-Base.

U stručnom kontekstu važnu ulogu imaju i Thread-Pool Delphi i Http 429 Too Many Requests kada integracije, tokovi podataka i daljnji razvoj moraju usklađeno djelovati.

Razgovarajte o projektu ili modernizaciji s Net-Base.

Sljedeći korak

Kad se tema pretvori u stvarni projekt, arhitektura, postojeći sustav i operativni rad trebaju se rano sagledati zajedno.

Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

  • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće biti odgođeni kao kasne posljedice.
  • Vidite rano koji je put ekonomski i operativno održiv.

Podijeli objavu

Izravno proslijedite ovu objavu

LinkedIn, X, XING, Facebook, WhatsApp i e-mail su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.