Importing large csv files with PHP into a MySQL MyISAM table

In one of my present projects there is a requirement to upload tens of millions of records from several csv files into a database. The files are uploaded to the server with the help of Ajax controlled browser/server-transactions. On the server the file contents should be checked and then transferred into an appropriate database table. In my case a MyISAM table.

This is the first time that I had to deal with such big record numbers on a MySQL database. On an Oracle databases I worked with around 30 million record tables some years ago – but those data came from other sources and were supplied more continuously than in form of a one step csv file import.

Although the data structure and table structures for the import are flat the pure amount of data makes it necessary to think a little about the way of transferring them from file lines to records of a MySQL database table with the help of a PHP server program. Below, I really do not present fundamentally new insights – a MySQL expert will not find anything new. But maybe others who for the first time stumble into a big data import problem may find some of the hints below useful.

A simple, but time consuming standard approach for a csv import with PHP

In the past when I had to import data from files I often used a sequence of steps comprising

  • the opening of the file for reading,
  • the use of fgetcsv() to read in one line,
  • the usage my own and problem specific perform check methods of the identified fields,
  • the processing of a sequence of one INSERT SQL command for each of the file lines.

Reasons for such an approach were: You have complete control about the data and the import process. You may check the contents of each file line thoroughly already before inserting it. You may send intermediate positive or negative messages to the client via Ajax, etc..

However, after some thorough tests I found the following:

A data import based on the approach described above is really time consuming. To give you an impression: The typical import time I found for my hardware and virtualization conditions was

108 seconds for

  • a standard MySQL 5 database on a small virtualized KVM server instance with one CPU core and 1GB RAM (256 MB reserved for MySQL; host and KVM guest both run Opensuse 13.1),
  • a file with 1 million lines (each containing 6 fields – 1 field for a primary integer key / 3 integer fields together forming another unique key / 1 quantity value field )
  • one INSERT for each line with parallel index building for each of the three integer columns.

That really meant trouble for my rather impatient customer. The extrapolated time for the import of similar contents of 8 additional files and a potential factor of 50 with respect to the final number of file lines/records made him nervous. And he was also aware of the fact that due to index building the total required import time may grow faster than linear with record number.

It may be hard to say good bye to good old habits. But when the upload and import time counts more than control one should look for other import methods. After some tests I found the following approaches which can lead to a significant improvement of import times:

Improvement 1: Work with bunches of records per INSERT

What is the first guess for time consuming operations? It is the overhead the preparation of an SQL statement takes. If such a statement is true one should reduce the amount
of INSERT statements. And really: After some trials I found that you may accelerate things significantly by avoiding to use one INSERT for each record/file line. Working with bunches of records bundled to be saved in just one INSERT statement really helps to save time. This seems in a way to be natural and understandable; see also the following article:
http://dev.mysql.com/doc/refman/5.1/en/insert-speed.html

So, one thing you could try is to put the result of several fgetcsv() calls into one INSERT statement. In a next step you would try to find an optimum number of records per INSERT by systematic testing. This may already give you a substantial acceleration factor.

Improvement 2: Use “LOAD DATA INFILE … “

If you need an acceleration factor above 10 for loading csv data, I strongly suggest that you use a special feature of the MySQL engine and its related SQL extension in your PHP loader program. See:

http://dev.mysql.com/doc/refman/5.6/en/extensions-to-ansi.html
http://dev.mysql.com/doc/refman/5.6/en/load-data.html

The article promises an acceleration factor up to 20. I can confirm a factor of 18:

In a case of a 1 million record csv file (each line for 6 fields, 4 indices generated on 4 columns) I got the following acceleration:

  • Single INSERTS: 108 seconds
  • LOAD DATA : 6 seconds


(Both loading processes were done with some defined and active indices that were built up during the filling of the table. Therefore, forget about the absolute numbers – without any indices you may load 1 million records in a time around or below one second.)

A relative factor of 18 is impressive. However, the usage of an optimized loop like program taking and saving (probably bunches of record data) also may have disadvantages. One question is: How are data with wrong field numbers or wrong data type formats handled? One can imagine that there might be a policy that the program cannot stop because of one defect in one line. The error handling would depend on a reasonable summary of which file lines/records were defect. We come back to this point below.

Improvement 3: Separate the index generation from the data filling process

