Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – IV

Wir setzen mit diesem Beitrag unsere kleine Serie über das Polling von Statusinformationen zu lang laufenden PHP-"RUN"-Jobs auf einem PHP-Web-Server von einem Web-Browser aus fort.

Das "Status-Polling" erfolgt clientseitig mit Hilfe von Ajax-Technologien, über die periodisch CHECKER-Jobs (PHP) auf dem Server gestartet werden, welche spezifische Statusinformationen abfragen, die der RUN-Job während seiner Aktivitäten in einer Datenbank hinterlegt hat. Die Statusinformationen werden per Ajax z.B. als JSON-Objekt zum Browser transferiert und dort in geeigneter Weise angezeigt (z.B. per jQuery-Manipulationen von HTML-Elementen der aktuellen Webseite).

Hierzu hatten wir vorbereitend in folgenden Artikeln einige spezielle Punkte betrachtet. Siehe:
Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – I
Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – II
Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – III

Im ersten Beitrag hatten wir begründet, warum es sinnvoll ist, die Statusinformation in einer Datenbank und nicht in einem PHP-SESSION-Objekt zu hinterlegen. Im zweiten Beitrag dieser Serie hatten wir bereits andiskutiert, dass sowohl der der langlaufende "RUN"-Job als auch die periodisch zu startenden "Checker"-Jobs, die die hinterlegten Statusinformationen zum laufenden "RUN"-Job vom Server "pollen", von 2 getrennten Formularen ein und derselben Webseite aus über Ajax-Mechanismen gestartet werden. Ferner werden Anzeigebereiche auf der Webseite selbst oder ggf. auch ein per Javascript geöffnetes weiteres Fenster Rückmeldungen und Informationen des RUN-Jobs aufnehmen. Die Statusinformationen werden dagegen in einen definierten Anzeigebereich der Webseite eingesteuert werden.

Zu den Formular - wie auch den Anzeigebereichen der Webseite - definieren wir zur besseren Kapselung unter Javascript "Control-Objekte", die

  • sowohl die zugeordneten (X)HTML/CSS-Elemente über jQuery-Selektoren, entsprechende Eigenschaften und Methoden,
  • aber auch mehr oder weniger abstrakte innere Verarbeitungsfunktionen für Ajax-Transaktionen und Daten
  • sowie weitere benötigte Datenaufbereitungsfunktionalität

über interne Eigenschaften und Methoden repräsentieren.

Diese Control-Objects kapseln und steuern u.a. die jeweils erforderlichen Ajax-Transaktionen und legen entsprechende Eigenschaften für das XMLHttpRequest-Objekt fest. Wir hatten ferner darauf hingewiesen, dass man bzgl. des Kontextes/Scopes des "this"-Operators bei der Definition der Methoden der Control-Objekete sehr genau aufpassen muss. Bei Einsatz von jQuery hat sich diesbezüglich die Verwendung der $.proxy()-Funktionalität zum Erzwingen des gewünschten Kontextes als sehr hilfreich erwiesen.

Skizzenhafte Übersicht über das Zusammenspiel der Formulare und Jobs

Das Verhältnis zwischen RUN-Job und CHECKER-Job stellt sich wie folgt dar:

Run_Checker

Alle blauen Verbindungen zwischen dem Browser Client und dem Server symbolisieren Ajax-Transaktionen zum Server oder zugehörige Antworten vom Server zum Client.

Ein Formular "FR" übernimmt den Start des RUN-Jobs auf dem Server und übergibt diesem Job Parameter. Zu besagtem Formular gibt es ein Javascript-Control-Objekt "Ctrl_Run", das die Steuerung des Submit-Prozesss über eine eigene Methode und Ajax-Funktionalitäten von jQuery übernimmt. Dieses Control-Objekt erzeugt außerdem ein neues Browser-Fenster, auf dessen Handler sich danach das Form-Attribut "target" beziehen wird. Entweder wird dieses Attribut bereits in der HTML-Form-Definition definiert oder rechtzeitig vor dem Form-Submit per jQuery gesetzt. Die direkten z.B. per "echo" oder "print/printf" erzeugten Ausgaben des RUN-Jobs erscheinen dann in diesem (Sub-) Fenster des Browsers.

Beim Submit des "FR"-Formulars wird primär der RUN-Job gestartet. Zu beachten ist aber, dass die zugehörige "Ctrl_Run"-Methode über eine spezielle Methode eines weiteren Control-Objekts "Ctrl_Check" zum Formular "FC" auch einen "Timer"-Prozess (Loop) startet, der dann wiederum periodisch den Start eines CHECKER-Jobs auslöst. Hierauf kommen wir gleich zurück.

Man beachte, dass der Start-Button im Formular "FC" mehr symbolisch für einen Submit-Event dieses Formulars steht. Der Submit-Event kann per Javascript natürlich mit einer Methode des Kontroll-Objekts verbunden werden. Dies hatten wir im letzten Beitrag diskutiert.

