Net-Base Revistë

06.06.2026

Server REST me performancë të lartë në Delphi: Kufizime të kërkesave, Thread-Pool dhe sjellje e kontrolluar ndaj mbingarkesës (Fragmente burimi)

Një server High Performance REST në Delphi nuk bëhet i shpejtë vetëm nga „JSON i shpejtë“, por nga paralelizmi i kontrolluar, timeout-e të rrepta dhe sjellje e pastër ndaj mbingarkesës. Ky artikull tregon një Concurrency-Gate të përshtatshëm për praktikë me Semaphore, përgjigje 429/503...

06.06.2026

Nga tema e revistës në praktikën e projektit

Faqe shërbimi dhe teknike të përshtatshme për artikullin

Pse „High Performance“ te REST në Delphi shpesh dështon për shkak të paralelizmit

Një High Performance REST Server Delphi në praktikë rrallë kufizohet vetëm nga koha e CPU-së për çdo kërkesë, por nga paralelizmi i pakontrolluar: shumë kërkesa njëherësh, shumë query në bazën e të dhënave që ekzekutohen paralelisht ose I/O bllokuese (skedar, rrjet, bazë të dhënash). Si rezultat nuk duket thjesht „pak më ngadalë“, por si një reaksion zinxhir: më shumë Threads, më shumë radhë pritjeje, Connection-Pool-Kollaps, latença në rritje, timeouts në anën e klientit dhe në fund një server që ndoshta ende „jeton“, por nuk jep më përgjigje të qëndrueshme.

Masa kundër është jo një truk i vetëm, por një sjellje e qëllimshme Overload: Kur serveri arrin kufijtë, duhet të refuzojë herët dhe në mënyrë deterministike (tipikisht HTTP 429 ose 503), në vend që të lejojë që kërkesat të grumbullohen në një radhë të pafund. Pikërisht për këtë shërben ky copë burimi: një Concurrency-Gate i lehtë (Semaphore) plus Timeouts, i cili mund të integrohet në endpoint-et ekzistuese të REST — pavarësisht nëse përdorni Indy, WebBroker, Horse ose një shtresë HTTP të vetëpunësuar.

Ideja e arkitekturës: Concurrency-Gate para pjesës „të kushtueshme“

Ideja themelore është e thjeshtë: Para pjesës së kushtueshme (akses në bazën e të dhënave, raporte komplekse, përgjigje të mëdha JSON) rezervohen token-e nga një Semaphore. Nëse nuk ka token të lirë, jepet menjëherë një përgjigje e kontrolluar. E rëndësishme: ky Gate duhet të lirohet në mënyrë të besueshme (try/finally), dhe ai duhet të vendoset në shtigjin e kodit që është vërtet i kushtueshëm — jo vetëm në fillim të handler-it të kërkesës, kur më pas priten parser/router/autentikim.

Kështu ngarkesa nuk „zhduket“, por kanalizohet: Serveri i përgjigjet më pak kërkesave njëherësh, por me latenca më të qëndrueshme. Në aplikacionet individuale të ndërmarrjeve kjo zakonisht është më e vlefshme sesa kohët më të mira sporadike në benchmark-e sintetike.

Fragment burimi: Request-Limiter me Timeout, 429/503 dhe Telemetrie-Hooks

Kodi i mëposhtëm Delphi implementon një Concurrency-Gate si klasë TRestRequestGate. Ai bazohet në TSemaphore (nga System.SyncObjs; një Semaphore është një numërues për akseset e kufizuara njëherësh). Thirrja e Gate-it ose kthen një objekt „Lease“ (i ngjashëm me RAII: lirimi në Destructor) ose vendos për një përgjigje të menjëhershme për shkak të mbingarkesës. Përveç kësaj ka hooks për Logging/Monitoring, në mënyrë që të shihni në operim pse kërkesat u refuzuan.

unit RESTRequestGate;

interface

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

type
// Kontekst minimal për logging/tracing; p.sh. mund të zgjerohet me User/Route.
TRESTGateContext = record
RequestId: string;
Route: string;
RemoteIp: string;
end;

TRESTOverloadDecision = (odAccepted, odRejectedBusy, odRejectedTimeout);

// Hook për telemetrinë e operimit (p.sh. në skedar, Syslog, Prometheus-Exporter, etj.)
TRESTGateEvent = reference to procedure(const Ctx: TRESTGateContext;
Decision: TRESTOverloadDecision;
WaitedMs: Integer;
InFlight: Integer);

// Objekt Lease: lirimi i token-it në 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: pa kohë pritjeje, menjëherë 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;

// Së pari zvogëlohet counter-i, pastaj lirohet semaphore.
TInterlocked.Decrement(FInFlightCounter^);
FSemaphore.Release;
end;

{ TRESTRequestGate }

