# PHP-Script aufrufen, ohne die Antwort abwarten zu müssen



## mapcado (27. Juli 2011)

Hallo, liebe Tutorialisten!

Ich habe ein Problem, mit dem ich leider nicht fertig werde. Die Lösung dafür ist schon halb fertig, allerdings weiß ich nicht, ob es vielleicht einen weitaus einfacheren Ansatz gibt.

Aber nun zu meinem Problem: Und zwar nutze ich für ein Web-Projekt die eBay-Api. Diese limitiert die Anzahl der Aufrufe auf insgesamt 18 Requests gleichzeitig, allerdings benötige ich für jede Suche sehr viele Api-Calls (teilweise mehrere 100 gleichzeitig). Ich habe mir bereits eine Funktion gebaut, die diese Anzahl Requests mittels curl_multi relativ schnell abarbeiten kann, dabei aber immer höchstens 18 Calls gleichzeitig "am Laufen" hat.

Klingt soweit eigentlich ganz gut, aber was ist nun, wenn 2 (oder noch mehr) User gleichzeitig eine Suche durchführen? Dann gäbe es einen Zeitpunkt, in dem ich 36 oder mehr Calls gleichzeitig durchführen würde. Dies führt zum einen zu einer Verlangsamung der Gesamtmenge der Requests, und zum anderen bei zu häufigen "Missbrauch" zwangsläufig zum Ausschluss aus dem eBay Developers Program.

Daher habe ich mir folgende Lösung überlegt, und wüsste jetzt gern, ob das so weiter geführt werden kann, oder evtl. völliger Humbug ist: Ich schreibe die Daten zu den ganzen Calls einfach in eine SQL-Tabelle, und lasse sie von einem Script (im folgenden einfach makeCalls.php genannt) abarbeiten, welche die Ergebnisse dann in eine weitere Tabelle schreibt. Dieses Script darf allerdings immer nur einmal gleichzeitig ausgeführt werden, daher wird in einer weiteren Tabelle vermerkt, ob es schon läuft. Wenn ja, wird es garnicht erst aufgerufen. Nun habe ich allerdings das Problem, dass das aufrufende Script schließlich immer darauf warten muss, dass makeCalls.php fertig wird, und das ist Mist, denn das erste Script wird durch eine Ajax-Anfrage vom Client sufgerufen, und soll so schnell wie möglich Daten zurückliefern, die der Client dringend braucht. Dieser kontaktiert dann wiederum über Ajax einmal in der Sekunde ein weiteres Script, das den Status (einfach eine Zahl zwischen 0 und 100%) zurückgibt, wie groß der Anteil der Calls ist, die schon durchgeführt wurden. Der Client ruft dann bei 100% ein weiteres Script auf, welches die gewünschten Ergebnisse des Api-Calls ausgibt. Wenn nun aber gerade viel los ist auf der Website, kann es ewig dauern, bis makeCalls.php fertig ist, daher keine Statusabfrage und keine Daten. Gibt es daher irgendeine Möglichkeit in PHP, ein Script, das auf dem gleichen Server liegt, aufzurufen ohne die Antwort abwarten zu müssen? In der Doku über curl oder fsockopen konnte ich leider nichts finden.

Der Königsweg ist das allerdings nicht, daher poste ich hier auch mal meine anderen Lösungsansätze:
1. makeCalls.php als cronjob laufen lassen, allerdings halte ich es für Ressourcenverschwendung, alle 100ms eine Datenbankabfrage zu machen, die mit sehr großer Wahrscheinlichkeit ohnehin nichts hergibt.
2. Das ganze über den Client laufen lassen. Ich nutze das Script, welches makeCalls.php aufruft, sozusagen für einen REST-Webdienst, den der Client über Ajax in Anspruch nimmt. Der könnte erst das erste Script aufrufen. Wenn dieses dann fertig geladen hat, also die Tabelleneinträge komplett vorgenommen hat, ruft der Client makeCalls.php auf. Mit Ajax ist es ja ganz einfach, asynchrone Anfragen auszuführen, bei denen man die Antwort garnicht erst abwarten muss. Dies ist meiner Meinung nach die einfachste Lösung, hat aber den Nachteil, dass sehr viel vom Client abhängt, und auf diesen verlasse ich mich nur sehr ungern.