Der einmal gestartete Run-Job schreibt seinen direkten Output in das dafür vorgesehen Fenster. Der RUN-Job liefert aber auch - eher später als früher - eine hoffentlich positive Ajax-Antwort zurück, für die das Control-Objekt "Ctrl_Run" Verantwortung übernehmen muss. U.a. muss spätestens dann der Timer für das periodische Starten der Checker-Jobs beendet werden. Dies kann durch Aufruf einer entsprechenden Methode des "Ctrl_Check"-Objekts erledigt werden (s.u.) (Natürlich sollte zusätzlich ein Stopp des Timers nach Ablauf eines maximal zugestandenen Zeitintervals vorgesehen werden). Ferner hinterlegt der RUN-Job Informationen zu seinem Zustand in einer dafür vorgesehenen Datenbank-Tabelle (s. den ersten Beitrag der Serie).

Genau diese Status-Informationen werden durch den über Ajax periodisch gestarteten "CHECKER"-Job per SQL abgefragt und z.B. als JSON-Objekt im Rahmen der Ajax-Antwort an den Browser-Client zurück übertragen. Das Control-Objekt für die CHECKER-Jobs stellt die ermittelte Status-Information dann in einem geeigneten HTML-Objekt (z.B. DIV) dar, das ggf. systematisch gescrollt werden muss - soweit es dies nicht selbst bei Füllen mit neuem HTML-Inhalt macht.

Bzgl. der Control-Objects beachten wir die im letzten Beitrag gemachten Ausführungen zum Scope des "this"-Operators.

Die stark vereinfachte Code-Darstellung des letzten Beitrages zeigt, wie die Control-Objekte prinzipiell aufgebaut sein müssen. Das Interessante an unserem Szenario ist, dass wir dabei parallel mit zwei Formularen und (mindestens) 2 entsprechenden Control-Objekten arbeiten. Im Fall des "Ctrl_Check"-Objekts müssen wir nun noch ein periodisches Starten des CHECKER-Jobs auf dem Server gewährleisten.

"this", setInterval() und das Control-Objekt für das "CHECKER"-Formular

Um den CHECKER-Job periodisch über Ajax anzustoßen, können wir z.B. die Javascript-Funktion "setInterval()" oder innerhalb von Loop-Strukturen auch "setTimeout()" benutzen. Ich betrachte hier nur "setInterval()". Diese Funktion des globalen "window"-Objektes nimmt als ersten Parameter die Bezeichnung einer (Callback-) Funktion auf, als zweiten Parameter die numerische Angabe eines Zeitintervalls in Millisekunden.

Folgen wir nun unserer früher propagierten Philosophie, dass Methoden eines Control-Objekts "Ctrl_Check" die Steuerung aller (Ajax-) Vorgänge im Zusammenhang mit dem CHECKER-Prozess übernehmen sollen, so müssen wir

  • einerseits "setInterval(") durch eine Methode eben dieses Kontrollobjekts aufrufen und
  • andererseits als Callback-Funktion bei der Parametrierung von setInterval() eine per "protoype"-Anweisung definierte Funktion/Methode des Control-Objekts selbst angeben.

Nun könnte man versucht sein, in Anlehnung an die Erkenntnisse des letzten Beitrags Code von ähnlicher Form wie folgender einzusetzen:

Falscher Code:

C_C = new Ctrl_Check_Obj(); 
C_C.startTimer(); 

function Ctrl_Check_Obj() {
	this.interval = 400; 
	this.num_int = 0; 
	this.max_num_int = 200;
 	...
 	this.id_status_form = "# ...."; 
	...
}

Ctrl_Check_Obj.prototype.startTimer = function () {
	this.timex = setInterval(this.submitChecker, this.interval); 
	....
};

Ctrl_Check_Obj.prototype.submitChecker = function(e) {
		
	e.preventDefault();
	....
	// Count nuber of intervals - if larger limit => stop timer 
	this.num_int++;
	if (this.num_int > this.max_num_int ) { 
		this.stopTimer(); 
	}
	....
	....
	// Prepare Ajax transaction 	
	var url = $(this.id_status_form).attr('action'); // in "action" ist der PHP-CHECKER-Job definiert !!!
	var form_data = $(this.id_status_form).serialize(); 
	var return_data_type = 'json'; 
	......
	$.ajaxSetup({
		contentType: "application/x-www-form-urlencoded; charset=ISO-8859-1",
		context:  this, 
		error: this.status_error, 
		.......
	});
	.....
	.....
	// Perform an Ajax transaction	
	$.post(url, form_data, this.status_response, return_data_type); 	
	.......		
	.......  				
};