Even if you use “LOAD DATA…” there is still room for more improvement if you separate the index generation from the filling of the table. This is described in detail in the documentation of the “LOAD DATA” statement. See:
http://dev.mysql.com/doc/refman/5.1/de/load-data.html

I have not tried or investigated this yet; so, I can neither give a general impression of this recipe nor an acceleration factor. But I consider it worth trying when I find the time. Maybe after the first 10 million record import 🙂 .

Addendum, 19.09.2014:

I can now confirm that separating the index creation from the loading process with “LOAD DATA INFILE” may give you another significant factor. This is especially true when unique indices are built in parallel to the data loading. Already omitting a simple auto-incremented unique index over one column may give you a factor of around 2. See a forthcoming article for more details.

Improvement 4: Combine quantities which have the same dependencies on keys in one file and insert them into one and
the same table

In my case we have to import 2 groups of data for N different quantities with the same key dependency – in our case 2 groups with 4 different quantities, each. Due to the requirements of using these data independently in different calculation steps, it seemed to be wise in a first approach to load all these quantities into different tables despite the very same key structure and key dependency (in our case on integer triples describing a 3-dimensional finite numerical grid structure).

However, for the sake of performance one should really reconsider and challenge such a separate table/file strategy and do some performance tests for a combined table/file strategy where all quantities with the same keys reside in one and the same file/table. Reasons:

  • To deliver data for one and the same key combination in one combined file is also a matter of transfer efficiency to the server as the total amount of data to be transferred via the network/Internet gets less.
  • It is not necessary to change your programming statements for calculations with data taken from separate tables if you work with appropriate views of the combined table that “simulate” such separate tables for you. In my case views of the type “MERGE” were appropriate. I could not see any significant performance reductions when using views instead of tables.
  • Regarding csv import the most important effect is that you instead of N times importing a file of the same structure you only import data from one file. That reduces the amount of INSERTs by a factor of N. The question remains how that relates to the fact the each INSERT writes more columns of the import table in the database.

The last point is really interesting. In our test case which may be a bit special we got the following result when using the “LOAD DATA INFILE” statement for loading directly from a file with four quantities combined instead of just one:

For a million records the import time did not change very much in comparison to the time of 6 seconds required for importing a file for just one quantity – actually the difference for loading our file for 4 quantities was below 10%. That means that in our case we gained an additional factor of almost 4 for shortening the total required import time.

So, by using “LOAD DATA INFILE” AND combining data quantities with the same key depency in just one file I could reduce the total loading time for 4 files with each a million records by a factor of around 70.

A hint regarding error handling for LOAD DATA

In our case we still use the old “mysql_query” interface and not “mysqli”. Do not ask me for the reasons – most of it is laziness. From a professional point of view I advice against the usage of the old interface – especially as it may disappear with coming PHP versions. Nevertheless, if you use the old interface you will probably use statements like ( I simplify )

Wrong approach for error control

	$sql_load = 	" LOAD DATA INFILE '" . $this->imp_file . "'" . 
			" INTO TABLE " . $this->tbl_name . 
			" FIELDS TERMINATED BY " . "';'" . 
			// " LINES TERMINTED BY " . "\r\n" . 
			" IGNORE 1 LINES"; 
	
	if (!mysql_query($sql_load, $this->DB->db) === false) {
		// Steps to control errors 
	}
 

(You may need the commented line in case of files generated on Windows).

Do not always expect something reasonable from this error detection approach! My impression is that it works reliably only in case of SQL syntax errors. But not in case of defect lines of the csv file. Instead you may have to explicitly look at the output of mysql_error():

Better approach

	$sql_load = 	" LOAD DATA INFILE '" . $this->imp_file . "'" . 
			....
	}

	if ( mysql_error() != '' ) {
		// Steps to control errors 
	}
 

Or you may even use “Show Warnings“. An explicit look at the existence of errors or warnings is also helpful if the whole import is part of an Ajax transaction and you want send appropriate error messages to the client in case of detected errors during the “LOAD DATA ..” process. This leads us to the next point.

What about the requested checks of the data ?

In our approach centered around “LOAD DATA INFILE ..” we have lost track of another requirement, namely that the file data to be imported should first be checked for errors. Well, in this early stage of experimenting I have two – probably not satisfactory – comments:

Many tests can be made before or after the “LOAD DATA” controlled database import. This holds e.g. for consistency checks like number comparisons. For such points you can perform pretty fast SELECT COUNT() statements on the filled database table. In our case e.g:
For all network nodes (described by tupels of integer key numbers): Are there equal numbers of records in the third key dimension (in our case time) for all nodes?

Other checks may, however, directly concern the quality and type consistency of the input data. So, what happens, if “LOAD DATA …” stumbles across a line with missing fields or a wrong type of data (compared to the database table column definitions) ?
Tn the documentation http://dev.mysql.com/doc/refman/5.1/en/load-data.html it is described what e.g. happens if a line has insufficient fields or too many fields. Automatic type conversions are probably tried, when the type of a field value does not fit.

I have not yet tested what really and exactly happens in such error cases. The official documentation is not at all clear to me regarding this point. It seems to be reasonable to assume that an automatic routine for the import of csv data lines would try anything to get a line into the database table. So, I would expect a standard procedure to compensate missing or too many fields and trying some automatic type conversions before a line is skipped. I would not relly expect that a faulty line will lead to a direct stop of the import process.

In addition I would expect something like a bad record log. Unfortunately, there are indications that such a log is not generated. See:
http://chrisjohnson.blogsite.org/php-and-mysql-data-import-performance/

The documentation http://dev.mysql.com/doc/refman/5.1/en/load-data.html says:

When the LOAD DATA INFILE statement finishes, it returns an information string in the following format:
 
Records: 1 Deleted: 0 Skipped: 0 Warnings: 0

So at least this information could in principle be evaluated. In addition the documentation says:

Warnings occur under the same circumstances as when values are inserted using the INSERT statement (see Section 13.2.5, “INSERT Syntax”), except that LOAD DATA INFILE also generates warnings when there are too few or too many fields in the input row.
 
You can use SHOW WARNINGS to get a list of the first max_error_count warnings as information about what went wrong. See Section 13.7.5.42, “SHOW WARNINGS Syntax”.

Show Warnings” actually also informs about errors. See: http://dev.mysql.com/doc/refman/5.1/en/show-
warnings.html

However, the stupid thing is that you still may not get the relevant information (What record was wrong? What field had wrong values ? ) from this when the INSERT eventually worked. What we really would need is something like a bad record log.

So, right now it seems to me that the impressive reductions of load times when using “LOAD DATA INFILE …” also does have its drawbacks and disadvantages.

Therefore, I am thinking about precursor programs that would open the file and analyze the contents of each line before “LOAD DATA” is afterwards used on the whole file. The overhead for such precursor programs must of course be evaluated by tests. When I have gotten more experience with “LOAD DATA” I will come back to this point in this blog.

Conclusion

Using an SQL statement with “LOAD DATA INFILE …” will reduce the time to import flat CSV data into a corresponding database table significantly by factors of around 18 in comparison to standard methods based on INSERTS per file line. The official MySQL documentation promises a bit more – but the factor measured for a real world example is still impressive. A disadvantage of the “LOAD DATA ..” statement from my point of view is that the handling of errors in a PHP program remains unclear.

Nevertheless, for the time being I still would recommend the usage of “LOAD DATA INFILE …” in PHP programs for uploading csv files – if the performance of a csv import is a dominant objective in your project.

New series of articles on using my ixCMF framework to build a medium sized CMS

The last 2 weeks I found some time to start a new ixCMF project:

I want to use the results of different PHP web application projects which I did for customers and which were based on my ixCMF meta framework for web applications to build my own CMS for small and medium sized web sites.

I describe my considerations and successive steps how to develop such a special and extensive ixCMF web application by a series of blog articles in my ixCMF blog.

I think that the topic of designing and developing a CMS might be interesting for some people besides myself – even if you do not know anything about the ixCMF framework.

So, if you are interested in CMS designing and hierarchical master detail data structures have a look at
The ixCMF CMS project – I – some objectives
and its present subsequent articles

The ixCMF CMS project – II – setting up the IDEs
The ixCMF CMS project – III – the object hierarchy
The ixCMF CMS project – IV – objects and their web page counterpart
The ixCMF CMS project – V – main and sub menus

OR

the category
http://ixcmf.anracom.com/category/the-ixcmf-cms-project/
in my ixCMF-blog.

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://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);

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.