constructor TRESTRequestGate.Create(AMaxInFlight: Integer);
begin
inherited Create;
if AMaxInFlight <= 0 then
raise EArgumentException.Create(‚AMaxInFlight duhet të jetë > 0‘);

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 kur TimeoutMs > 0: pritje e synuar, por e kufizuar.
Decision := odRejectedTimeout;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
else
begin
// wrAbandoned/raste gabimi: refuzo në mënyrë konservatore
Decision := odRejectedBusy;
Result := False;
if Assigned(FOnEvent) then
FOnEvent(Ctx, Decision, WaitedMs, InFlight);
end;
end;
end;

end.

Qëllimi: Qëndrueshmëri nën ngarkesë në vend të „gjithçka njëherësh“

Me MaxInFlight përcaktoni sa Requests mund të hyjnë njëherësh në „pjesën me kosto të lartë“. Kjo qëllimisht nuk është „Numri i bërthamave të CPU-së“, por një madhësi operative. Tek endpoint-et me ngarkesë të bazës së të dhënave shpesh është e dobishme të vendosni MaxInFlight në relacion me DB-Connection-Pool (për shembull Pool = 20, MaxInFlight = 12 deri 16), në mënyrë që jo çdo Request të bllokojë një lidhje dhe pastaj të tërheqë thread-e të tjerë.

Kushtet kufizuese dhe kurthet

  • Try/Finally ist Pflicht: Lease duhet patjetër të lirohet. Nëse keni Exceptions në Endpoint, përndryshe porti bëhet „i rrjedhshëm“ dhe serveri mbetet përkohësisht në „busy“.
  • Timeout sinnvoll wählen: TimeoutMs=0 është një limit i prerë (refuzim menjëherë). Një timeout i shkurtër (tipikisht 50 deri 150 ms) zbut majat, pa krijuar radhë të vërteta.
  • Gate nicht zu früh: Autentikimi (për shembull Bearer/JWT) ose routing mund të jetë i favorshëm; semaphore-a duhet të veprojë përpara pjesës realisht të kushtueshme. Në anën tjetër: nëse auth bëhet i shtrenjtë (p.sh. kundrejt një sistemi identiteti jashtë), edhe ai duhet të kufizohet.
  • 429 vs 503: HTTP 429 („Too Many Requests“) i përshtatet kur klientët duhet të provojnë përsëri me qëllim. 503 („Service Unavailable“) i përshtatet kur shërbimi përkohësisht nuk është në gjendje të pranojë kërkesa në mënyrë të përshtatshme. Në të dy rastet rekomandohet një header Retry-After.

Integration in REST-Handler: Indy/WebBroker/Horse pragmatik

Snippet-i është qëllimisht neutral ndaj framework-eve. Ju nevojitet vetëm një vend ku Requests „kalojnë përmes“. Tipikisht është një Singleton global ose një Gate për grup-rutë (për shembull „/reports“ më i vogël, „/health“ pa Gate). Si shembull, një mënyrë e integrimit si model:

  • Plotësoni kontekstin (RequestId, Route, RemoteIp)
  • TryAcquire me timeout të shkurtër
  • Në rast refuzimi shkruani menjëherë response (429/503) dhe përfundoni
  • Lease jeton në scope deri pas pjesës me kosto të lartë

Në Horse (Middleware) Gate është afër një grupi rutash. Në WebBroker mund të punoni në action-handler përkatës. Tek Indy varet nëse keni një thread për çdo Request; Gate-i vepron gjithsesi, për sa kohë pjesët me kosto të lartë kufizohen qartë.

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