Und nun, zu guter letzt noch das, was ich mir eigentlich vorstellen würde, aber - wie bereits erwähnt - keine Ahnung habe, wie es umzusetzen sein könnte: Script_1.php ruft makeCalls.php auf, und gibt, ohne auf die Antwort zu warten. sofort die gewünschten Daten an den Client weiter. makeCalls.php sieht in der SQL-Tabelle nach, ob es schon läuft und macht sich, wenn nicht, an die Ausführung der Api-Calls. Durch den Cleint erfolgen dann die Statusabfrage, sowie der Download der gewünschten Ergebnisse des Api-Calls, aber dafür habe ich schon eine Lösung.

Aber natürlich ist dieser Lösungsansatz nicht in Stein gemeißelt. Wenn ihr bessere Vorschläge habt, nur her damit. Vielleicht ist die Lösung auch ganz einfach, nur seh ich den Wald vor lauter Bäumen nicht.

So, das wars. Ich hoffe, der Text war nicht zu lang oder zu kompliziert oder beides und mein Problem einigermaßen verständlich.

PS: Wie ihr an meinem Beitrag wahrscheinlich erkennen könnt, bin ich noch ein ziemlicher Anfänger auf dem Gebiet, also tut bei euren Antworten bitte so, als würdet ihr eurer Oma erklären, worum es geht.


----------



## mapcado (28. Juli 2011)

OK, evtl. war der Text doch etwas zu lang. Für alle Lesefaulen -)) unter euch auf eine Zeile komprimiert: Ist es möglich, z.B. mit curl oder fsockopen eine .php-Datei aufzurufen, die auf dem gleichen Server liegt, die Antwort jedoch nicht abwarten zu müssen?

Pseudo-Code-mässig stelle ich mir das so vor:


```
$ch = curl_init($_SERVER['DOCUMENT_ROOT']."/script.php?option=value");
curl_setopt($ch, CURLOPT_ANTWORT_NICHT_ABWARTEN_MUESSEN, 1);
curl_exec($ch);
exit; // und zwar bevor "script.php" fertig ist
```

Nur gibt es diese curl-Option nicht, habs schon ausprobiert ;-) Also, wie geht das? Weiss das einer?


----------



## mapcado (30. Juli 2011)

Hallo nochmal!

Habe immer noch keine Lösung für das Problem gefunden, und bin langsam echt am verzweifeln. Kann mir einer von euch vielleicht einen Tipp geben, ob man das ganze in irgendeiner anderen serverseitigen Scriptsprache (perl, oder so) realisieren kann? Hat jemand von euch vielleicht mal  ein ähnliches Problem gehabt? Wenn ja, wie habt ihr es gelöst?


----------



## chmee (30. Juli 2011)

Ohne jetzt komplett durchgelesen zu haben. Eine Warteschlange macht auch auf anderen Seiten Sinn. warum nicht diese aufbauen. Anhand der Position in der Wartschlange wird eine geschätzte Zeit eruiert, runtergezählt, dazu noch eine zB 12 Stunden gültige URL mit ID angezeigt.

mfg chmee


----------



## mapcado (30. Juli 2011)

Hallo!

Erstmal vielen Dank für deine Antwort. Sowas wie eine Warteschlange realisiere ich auch, indem ich einfach alle URLs in eine SQL-Tabelle schreibe. Das Problem ist nur die Abarbeitung. Dazu brauche ich ein Script, welches garantiert höchstens einmal gleichzeitig läuft, damit nicht zu viele Requests gleichzeitig ausgeführt werden. Auch das ist eigentlich kein Problem, wenn dieses Script in einer weiteren Tabelle vermerkt, ob es schon läuft, und, wenn ja, sich sofort wieder beendet. Das *Problem* ist nur: Wenn ich dieses Script aufrufe, kann es sehr lange dauern, bis die Antwort kommt, wenn z.B. viel auf der Seite los ist, und ständig neue URLs in die Tabelle geschrieben werden, noch bevor alle abgearbeitet sind. Daher möchte ich gern ein Script aufrufen, *ohne die Antwort abwarten zu müssen*.

Oder meinst du mit "Warteschlange" vllt irgendeine PHP-Funktion, -Bibliothek, oder -Klasse, die ich nicht kenne? Konnte jetzt durch 5min Google auf die Schnelle nichts finden.


----------



## chmee (30. Juli 2011)