Ctrl_Check_Obj.prototype.status_response = function(status_result) {
	// Do something with the Ajax (Json) response 
	.....
	this.msg = status_result.msg;
	.....
};

Ctrl_Check_Obj.prototype.stopTimer = function() {
	....	
	clearInterval(this.timex);
};

Das funktioniert so jedoch nicht!

Der Hauptgrund ist der, dass der "this"-Operator der Funktion setInterval() zum Zeitpunkt des Aufrufs der Callback-Funktion auf den Scope des globalen "window"-Objekt verweist - und wieder mal nicht auf den Kontext unseres Control-Objekts. Das ist eigentlich logisch: die Funktion setInterval() muss in Javascript ja völlig unabhängig von bestimmten Objekten realisiert werden. Der einzige konstante Kontext, der sich hierfür anbietet ist der globale. Alles andere erfordert eben entsprechende Zusatzmaßnahmen seitens des Entwicklers.

Der Fehler liegt also in der Definition der setTimer()-Methode - oder besser im der unreflektierten Einsatz von "this". Wie müssen wir die fehlerhafte Zeile

this.timex = setInterval(this.submitChecker, this.interval);

abändern?

Ein einfacher Ausweg könnte über den globalen Kontext des "window"-Objektes führen. Wir könnten dort globale Funktionen als Callback für setInterval() hinterlegen, die dann wiederum Methoden der definierten Control-Objekte aufrufen. So einfach wollen wir es uns aber nicht machen, denn dadurch würde das Prinzip der Kapselung in Methoden und Variablen unserer Control-Objekten durchbrochen werden.

Der Leser des letzten Beitrags vermutet schon, dass auch hier wieder der "$.proxy()"-Mechanismus von jQuery für eine elegante Lösung zum Einsatz kommen kann. Das ist richtig und sieht dann wie folgt aus:

this.timex = setInterval( $.proxy(this.submitChecker, this), this.interval);

Siehe auch:
http://stackoverflow.com/questions/14608994/jquery-plugin-scope-with-setinterval

Zu anderen - nicht jQuery-basierten - Lösungen auf der elementaren Basis von JS-Closures siehe dagegen folgende Artikel:
https://coderwall.com/p/65073w
http://www.novogeek.com/post/Scope-problems-with-JavaScript-setInterval-setTimeout-Use-closures!.aspx
http://techblog.shaneng.net/2005/04/javascript-setinterval-problem.html

In unserem Fall ergibt sich eine funktionierende Lösung auf der Basis von $.proxy() als :

C_C = new Ctrl_Check_Obj(); 
C_C.startTimer(); 

function Ctrl_Check_Obj() {
	this.interval = 400; 
	this.num_int = 0; 
	this.max_num_int = 200;
 	...
 	this.id_status_form = "# ...."; 
	...
}

Ctrl_Check_Obj.prototype.startTimer = function () {
	this.timex = setInterval( $.proxy(this.submitChecker, this), this.interval);	
	....
};

Ctrl_Check_Obj.prototype.submitChecker = function(e) {
		
	e.preventDefault();
	....
	// Count nuber of intervals - if larger limit => stop timer 
	this.num_int++;
	if (this.num_int > this.max_num_int ) { 
		this.stopTimer(); 
	}
	....
	....
	// Prepare Ajax transaction 	
	var url = $(this.id_status_form).attr('action'); 
	var form_data = $(this.id_status_form).serialize(); 
	var return_data_type = 'json'; 
	......
	$.ajaxSetup({
		contentType: "application/x-www-form-urlencoded; charset=ISO-8859-1",
		context:  this, 
		error: this.status_error, 
		.......
	});
	.....
	.....
	// Perform an Ajax transaction	
	$.post(url, form_data, this.status_response, return_data_type); 	
	.......		
	.......  				
};

Ctrl_Check_Obj.prototype.status_response = function(status_result) {
	// Do something with the Ajax (Json) response 
	.....
	this.msg = status_result.msg;
	.....
};
Ctrl_Check_Obj.prototype.status_response = function(status_result) {
	// Do something with the Ajax (Json) response 
	.....
	this.msg = status_result.msg;
	.....
};
Ctrl_Check_Obj.prototype.stopTimer = function() {
	....	
	clearInterval(this.timex);
};

Man beachte, dass das "this" im Übergabe-Parameter "this.interval" kein Problem darstellt. Der übergebene Parameter wird beim Setup der globalen Funktion setInterval() direkt im aktuellen Kontext der Ctrl_Check-Klasse ausgelesen und zur Konstruktion des Timer-Loops benutzt. Probleme macht nur der Kontext für die Callback-Funktion, die ohne Eingriffe im Scope des "window"-Objekt von Javascript erwartet werden würde.

Die Wahl eines geeigneten Polling-Zeitintervals