Përgjigjet ndaj mbingarkesës janë më shumë se kodet e statusit. Nëse klientët në 429/503 dërgojnë menjëherë përsëri në mënyrë agresive, krijoni një stuhi retries. Në peizazhe sistemesh heterogjene (Mobile Apps, C# Services, klientë legacy) ndihmon një sjellje konsistente:

  • Retry-After: p.sh. 1 deri 3 sekonda, varësisht nga Endpoint-i. Ky është një takt i qartë.
  • Kurzer Body: Një JSON i vogël si {"error":"server_busy","requestId":"..."} mjafton. Objektet e mëdha të error-it kushtojnë përsëri CPU dhe bandwidth.
  • Health-Endpoint ungedrosselt: Monitorimi duhet të japë informacion edhe nën ngarkesë (ndoshta me flag „degraded“).

Nëse përpara aplikacionit keni një reverse proxy si nginx: rregulloni atje timeouts dhe buffering. Një proxy mund të lehtësojë (TLS-Termination, Keep-Alive), por gjithashtu mund të zhvendosë ngarkesën (p.sh. të mbajë në buffer body-t e mëdhenj të Request-it). Në prodhim rëndësi ka që limitet të jenë konsistente: Proxy-Timeout > App-Timeout, përndryshe klientët do të shohin „Gateway Timeout“, megjithëse aplikacioni do të kishte refuzuar në mënyrë të duhur.

Threading, DB-Pools und Keep-Alive: Ku prishet në praktikë

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-zëvendësimi me lidhje native-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 (analizë e përafërt e shkakut, pa anashkaluar mbrojtjen e të dhënave)
  • Kjo ju jep një sinjal nëse kufijtë janë të tepërt (shumë 429) ose të dobët (WaitedMs i lartë, latenca në rritje). Dhe shihni nëse rrugë të veçanta dominojnë. Për Windows- dhe Linux-Services kjo është vendimtare në përditshmëri: Pa telemetri, një problem performancë shndërrohet shpejt në lojë gjetjeje midis rrjetit, bazës së të dhënave, proxy-t dhe aplikacionit.

    E pazakontë, por jashtëzakonisht e dobishme: „WaitedMs“ si indikator paralajmërues i hershëm

    Shumë ekipe shikojnë vetëm kohën e përgjigjes dhe CPU-në. WaitedMs shpesh është indikatori më i mirë, sepse tregon se Requests po presin tashmë para punës së vërtetë. Nëse WaitedMs rritet, ndërsa CPU mbetet moderate, burimi i pakët shpesh nuk është CPU, por një pool (lidhje DB), një bllokim në logjikën e biznesit ose një shërbim downstream i jashtëm. Kjo kursen kohë në analizën e shkaqeve, sepse kërkoni më saktësisht drejt „Pool/Lock/I/O“ në vend të „optimizimit të kompajlerit“.

    Varianta: Gate për çdo rute, prioritete dhe „Fast Lane“

    Një Gate për gjithçka është i thjeshtë, por jo gjithmonë ideal. Variante të dobishme:

    • Gate pro Route-Gruppe: „/reports“ të rreptë, „/api/orders“ të moderuara, „/health“ e hapur. Kështu parandaloni që kërkesat e shtrenjta për raporte të zëvendësojnë proceset kryesore.
    • Fast Lane für Admin/Monitoring: Gate i veçantë me paralelizëm të vogël, në mënyrë që veprimet operative të jenë të mundshme edhe gjatë ngarkesës.
    • Budget-basierte Limits: Kur madhësitë e përgjigjeve variojnë shumë, mund të ndihmojë një buxhet byte shtesë (p.sh. maksimalisht X MB njëkohësisht në gjenerim). Kjo është më komplekse, por realiste për shkarkime të mëdha.

    E rëndësishme: Prioritetizimi shndërrohet shpejt në çështje politike („“endpoint-i im është më i rëndësishëm““). Nga ana teknike, qëndron i qëndrueshëm nëse prioritetet lidhen me proceset (p.sh. regjistrimi i porosisë përpara raportimit), jo me role ose departamente.

    Përfundim: A ia vlen Gate – dhe ku dështon qasja?

    Një Concurrency-Gate është një ndërtese pragmatike për një High Performance REST Server në Delphi, sepse e bën Overload-in të kontrollueshëm dhe mban sistemet tuaja të qëndrueshme gjatë pikut të ngarkesës. Ia vlen veçanërisht nëse keni endpoint-e të lidhura me bazën e të dhënave, nëse përpara qëndron një Reverse Proxy ose nëse disa klientë (Legacy, Portale, Services) krijojnë ngarkesë në valë.

    Kufijtë janë të qartë: Nëse puna reale për çdo kërkesë është shumë e shtrenjtë (query joefikasë, objekte JSON të mëdha, sisteme të jashtme që bllokojnë), Gate vetëm maskon simptomat. Atëherë duhet të rishikohen akseset në të dhëna, strategjitë e caching, timeout-et dhe, nëse është e nevojshme, përpunimi asinokron (Queue/Job-System). Si rrip sigurie në operim, Gate shpesh është diferenca midis „pak i ngadaltë“ dhe „plotësisht i pabanueshëm“.

    Nëse dëshironi të sjellni sjelljen e Overload-it në një ekzistuesh Delphi REST-API und REST-Server ose të balanconi kufijtë me timeout-et e bazës së të dhënave dhe proxy në mënyrë të saktë: diskutojeni projektin ose përpjekjen e modernizimit me Net-Base.

    Në kontekstin teknik, edhe thread-pooli Delphi dhe Http 429 Too Many Requests luajnë një rol të rëndësishëm, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të bashkëveprojnë në mënyrë të qartë.

    Diskutoni projektin ose përpjekjen e modernizimit me Net-Base.

    Hapi tjetër

    Kur nga një temë bëhet një projekt real, arkitektura, sistemi ekzistues dhe operimi duhet të merren në konsideratë së bashku që në fazat e hershme.

    Ne nuk mbështesim vetëm në çështje të veçanta, por edhe kur nga fragmente të kodit burimor, temat legacy ose idetë për portale duhet të zhvillohen në një projekt korporativ të qëndrueshëm.

    • Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
    • REST, akses në të dhëna, portalet dhe Rollout nuk shtyhen si pasoja të mëvonshme.
    • Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.

    Ndaje postimin

    Shpërndaj këtë postim drejtpërdrejt

    LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

    Postë elektronike

    Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.