Die Warteschlange wird einfach anders aufgebaut. PRIMÄR wird die Abfrage nur durch die Einträge in der Warteschlange/Queue/Pipe (wie man es auch nennen will) bedient. Eine Anfrage von außen (über eine Seite) landet immer in der Queue.. Somit garantierst Du, dass die Anfragen wirklich nur sequentiell und nicht gleichzeitig ausgeführt werden. Zudem kann das im Hintergrund passieren, indem Du zB über _GET oder _POST-Variablen das Script sich selbst rekursiv aufruft - und wenn die Queue leer ist, stoppt. Somit verhinderst Du auch TimeOuts aufgrund langer Scriptzeiten..

Beim nächsten Aufruf der Seite - wenn also wiedermals eine Anfrage in die Queue geworfen wird, wird auch das Abfragescript wieder angestoßen.

mfg chmee


----------



## mapcado (30. Juli 2011)

Hallo!

Danke für die Ausführungen. Nur damit ich das richtig verstehe: Du meinst, das Script soll erst die Abfrage durchführen, und sich dann selbst mit der nächsten URL im queue wieder aufrufen. Aber dann muss das Script - bevor es sich immer wieder selbst aufruft - doch erstmal von einem anderen Script aufgerufen werden. Und das muss dann so lange warten, bis alle Rekursionen durchlaufen sind, die queue also leer ist, genau das will ich aber vermeiden. Es soll einfach nur eine kleine Notiz bekommen, dass es loslegen soll, wenn es noch nicht dabei ist. Wenn es fertig ist, kann ich das an einem weiteren Datenbankeintrag erkennen, und der Client weiß, dass er mit dem Download der Daten beginnen kann.

Oder hab ich da irgendwas falsch verstanden?

PS: Ich hab mal nen bisschen gegooglet zum Thema PHP queue, aber nur Lösungen gefunden, bei denen die Erledigung absolut nicht eilt (Mail-Notifications, oder Aktualisierung von RSS-Feeds) und die daher als cronjob (ungefähr 1 x / Minute) laufen. Das geht bei mir aber nicht, weil ich meine Seitenbesucher schlecht eine Minute auf ihr Suchergebnis warten lassen kann. Daher müsste ich den Cronjob min. 1 x / Sekunde ausführen, aber ist das nicht Ressourcenverschwendung?


----------



## chibisuke (30. Juli 2011)

Hallo,

ich könnte mir hier ne reihe unterschiedlicher ansätze vorstellen. Je nachdem wie du das ganze umsetzen willst, und was du technisch (server) für möglichkeiten hast.

1.) queue system
Du könntest die abfragen in ein queue system wie beispielsweise php-resque oder rabbitmq pushen und assynchron verarbeiten lassen. 
Erfordert auf der serverseite allerdings einen entsprechenden queueserver. 

2.) den request vorzeitig beenden
Solltest du zu den leuten gehören die gerne innovative lösungen ausprobieren und sich nicht im einem standart apache begnügen, kann ich dir php-fpm nur nahelegen. 
Damit - und nur damit - gibt es ne funktion die sich fastcgi_finish_request() nennt. 
Wird sie aufgerufen, wird die browser anfrage beendet, der PHP prozess aber weiter ausgeführt. 

3.) exec/proc_open/whatever
du kannst ein eigenes script asyncron starten und damit deine requests ausführen. 
Achtung - verursacht einiges an last

4.) batch processing
schreib die requests in ne datenbank und verwende einen zyklischen cronjob um sie zu verarbeiten.

Das sind die optionen die mir spontan einfallen um sowas umzusetzen.

Ich persönlich würde warscheinlich php-resque verwenden. Einziger nachteil dabei ist halt, dass du einen server brauchst, wo du redis und den php-reque worker laufen lassen kannst.
Der grosse vorteil ist alledings, dass du ohne probleme über die anzahl worker steuern kannst wie viele requests gleichzeitig verarbeitet werden können. (eben z.B. genau 18)

edit: hier die referenz auf github: https://github.com/chrisboulton/php-resque
zusätzlich dazu brauchst du einen REDIS server http://redis.io/


----------



## mapcado (30. Juli 2011)

Vielen Dank! Werd mir die Möglichkeiten mal ansehen und dann berichten, wofür ich mich entschieden habe. Nr. 1) und 2) werden wohl flach fallen, da ich nen Managed Server habe, bzw. ich werd mal anfragen, ob man sowas installieren könnte.

Danke nochmal und viele Grüße!


----------