Ein kleiner Aspekt verdient noch etwas Beachtung. Das Schreiben der Statusinformation durch den RUN-Job erfordert Zeit. Das Erscheinen neuer Information hängt von der Art der Aufgaben ab, die der RUN-Job sequentiell erledigt. Ferner erfordert auch der Ajax-Transfer über das Netzwerk/Internet Zeit. Weder eine zu kurze noch zu lange Wahl des Polling-Zeitintervalls - im obigen Code entspricht dies der Variable "interval" der Klasse Ctrl_Check_Obj() - ist daher klug. Wählt man "interval" zu kurz, stauen sich ggf. CHECKER-JObs, ohne dass sie in jedem Lauf überhaupt was Neues an Information liefern könnten. Wählt man "interval" dagegen zu lang, so bügelt man gewissermaßen über die Taktung der Aufgaben und des zugehörigen Status des RUN-Jobs hinweg.

Eine vernünftige Wahl des Polling-Intervalls - also der Periode für das Starten der CHECKER-Jobs - ist daher primär von der zeitlichen Untergliederung, der zeitlichen Granularität des RUN-Jobs abhängig und sekündär von Netzwerk-Transfer-Zeiten, die evtl. in der gleichen Größenordnung liegen mögen. In vielen meiner Fälle ist 500 msec ein guter Startwert.

Zusammenfassung

Aus meiner Sicht habe ich hiermit die grundsätzlichen Werkzeuge beleuchtet, die auf der Javascript-Seite - also der Client-Seite für das "RUN/CHECKER"-Szenario zum Einsatz kommen sollten.

Die PHP-Seite ist eher langweilig und erschöpft sich in elementaren Datenbanktransaktionen sowie einem Standard-JSON-Encoding der gesammelten Informationen für den Ajax-Transfer. Das sind aus meiner Sicht elementare Ajax-Dinge, die hier nicht weiter beleuchtet werden müssen. Hingewiesen sei auf den möglichen Einsatz der PHP-Funktion
json_encode($ay_ajax_response);json_encode($ay_ajax_response);

zur Codierung der Resultate, die etwa in einem assoziativen Array "json_encode($ay_ajax_response)" gesammelt wurden.

Welche Informationen als Statusinformationen in der Datenbank hinterlegt, dann vom CHECKER-Job gelesen und zum Web-Client transportiert sowie schließlich im Web-Browser optisch aufbereitet und angezeigt werden, ist natürlich vom Einsatzzweck des RUN-Jobs abhängig.

Somit beenden wir nun unseren Ausflug bzgl. potentieller Fallen, in die man beim Setup eines RUN/CHECKER-Systems zum Pollen von Statusinformation von einem Web-Client aus über den Zustand eines lang laufenden Server-Jobs stolpern kann. Wir fassen abschließend einige wesentliche Punkte der Beitragsreihe zusammen:

  1. Der lang laufende PHP Server-Job "RUN" sollte seine zwischenzeitlichen Statusinformationen in eine Datenbank-Tabelle und nicht in ein SESSION-Objekt schreiben.
  2. Das Starten und die Ajax-Transaktionen für den RUN-Job und die CHECKER-Jobs können über zwei Formulare einer Webseite und parallel abgewickelt werden. Die Kontrolle der Transaktionen übernehmen "Control-Objekte", die über Methoden (prototype-Funktionen) die Ajax-Umgebung und die Callbacks für die Response/Error-Behandlung definieren.
  3. Bei der Kapselung der Ajax-Response/Error-Behandlung in Methoden der Control-Objects ist der Scope/Kontext für den "this"-Operator zu beachten. Der Einsatz der $.proxy()-Funktionalität von jQuery hilft hier, schnell, elegant und ohne explizite Ausformulierung von Closures zum Ziel zu kommen.
  4. Auch beim der Steuerung des periodischen Starten der CHECKER-Jobs mittels Methoden eines geeigneten Control-Objects und setInterval() hilft $.proxy() bei der Kapselung der periodischen Ajax-Transaktionen bzgl. CHECKER im Kontext des zuständigen Control-Objects.
  5. Das Zeitintervall für das periodische Starten der CHECKER-Jobs muss an die zeitliche Granularität der Aufgabnebehandlung im RUN-Job und an evtl. Netzwerk-Latenzen angepasst werden.

Viel Spaß nun mit der Überwachung des Status von lang laufenden PHP-Jobs von einem Web-Client aus.

Hingewiesen sei abschließend darauf, dass die gesamte Methodik natürlich auch viel allgemeinerer Weise dazu benutzt werden kann, um mehrere Jobs eines Web-Servers von einem Web-Client aus zu starten und zu überwachen. Dies ist auch deswegen interessant, weil ein evtl. gewünschtes Threading von PHP-Jobs spezielle Maßnahmen auf dem Server erfordern. Manchmal ist es viel einfacher Ajax auf dem Client einzusetzen, um mehrere Jobs auf dem Server zu starten und zu kontrollieren. Ein ggf. erforderlicher Informationsaustausch zwischen den laufenden Jobs lässt sich dabei in vielen über die Datenbank erledigen.

Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – III