## chmee (30. Juli 2011)

Um nochmal auf das Essentielle einzugehen.. Die eBay-API-Restriktionen, die Du nicht überlisten willst, setzen ganz klare Regeln. Der API ist es egal, ob Du einen Browserzugriff abbrichst, dort werden die API-Zugriffe gezählt, ob Du das nun asynchron oder sequentiell machst, ist denen soweit egal, als dass Du einfach eine bestimmte Anzahl nicht überschreiten darfst. Ich sehe da keine Möglichkeit, diese Einschränkungen zu überlisten - außer Du hast mehrere API-IDs. Wenn Du "nur" zB 1000 API-Zugriffe die Stunde machen darfst, kann auch anderer Code nix dran ändern.

Zu den Ideen von chibisuke: Die klingen doch alle recht ok. Ein managed Server wird genau in solchen Dingen ge'manage'd - damit man dem kompetenten "Manager" auch n paar Aufgaben überlassen kann. Oder Du hast nur nen Webhost.. Dann wäre - wenn meine Aussage in #6 verstanden wäre - die Wahl. Du baust eine Queue, die automatisiert abarbeitet, bis sie leer ist und erst beim nächsten Aufuruf und Füllen der Queue wieder anspringt. Ähnliches hatte ich schon Hier angesprochen, ein php-script, dass sich solange selbst aufruft, bis die Queue leer ist.

mfg chmee


----------



## mapcado (31. Juli 2011)

Du hast Recht. Die max. Anzahl der Requests ist pro 24 Srunden auf 5000 beschränkt, und das würde auf jeden Fall eng werden. Allerdings hat man, sobald man seine Anwendung zertifizieren lässt (was angeblich nicht besonders schwer sein soll) gar keine Restriktionen mehr, außer der, dass nur 18 Requests gleichzeitig erlaubt sind.

Und zu deiner Rekursionslösung: Bei mir liegt das Problem nicht, wie in deinem Beitrag hier, darin, dass ich die max_execution_time evtl. überschreiten muss, sondern darin, dass das Script, welches makeCalls.php aufruft, die Antwort abwarten muss, die evtl. sehr lange dauern kann. Und das ist bei deiner Rekursionslösung, wenn ich das so richtig überblicke, doch auch der Fall, oder?

Um zu verdeutlichen, was eigentlich gemacht werden soll, habe ich mal den grundsätzlichen Aufbau mit Paint aufgezeichnet. Sieht hässlich aus, trifft es aber ungefähr. Der Client braucht die Request-ID von initialize.php, um den Status abfragen zu können, und auch um anschließend die Ergebnisse herunterzuladen. Wenn initialize.php erst abwarten muss, bis makeCalls.php fertig ist, gibt es keine Statusanzeige, und das vergrault die User, wenn sie evtl. über 10 Sekunden werten müssen. Noch schlimmer, evtl. führt makeCalls.php noch munter einige weitere Requests durch, und der Client muss warten, obwohl seine Ergebnisse längst da sind - weil er keine Request-ID zum Herunterladen der Ergebnisse bekommt.

Und ich habe einen Virtual VPS. Ich werd am Montag mal anfragen, aber bin nicht sehr zuversichtlich, was die Installation eines solchen Servers anbelangt. Aber fragen kost ja nix...

Vielen Dank euch beiden für die guten Tipps, aber wies aussieht, werde ich das wohl so machen müssen, dass ich makeCalls.php mit der Request-ID durch den Client aufrufe.

Obwohl mir gerade ein "genialer" Gedanke kommt ;-): Könnte man nicht zwischen makeCalls.php und initialize ein weiteres Script schalten, welches nur die Aufgabe hat, die Optionen von initialize.php entgegenzunehmen, und an makeCalls.php weiterzuleiten. Jetzt setzt man die max_execution_time für dieses Script auf 1s, und Zack - makeCalls.php läuft, aber initialize.php muss nur eine Sekunde warten, bis er dem Client die Request-ID geben kann. Das muss ich gleich heute mal ausprobieren. Ich sag dann Bescheid, obs klappt.

Viele Grüße!


----------



## chmee (31. Juli 2011)