Mit diesem Beitrag setze ich meine kleine Serie zu lang laufenden PHP-"RUN"s und asynchron gestarteten "CHECKER"-Programmen für Ajax-basierte Statusüberprüfungen des "RUN"s fort. Siehe:
Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – I
Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – II

Wir hatten bereits diskutiert, dass beide Jobs von ein und derselben Webseite über Ajax-Transaktionen zu starten und zu managen sind. Im besonderen ist das Status-Polling durch periodisches Starten eines "CHECKER"-Jobs zu erledigen. Das Starten erfolgt über eine periodische, jeweils nur kurze Zeit dauernde Ajax-Interaktion mit dem Server.

In beiden Fällen - beim einmaligen Start des RUNs und beim periodischen Start des CHECKERS - sind i.d.R. einige Daten zum Server zu übertragen. Diese Daten kann man z.B. in unsichtbaren Feldern eines Formulars hinterlegen und vor dem Ajax-Transfer serialisieren.

Im letzten Beitrag hatte ich bereits angedeutet, dass es im Zusammenhang mit der klaren Strukturierung der parallelen Ajax-Handhabung von mehreren Formularen folgende Vorgehensweisen sinnvoll sind:

  1. Jedes HTML Formular für einen per Ajax zu startenden Job wird auf der Javascript Seite vollständig auf ein zugehöriges, spezifisches Kontroll-Objekt "K" abgebildet. Definierte Methoden dieses Kontroll-Objektes übernehmen nicht nur die Serialisierung der asynchron zum Server zu transferierenden Daten, sondern kümmern sich auch um einen angemessene Reaktion auf Ajax-Rückmeldungen des Servers und übergeben bei Bedarf Teile der Server-Antwort an bestimmte HTML-Objekte (bzw. weitere zugehörige, abstrakte JS-Kontroll-Objekte).
  2. Um Submit-Events durch "K"-Methoden zu kontrollieren, ist die $.proxy - Funktion einzusetzen. Sie sorgt dafür, dass der "this"-Operator bei der Ersetzung der Submit-Funktionalität des HTML-Formulars (und seines Submit-Buttons) durch eine spezielle Submit-Methode (z.B. submit_event() ) des Kontroll-Objektes innerhalb dieser Methode weiterhin auf das Kontroll-Objekt zeigt - anstatt dem jQuery-Standard entsprechend auf das eventauslösende HTML-Element (also das Formular bzw. den dortigen Submit-Button).

Wir wollen diesen Punkt nachfolgend noch etwas vertiefen und anschließend beleuchten, wie sich der "this" Operator unter jQuery in den Methoden des Kontroll-Objektes zum Formular verhält. Wir berühren dabei zwar nur elementares jQuery/Ajax-Know-How, aber vielleicht ist das ja für den einen oder anderen interessant. Und es liefert die Basis für die Realisierung unseres RUN/CHECKER-Szenarios.

Einfacher Beispielcode für die Verwendung von $.proxy(), Ajax und this in einem Kontroll-Objekt zu einem Formular

Wir betrachten also ein Formular "F", für das wir ein zugehöriges Kontroll-Objekt "K" definiert haben. Der Submit-Event des Formulars soll über eine Callback-Funktion "submit_event()" des "K"-Objektes abgewickelt und kontrolliert werden. Die spezielle Submit-Methode des Kontroll-Objekts ist über eine entsprechende "prototype"-Definition festzulegen. Also z.b. etwa so:

....
....
K = new Ctrl_k(); 	// in diesem Beispiel nur der Einfachheit halber eine globale Definition   

function Ctrl_K () {

	// Define and manage handles to other JS Control objects ...... 
	// Define and manage jQuery selectors for the form and its elements .....
	.......
	this.id_status_form = "# ...."; 
	.......
	.......
	
	// Register Events - e.g. replace the submit event of the form by a local method of the "K" object
	$(this.id_status_form).submit( 
		$.proxy(this, 'submit_event') 
	);
}

Ctrl_K.prototype.submit_event = function(e) {
		
	e.preventDefault();
	....
	....
	// Prepare Ajax transaction 	
	var url = $(this.id_status_form).attr('action'); 
	var form_data = $(this.id_status_form).serialize(); 
	var return_data_type = 'json'; 
	......
	$.ajaxSetup({
		contentType: "application/x-www-form-urlencoded; charset=ISO-8859-1",
		context:  this, 
		error: this.status_error, 
		.......
	});
	.....
	.....
	// Perform an Ajax transaction	
	$.post(url, form_data, this.status_response, return_data_type); 	
	.......		
	.......  				
};