Thema *Vergraulen von Usern*
Grundsätzlich teile ich diese Meinung, ABER wenn ein Service gut/es wert ist, ist das ein minderes Problem. Siehe bfbcs.com, die haben Wartezeiten von bis zu 10 Stunden, erfreuen sich aufgrund des Service höchster Beliebtheit (~80.000+ Requests/Stunde), obwohl sie reell nur von ~50 Spielern pro Minute Daten vom offiziellen Server saugen dürfen.. Heisst also das Verhältnis liegt bei 50:1333..

Versuch Deine Idee, sowas fällt einem oft dann ein, wenn man die "Schaltlogik" mal aufgezeichnet hat  

p.s. Wie sieht es denn aus, wenn Du im Ajax-Request anstatt ReadyState 4 schon bei ReadyState 3 Daten entgegennimmst? Somit darf das Script im Hintergrund weiterarbeiten, obwohl Du gleich als Erstes die Request-ID zurückgesendet und erfolgreich verarbeitet hast.

p.p.s.: Übrigens, hast Du Dir Gedanken gemacht, ob Du nicht die Anzahl der API-Queries/Zugriff senken kannst, indem Du Anfragen kombinierst? Das halte ich für eine sinnvolle Maßnahme.

weniger Queries/User = schnellere Seitenzugriffe und mehr User/Tag realisierbar

mfg chmee


----------



## mapcado (31. Juli 2011)

Hallo nochmal.

Mein Versuch hat leider nicht geklappt. Ich habe einfach ein weiteres Script mit folgendem Inhalt erstellt:


```
$ch = curl_init("http://localhost/makeCalls.php");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// hier noch verschiedene weitere Optionen ausprobiert, siehe unten
curl_exec($ch);
```

Hier die Optionen, die ich versucht habe:

```
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
```
Bricht die Verbindung zwar nach 1 Sekunde ab, aber auch makeCalls.php wird nur eine Sekunde lang ausgeführt.


```
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
```
Gilt nur für den Verbindungsaufbau.

Außerdem habe ich noch versucht, die Ausführungszeit des Scripts einfach mit 
	
	
	



```
set_time_limit(1);
```
 zu begrenzen. Das funktioniert leider auch nicht. Die Anfrage wird komplett durchgeführt (was deutlich länger als eine Sekunde dauert), aber am Ende gibt es doch die Fehlermeldung "Maximum execution time of 1 second exceeded" Kann mir das mal einer erklären?

Trotzalledem ist, denke ich, der erste Ansatz mit 
	
	
	



```
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
```
 der vielversprechendste. In diesem Fall läuft es so: Ich lege fest, dass die *Ausführung* des curl-Befehls (ohne HTTP-Anfrage) nur 1 Sekunde dauern darf. Wenn die vorbei ist, wird die Anfrage aufgehoben. Der Server merkt das, und bricht das Script ab. Zugegeben, diese Erklärung klingt ungefähr so, als käme sie aus der Sesamstraße, aber habe ich das jetzt halbwegs richtig wiedergegeben? Ich kenne mich mit dem Aufbau von HTTP-Anfragen leider nicht besonders aus, und auch aus dem Herrn Google konnte ich auf die Schnelle nichts rauskitzeln. Daher meine Frage:* Kann man innerhalb eines PHP-Scripts irgendwie festlegen, dass es auf jeden Fall bis zum Ende ausgeführt werden soll, auch wenn die HTTP-Anfrage abgebrochen wurde*, z.B. durch Änderung der php.ini, oder Manipulation irgendwelcher Server-Variablen? Oder kann man irgendwas in den HTTP-Header des Requests reinsetzen, das den Abbruch des Scripts verhindert?

@chmee: Zum Thema Vergraulen von Usern stimme ich dir durchaus zu, und ich hoffe auch, dass ich mit dem ganzen Kram hier einen Dienst bieten kann, auf den es sich 10-20 Sekunden zu warten lohnt, zumal man sich dadurch evtl. stundenlanges Suchen bei eBay ersparen kann. Aber einen Besucher so lange ohne Statusbalken im Regen stehen zu lassen, ist nicht unbedingt die feine Art 
Und was das Senken der Request-Anzahl anbelangt: Ja, auch da versuche ich durch zig Fallunterscheidungen immer so wenig Anfragen wie möglich zu senden. Allerdings ist das mit den 5000 Requests nur eine Übergangslösung. Sobald die Website fertig ist, werd ich sie für die Zertifizierung anmelden, und wenn alles glatt läuft, hab ich dann ca. eine Woche später keine Restriktionen mehr (eben bis auf diese 18 Requests gleichzeitig).


----------