Ctrl_K.prototype.status_response = function(status_result) {
	// Do something with the Ajax (Json) response 
	.....
	this.msg = status_result.msg;
	.....
};

Ctrl_K.prototype.status_error = function(jqxhr, error_type) {
	// Analyze the error  
	.....
	.....
};

"this" in der per $.proxy() adressierten Methode "submit_event" bezieht sich in diesem Beispiel auf das K-Objekt selber und nicht, wie es ansonsten unter jQuery der Fall wäre, auf das HTML-Objekt, das den Submit-Event ausgelöst hat (z.B. der Submit-Button des Formulars). Nur wegen $.proxy() können wir "this" in der gewählten Form überhaupt erfolgreich und in seiner angestammten Bedeutung in "submit_event()" verwenden.

Das bedeutet: Durch den konsequenten Einsatz von $.proxy ergibt sich die Möglichkeit innerhalb des JS-Codes systematisch und sehr weitgehend von Details der HTML-Oberfläche zu abstrahieren, ohne die intuitive Bedeutung für Objekt-Methoden unter JS/jQuery zu verletzen. Jeder HTML-Bereich (u.a. ein Formularbereich), der mit JS/jQuery zu behandeln ist, kann in Form eines

  • zugehörigen Kontroll-Objektes und
  • zugehöriger registrierter Methoden für die Events seiner HTML-Objekte

behandelt werden. Die notwendigen Kontroll-Objekte werden i.d.R. beim Laden der Web-Seite über Initialisierungsfunktionen erzeugt. Das ermöglicht einen sehr klaren Code-Aufbau - auch dann wenn man wie im RUN/CHECKER-Szenario für ein und dieselbe Webpage gleich mehrere Formulare und Ergebnisbereiche parallel und periodisch handhaben muss.

"this" und der Kontext der Ajax Success bzw. Error-Funktion

Wir wenden uns nun einem weiteren interessanten Punkt zu. Worauf verweist eigentlich "this" in der Ajax-Callback-Funktion "status_resonse" des "K"-Objektes ? Das ist ja die Funktion, die im Falle des Erfolges der Ajax-Transaktion aufgerufen werden soll.

In unserem Beispiel oben ist die Ajax Success-Funktion als Parameter der jQuery Convenience-Funktion $.post(..)" festgelegt worden. Hätte man nur $.ajax({ ... }) zum Auslösen der Ajax-Transaktion verwendet, hätte man dort eine Callback-Funktion über
$.ajax({ ..., success: this.status_response, ... })
festgelegt. Etwas Analoges gilt natürlich für die Callback-Funktion zur Ajax-Fehlerbehandlung.

So ohne weiteres ist es leider überhaupt nicht klar, was "this" in einer Ajax-Success/Error-Callback-Funktion unter jQuery bedeutet oder bedeuten soll. Die Doku (http://api.jquery.com/jQuery.ajax/) sagt dazu:

By default, the context is an object that represents the ajax settings used in the call ($.ajaxSettings merged with the settings passed to $.ajax).

Na, wunderbar. Damit bezöge sich das "this" in unserer schönen Methode des "K"-Objekts also leider gar nicht auf das "K"-Objekt. Das diese Feststellung zutrifft, sollte der Leser selber testen. Siehe aber die Links unten.

Das wollen wir aber nicht. Wenn wir eines klareren Programm-Designs halber schon ein abstraktes Kontroll-Objekt einführen, soll das "this" in seinen Methoden bitte schön auch immer auf das Kontroll-Objekt selbst verweisen - Ajax hin oder her. jQuery ist das aber zu Recht egal:

Bei etwas Nachdenken wird einem klar, dass der Kontext, auf den sich "this" im Ajax Success-Callbakc beziehen soll, sich auch nicht wirklich standardmäßig zuweisen lassen kann. Denn man hätte die Callback-Funktion ja auch direkt deklarieren können - nach dem Muster:

$.ajaxSetup({ 
	..., 
	success: function () {
		this.xxx = , ... 
	}, 
	.....
	.....
})      

Was soll "this" denn dann sein ??? Das hängt wohl von Bedarf und Design der Applikation ab. Die Antwort ist also:

Der Entwickler muss den Context für die Callback-Funktion explizit setzen. Er ergibt sich in sinnvoller Weise nicht von allein. Dazu verwendet man das Attribut "context" in demjenigen Objekt, das wir bei lokalen Ajax-Transaktionen der Funktion $.ajax() oder bei einer globalen Festlegung für alle Ajax -Transaktionen der Funktion $.ajaxSetup() übergeben.

Viele Entwickler weisen nun den Context des Success-Callbacks meist einem (hidden) HTML-Objekt zu. In unserem Ansatz mit einem abstrakten Kontroll-Objekt zum Formular tun wir das natürlich nicht! Wir weisen den Kontext vielmehr dem erzeugten Kontroll-Objekt "K" selbst zu. Sollte Output in anderen, von ihm nicht kontrollierten HTML-Objekte erzeugt werden müssen, können die notwendigen Daten aus der Ajax-Antwort ja immer noch in definierter Weise an andere Kontroll-Objekte weitergereicht werden.

Wenn das Objekt "K" global definiert ist, kann man also schreiben:

$.ajaxSetup({ 
	..., 
	context: K, 
	..... 
})     

Aufgrund des Einsatzes von $.proxy() geht es aber deutlich eleganter:

$.ajaxSetup({ 
	..., 
	context: this, 
	..... 
})     

So, nun haben wir gesehen, wie wir Formulare und zugehörige Ajax-Transaktionen mit $.proxy() und spezifischen Methoden von Kontroll-Objekten handhaben können. Im nächsten Beitrag dieser Serie

Fallen beim Statuscheck lang laufender PHP-Jobs mit Ajax – IV

befasse ich mich mit dem Timing des periodischen Aufrufs des CHECKER-Prozesses.

Links

http://stackoverflow.com/questions/3863536/how-to-pass-context-in-jquery-ajax-success-callback-function
http://stackoverflow.com/questions/6394812/this-inside-of-ajax-success-not-working
http://stackoverflow.com/questions/11027276/jquery-this-not-accessible-after-ajax-post

Eclipse Luna, SVN – problem with older Subversion 1.6 repositories !

In Eclipse I have to handle a variety of PHP projects (with additional natures) which are interconnected in different ways. E.g. my present project may for convenience have links to folders of another project in another Eclipse workspace. Such links exist in my case for example to files containing very basic classes of some of my own frameworks. In some projects I combine the present project work with upgrading the basic framework classes in parallel to a new level of capabilities. In such a situation I want to directly change the basic classes for all other projects that use them and test compatibility aspects.

The version control of almost all interconnected projects is done by Eclipse Subversive plug-ins from Polarion. The link situation on the folder/file side of my projects leads to so called external SVN relations on the SVN side of my interconnected projects. These relations are generated automatically by the Subversive plug-ins for Eclipse.

In some cases I may even have different SVN locations - some local, some on a dedicated SVN servers in the local network or on the Internet. Unfortunately, not all of my SVN repositories are of the same Subversion version - which was no problem as long as different connectors were available in Eclipse and the repositories were generated to be backward compatible. Some of my SVN repositories are relatively old ones - i.e. of SVN version 1.6. Mostly associated with framework classes I continuously have worked on for years.

From KDE's SVN client "kdesvn" I am very familiar with the problem that e.g. a client for a Subversion version 1.8 is not able to deal with SVN repositories that were generated with an older version of Subversion (e.g. Subversion version 1.6). An error message including as "... working copy is too old .... " is a typical indication of this situation. In this case it does not help that you may have set up the repository with backward compatibility (e.g. to SVN version below 1.6). You have to update your repository by using the command

svnadmin upgrade

inside the base directory of the repository. Which may be delicate - if you did not set up any backward compatibility your Eclipse plug-in may no longer be able to handle the repository.

So, before updating SVN in your Linux OS and upgrading your SVN repositories you should always check carefully, whether the Subversive SVN plug-ins in Eclipse will be able to handle the repository afterwards. Actually, already this type of problem indicates that a strategy to maintain SVN repository versions consistently should be followed. However, I am a bit lazy. And as long everything worked smoothly I had no reason to upgrade my SVN repositories to one and the same Subversion version.

Some days ago - due to some irritating errors of syntax highlighting in the Kepler JSDT module - I installed Eclipse LUNA (i.e. version 4.4.2.) instead of KEPLER. The JSDT highlighting error went away. However, I ran into deep trouble with the subversion plug-ins.

Actually, my Eclipse upgrade lead to a complete mess because of my laziness in the past to keep the different Subversion versions of SVN repositories on the same level. The following discussion may help others to recover from such a situation and reconsider a strategy to keep the Subversion versions of one's SVN repositories consistently aligned.

Eclipse Subversive plug-ins for LUNA provide SVN Connector Kits for Subversion versions 1.7 and 1.8, only

Recently, I had started with a new PHP project in Eclipse KEPLER depending on a set of classes of one of my frameworks. So, I thought this project would be a good choice to get some experience with Luna. Without much thinking I had installed several Eclipse plugins I typically use into LUNA as PDT, WST, Mylyn, JSDT Jquery , etc. and of course Subversion and Polarion Subversive SVN Connector Kits. So, I felt well prepared to open the my Kepler workspace with Luna.

The first thing you may stumble across when using Luna first time is that you should make a copy of your original PHP projects in the Kepler workspace directory. Luna will upgrade your Kepler project in a way that is not backward compatible to older Eclipse versions. Ok, that done, I wanted to share my upgraded project by using a SVN repository. I chose an existing local SVN location - based on the "file" sub protocol of SVN. Then I started to check in the directories and files of my project. The check in process seemed to work well in the beginning. But then I was bombarded with error messages. Besides other things the messages told me that subversion stumbled across an "unsupported working copy".

Ok, I checked the installed connector versions of the SVN Polarion plug-ins and did additional research on the Internet - the connectors were for Subversion version 1.7 and 1.8, only! Others are not available for Luna! As I did not remember exactly the version of my local repository (probably 1.6) I disconnected the PHP project from SVN with allowing to delete all SVN information from disk.

Making a new repository for your present project may not help

Next thought: Well, lets make a new SVN repository then - of SVN version 1.8. I combined this step with setting up a local lightweight SVN server - because somebody had told me that the performance of the core SVN protocol would be much better than the "file" protocol. After the intermezzo of the server setup I manually created a new SVN database on my local SVN server (i.e. my workstation). As Subversion on the Linux workstation is of version 1.8 -I had no doubts that LUNA now should be able to work together with this SVN database. From Eclipse I shared my previously disconnected project by using the new SVN location with the generic "svn//" protocol. And started to check in again. Which started well and then ran into errors again ... 🙁

To analyze what happened I set up a fresh independent project and filled it with some directories and files and tested SVN transactions with my version 1.8 SV repository. That worked perfectly! So, something obviously was wrong with my other more complicated project. A closer analysis showed what the reader may already have guessed:

The errors occurred during the check in process when directories were reached that were linked to my basic long term projects with their old repositories. Which on the SVN side are located in an "external" repository. So, my conclusion was:

One must first remedy the outdated working copy situation for the basic projects which your present project may depend on by links. Their (external) SVN repositories have to be upgraded first.

In complicated dependency situations you may run into more problems

Although the above conclusion is correct it may not directly lead to success. By walking through my projects and trying to get them working again with SVN under LUNA I stumbled across several situations which stressed my nerves. Among others there are 2 stupid things that may lead you to dead end situations and prevent a recovery from them with the Eclipse subversion tools:

  • After a trial to check in files into (too old) repositories there may be open outgoing SVN transactions left which cannot be resolved due to the impossibility to handle old SVN working copies of connected projects with linked folders. You are forced to completely disconnect your present project from the SVN repository with a deletion of all SVN information from disk (i.e. ".svn" files in the folders and sub folders of your Eclipse project)
  • In complicated link situations you may find the following: There may be a mixture of ".svn" files in a project which describe the diverse repositories of linked folders of other projects on different version levels. I found that with the present plug-ins of LUNA this may lead to unrevoverable SVN situations and even repository corruption.
  • Despite disconnecting projects and allowing for a deletion of SVN information some ".svn" files may remain at unexpected locations. This may depend of what kind of SVN trouble and corruption you had and what you tried to remedy the situation. The remaining ".svn" files may include old SVN version information - leading to trouble again when you try to connect to an SVN repository next time.

Eventually, I gave up and really tried to build my previous Kepler projects from scratch again under Luna. Including the SVN aspects.

Steps to recover and get a working LUNA - SVN implementation again

The following sequence of steps worked in my case:

  • First, make copies of your projects AND the associated SVN repositories. You never know ....
  • Check in all files and directories in all of your projects with the old Eclipse version (in my case Kepler) to get the latest versions into the existing SVN repository.
  • If one of the previous SVN repositories used under Kepler is corrupted (according to SVN messages in Eclipse) - do not use it any longer in the steps below. Build a new repository location instead.
  • Disconnect all existing Kepler projects from SVN via the context menu of the project by using the menu points "Team >> Disconnect" and allow for a deletion of the SVN information from disk.
  • Important point: Remove all remaining ".svn" files inside your Eclipse projects which may have remained there by using the command
    "find . -name ".svn" -exec rm -fR {} \;"
  • Go to the SVN repositories that still worked in Kepler (wherever they may reside) and upgrade by "svnadmin upgrade" consistently to the present SVN version - in my case 1.8.
  • Systematically restore your basic projects - i.e. the ones on which other projects may depend - from scratch by using the upgraded repositories. Or share these projects again with reference to the upgraded repository from which you then update the files to the latest repository version.
  • If one of the last steps is not possible due to repository corruption you build up new projects in Eclipse and fill them with the files from your (Kepler) project backups. Then share them by using a new repository location of your choice. Check that all SVN transactions work as expected for your basic projects.
  • Only then rebuild your more complicated projects depending which depend on your basic projects and share them again with the upgraded SVN repositories or newly generated ones.

And in the future:

Follow a systematic approach to upgrade your SVN repositories consistently to Subversion versions your Eclipse installation AND your other tools of your Linux desktop can handle.

Finally: Have fun with Eclipse LUNA and PHP or JS/Ajax projects. It feels great so far ....