PHPAsyncIBMi

L’exécution de traitements en mode asynchrone est une problématique que les développeurs IBMi connaissent bien, avec l’utilisation des bonnes vieilles commandes SBMJOB (pour le lancement de travaux batchs) et WRKJOBSCDE (pour l’accès au planificateur de travaux).

Dans le contexte d’une application PHP s’exécutant dans un environnement IBM i, il est tout à fait possible d’utiliser ces même outils de soumission de travaux. Je vous indique la méthode de base ci-dessous, et ensuite je vous présenterai une méthode alternative, que je trouve beaucoup plus pratique, à savoir ZendJobQueue.

Si je décide d’exécuter des scripts PHP en passant par la commande IBM i SBMJOB, ou la commande ADDJOBSCDE (qui est liée au planificateur de travaux), je dois passer par l’API QSH, et lancer l’interpréteur PHP en mode CLI.

Attention : dans une précédente version de cet article, j’avais évoqué l’utilisation de l’API QP2SHELL, mais suite à des difficultés d’utilisation, je me suis rabattu sur l’API QSH, qui fonctionne très bien, et qui offre une syntaxe finalement plus simple que QP2SHELL.
Si vous souhaitez savoir quel genre de difficulté vous pouvez rencontrer avec l’API QP2SHELL, je vous renvoie vers un post dans lequel Scott Klement préconise l’utilisation de QSH.

Dans un environnement Zend Server récent et configuré de manière standard, l’emplacement de l’interpréteur PHP-CLI est le suivant :

‘/usr/local/zendsvr/bin/php-cli’

Pour lancer un script PHP en mode CLI, et surtout en environnement IBM i, il faut respecter les étapes suivantes :

– Etape 1 : appel du script PHP en mode CLI via QSH (avec 2 paramètres x1 et x2, pour l’exemple)

QSH CMD(‘/usr/local/zendsvr/bin/php-cli /www/zendsvr/htdocs/monappli/monscript.php x1 x2’)

– Etape 2 : commande QSH encapsulée dans un SBMJOB IBM i (pour lancement du traitement en mode « batch »)

SBMJOB CMD(QSH CMD(‘/usr/local/zendsvr/bin/php-cli /www/zendsvr/htdocs/monappli/monscript.php x1 x2’))
JOB(nom_du_job) JOBQ(ma_jobqeue)

– Etape 3 : encapsulation de la commande préparée à l’étape 2 dans l’API QCMDEXC pour une excécution via SQL DB2

CALL QCMDEXC (‘SBMJOB CMD(QSH CMD( »/usr/local/zendsvr/bin/php-cli /www/zendsvr/htdocs/monappli/monscript.php x1 x2 »)) JOB(TRAD_ja) JOBQ(QGPL/QS36EVOKE)’, 143)

Vous noterez que, si vous souhaitez lancer votre script PHP à partir d’un programme IBM i de type CLP, RPG ou autre… l’étape 3 n’est pas nécessaire, vous pouvez vous arrêter à l’étape 2. L’étape 3 vous permet d’obtenir une commande que vous pouvez lancer via un script PHP, ou tout type d’application web, du moment qu’elle est en mesure de se connecter à DB2 pour exécuter des requêtes SQL.

La commande préparée lors de la troisième étape a une particularité que vous aurez peut être remarquée : les apostrophes situées à l’intérieur de …QSH CMD( )… sont doublés. C’est indispensable pour que l’interpréteur SQL DB2 exécute correctement la commande. Car ce que vous obtenez à l’étape 3, c’est finalement une requête SQL que vous pouvez exécuter soit via PDO, soit via db2_connect(). DB2 va se contenter d’appeler l’API QCMDEXC, et c’est cette dernière qui fera le gros du travail. Attention : la longueur de 143 caractères ne doit pas tenir compte du « doublage » des apostrophes, il faut donc la calculer avant de doubler les apostrophes.

Pour vous simplifier la vie, je vous propose 2 petites fonctions que j’ai créées dans le cadre d’un projet sur lequel j’ai travaillé récemment :


/**
* Fonction destinée à préparer la commande d'exécution d'un script PHP
*/
function shellCmd2($cmd, $sbmjob = true, $job_name = 'PHP_CLI', $job_queue = 'QGPL/QBATCH') {
// chemin d'accès complet vers le script PHP à exécuter (avec ou sans paramètres)
$cmd = trim($cmd);
// QSH a été préféré à QP2SHELL car plus facile à utiliser notamment
// avec des scripts nécessitant le passage de paramètres
$cmd = "QSH CMD('/usr/local/zendsvr/bin/php-cli {$cmd}')" ;

// Commande encapsulée dans un SBMJOB IBM i
if ($sbmjob) {
$cmd = "SBMJOB CMD({$cmd}) JOB({$job_name}) JOBQ({$job_queue})";
}
return $cmd;
}

/**
* Fonction destinée à encapsuler une commande système IBM i, selon les
* règles syntaxiques définies par l'API QCMDEXC, de manière à pouvoir
* l'exécuter comme une simple requête SQL.
*/
function db2SysCmd($cmd) {
$cmd = trim($cmd);
// calcul de la longueur de la commande à exécuter
$cmd_length = strlen($cmd);
// on double les apostrophes (sans recalculer la longueur de la commande)
$cmd = str_replace("'", "''", $cmd) ;
// génération de la commande DB2 complète
$cmd2 = "CALL QCMDEXC ('{$cmd}', {$cmd_length})";
return $cmd2;
}

Avec l’aide de ces 2 fonctions, si le script PHP que je souhaite lancer est le suivant :

/www/zendsvr/htdocs/monappli/monscript.php x1 x2

Résultat renvoyé par la fonction ShellCmd2 :

SBMJOB CMD(QSH CMD(‘/usr/local/zendsvr/bin/php-cli /www/zendsvr/htdocs/monappli/monscript.php x1 x2’)) JOB(PHP_CLI) JOBQ(QGPL/QBATCH)

Résultat renvoyé par la fonction db2SysCmd :

CALL QCMDEXC ('SBMJOB CMD(QSH CMD(''/usr/local/zendsvr/bin/php-cli /www/zendsvr/htdocs/monappli/monscript.php x1 x2'')) JOB(PHP_CLI) JOBQ(QGPL/QBATCH)', 133)

 

Points à noter :

– L’utilisation du XMLToolkit pour lancer le script PHP peut constituer une alternative à la solution que j’ai présentée ci-dessus, mais je préfère ma solution.

– On pourrait aussi faire appel à une procédure stockée de type externe encapsulant un programme CL, qui effectuerait le SBMJOB souhaité. Cette procédure stockée serait appelée via une simple requête SQL de type « CALL procédure ». Cette requête SQL pourrait être exécutée via PDO si votre environnement d’exécution Zend Server se trouve sur un serveur Windows et attaque une base DB2 pour IBM i via le driver « Iseries Access ODBC Driver », ou via db2_connect si votre environnement d’exécution Zend Server est un serveur IBM i.

Toutes les solutions de lancement de travaux présentées ci-dessus fonctionnent, mais elles présentent certaines contraintes. Car si je souhaite par la suite déployer mon application sur un serveur autre qu’IBM i, je devrai réviser les portions de code relatives à l’exécution de traitements asynchrones. En environnement Linux, je passerai très certainement par « cron », en environnement Windows je passera très certainement par le planificateur de travaux de cet OS. Bref, ce n’est pas encore la Tour de Babel, mais ça commence à faire désordre.

A noter que des extensions PHP dédiées au traitement de processus existent. Elles sont présentées dans la page suivante :
Extensions PHP pour le contrôle de processus

L’une des extensions les plus connues, et utilisées, est PCNTL. Mais si ces solutions peuvent être intéressantes dans certains cas, leur configuration passe très souvent par la recompilation du noyau PHP, et elles ne sont pas opérationnelles dans tous les environnements d’exécution possibles (Windows, Linux, IBMi). Des alternatives open source existent également en dehors des extensions PHP, je pense notamment aux projets RabbitMQ et Gearman, mais ces projets sont conçus à la base pour des environnements Linux et/ou Windows, et il n’est pas du tout évident qu’ils puissent être utilisés en environnement IBM i.

ZendJobQueue et ses avantages

J’en viens donc à ZendJobQueue. Cette fonctionnalité est intégrée en standard dans Zend Server, mais attention, elle n’est opérationnelle que dans la version complète et payante de Zend Server. Elle n’est pas activée dans la version CE (Community Edition) de Zend Server.

ZendJobQueue est une extension propriétaire de Zend que vous allez pouvoir utiliser dans vos scripts PHP au travers d’une classe qui porte le même nom. ZendJobQueue contient toutes les API PHP nécessaires pour la gestion de files d’attente.

Comme ZendJobQueue est intégrée dans toutes les déclinaisons de Zend Server, que ce soit pour Linux, Windows ou IBM i, vous allez pouvoir exploiter cette fonctionnalité de manière homogène, quel que soit l’environnement d’exécution de votre (vos) application(s) PHP. C’est un atout majeur sur lequel je crois utile d’insister. Et en plus c’est facile à utiliser, comme vous allez le voir dans les exemples qui suivent.

Connexion à la JQ (Job Queue) du serveur courant :
$queue = new ZendJobQueue();

Connexion à la JQ d’un autre serveur :
$queue = new ZendJobQueue(‘tcp :1.2.3.4 :5678’);

Les travaux sont créés en utilisant la méthode createHttpJob() :
$queue = new ZendJobQueue();
$jobId = $queue->createHttpJob(‘http://backend.local/jobs/somejob.php’) ;

On peut aussi créer le travail en utilisant le nom du serveur hôte courant :
$queue = new ZendJobQueue();
$joblink = ‘http://’. $_SERVER[‘HTTP_HOST’] . ‘/jobs/somejob.php’ ;
$jobId = $queue->createHttpJob($joblink) ;

A noter : La méthode createHttpJob() renvoie systématiquement l’Id du job qu’elle vient de créer, aussi il peut se révéler intéressant de conserver cet ID. Ainsi, si à l’intérieur de votre traitement, vous estimez que certaines conditions ne sont pas remplies et qu’il est nécessaire de retirer de la file d’attente un traitement lancé quelques minutes plus tôt, vous pouvez le faire très simplement en réexploitant cet ID de la façon suivante :
$queue = new ZendJobQueue();
$statut = $queue->removeJob($jobID);

La liste des méthodes proposées par l’objet ZendJobQueue est assez conséquente (l’auto-complétion du Zend Studio fournit une aide détaillée pour chacune de ces méthodes) :

– deleteSchedulingRule($rule_id);
– getApplications();
– getConfig();
– getCurrentJobId();
– getCurrentJobParams();
– getDependentJobs($job_id);
– getJobInfo($job_id);
– getJobsList($query, $total);
– getJobStatus($job_id);
– getSchedulingRule($rule_id);
– getSchedulingRules();
– getStatistics();
– isJobQueueDaemonRunning();
– isSuspended();
– reloadConfig();
– removeJob($job_id);
– restartJob($job_id);
– resumeQueue();
– resumeSchedulingRule($rule_id);
– setCurrentJobStatus($completion, $msg);
– suspendQueue();
– suspendSchedulingRule($rule_id);
– updateSchedulingRule($rule_id, $script, $vars, $options);

Lors de la création d’un travail dans la file d’attente de travaux, via la méthode createHttpjob, on peut passer des paramètres au script PHP appelé, de 2 manières différentes :

1 – Passage de paramètres directement dans l’url du script appelé :
$joblink = ‘http://’. $_SERVER[‘HTTP_HOST’] . ‘/jobs/somejob.php?codcli=1002&codart=12344’ ;
$jobId = $queue->createHttpJob($joblink) ;

2 – Passage de paramètres par l’intermédiaire d’un tableau
La méthode createHttpjob est en mesure de recevoir un second paramètre, facultatif et de type tableau, qui contiendra les différentes paramètres « métier » attendus par le script PHP appelé.
$params = array(‘param1’=>array(1, 2, 3, 4), ‘param2’=>’toto’) ;
$jobId = $queue->createHttpJob($joblink, array($params)) ;

Notez que la structure du tableau $params peut être beaucoup plus complexe que celle que j’ai indiquée dans l’exemple ci-dessus.

Quand vous exécutez un script PHP en mode CLI, les paramètres transmis en entrée de ce script sont automatiquement réceptionnés par l’interpréteur PHP dans 2 variables globales qui sont :
– $argc : variable globale contenant le nombre de paramètres reçus par le script en cours
– $argv : variable globale de type tableau (postes numérotés de 0 à x) contenant la liste des x paramètres reçus par le script en cours

Pour de plus amples renseignements sur l’utilisation de ces paramètres, prière de se reporter à la documentation officielle :
Documentation sur l’utilisation de la variable $argv

Avec ZendJobQueue, la réception des paramètres à l’intérieur du script en cours d’exécution se fait différemment, comme le montre l’exemple ci-dessous :
$job = new ZendJobQueue() ;
$params = $job->getCurrentJobParams();

A l’arrivée, la variable $params contient un tableau contenant les différent paramètres transmis au script, qu’ils aient été transmis via un tableau ou directement dans l’URL d’appel.

A noter que l’on peut aussi sur le même principe récupérer l’identifiant du Job en cours d’exécution avec la méthode getCurrentJobId() :
$job = new ZendJobQueue() ;
$params = $job->getCurrentJobId();

La méthode CreateHttpjob accepte un 3ème paramètre optionnel, de type tableau associatif, dont les clés peuvent contenir une ou plusieurs des valeurs optionnelles ci-dessous :

  • name : pour affecter un nom précis au travail lancé
  • priority : 4 niveaux de priorité peuvent être utilisés, au travers des constantes PRIORITY_LOW, PRIORITY_NORMAL, PRIORITY_HIGH, and PRIORITY_URGENT
  • persistent : valeur booléenne permettant de préciser si le travail lancé doit être conservé de manière permanente dans le scheduler (pas testé)
  • predecessor : valeur de type integer correspondant au numéro de Job du travail précédent (pas testé)
  • http_headers : entêtes HTTP additionnels (pas testé)
  • schedule : planification de travaux à la manière de « cron »
  • schedule_time : date et heure d’exécution souhaitée pour le travail soumis

Je n’ai pas testé certains paramètres, faute de temps, j’ai précisé lesquels dans la liste ci-dessus. Je ne donnerai donc pas d’exemple d’utilisation pour ces paramètres particuliers.

La planification de travaux avec ZendJobQueue

Je vais en revanche m’intéresser aux possibilités de planification offertes par les paramètres « schedule » et « schedule_time » :

$params = array(« param1 » => 10, « param2 » => « PHP is cool »);

// Soumission d’un process à exécuter dans 1 heure
$options = array(« schedule_time » => date(« Y-m-d H:i:s », strtotime(« +1 hour »)));
$jobId = $queue->createHttpJob(« http://example.com/jobs/somejob.php », $params, $options);

// Processus soumis tous les jours à 1h05 du matin (syntaxe spécifique à Cron)
$options = array(« schedule » => « 5 1 */2 * * »);
$queue->createHttpJob(« http://example.com/jobs/somejob.php », $params, $options);

Pour de plus amples renseignements sur la syntaxe spécifique à Cron, je vous invite à consulter la page de présentation de Cron sur Wikipédia.
Je vous encourage également à effectuer des recherches sur internet, car vous y trouverez de nombreux tutoriaux consacrés à l’utilisation de Cron.

La gestion des erreurs

Pour « monitorer » les erreurs, vous utiliserez le bon vieux système du try catch, comme dans l’exemple ci-dessous :
try {
$job = new ZendJobQueue() ;
$jobid = $job->getCurrentJobId() ;
$params = $job->getCurrentJobParams();
// insérez votre code métier ici
if ($no_problem) {
$job->setCurrentJobStatus(ZendJobQueue::STATUS_OK);
} else {
$job->setCurrentJobStatus(ZendJobQueue::STATUS_LOGICALLY_FAILED,
‘Anomalie durant le processus’) ;
}
} catch (Exception $e) {
ZendJobQueue::setCurrentJobStatus(ZendJobQueue::STATUS_FAILED, $e->getMessage());
}

La variable $no_problem est un booléen sensé avoir été mis à « true » ou « false » à l’intérieur du « code métier », c’est bien sûr un exemple, vous n’êtes pas obligé de faire comme cela, mais l’important est de bien positionner le « current job status » à la fin du traitement, pour pouvoir effectuer un suivi efficace des travaux soumis.

Vous noterez qu’il existe plusieurs types de statuts, j’en ai utilisé ici 3 dont les noms sont – je pense – assez parlants (STATUS_OK, STATUS_LOGICALLY_FAILED, STATUS_FAILED), je donne la liste complète ci-dessous :

  • STATUS_COMPLETED ;
  • STATUS_FAILED;
  • STATUS_LOGICALLY_FAILED;
  • STATUS_OK;
  • STATUS_PENDING;
  • STATUS_REMOVED;
  • STATUS_RUNNING;
  • STATUS_SCHEDULED;
  • STATUS_SUSPENDED;
  • STATUS_TIMEOUT;
  • STATUS_WAITING_PREDECESSOR;

Nous avons parlé de l’utilisation de ZendJobQueue au niveau du code, mais il est intéressant de noter que Zend Server fournit une interface graphique permettant de consulter la liste des travaux, en appliquant si on le souhaite des filtres (pour par exemple n’afficher que les travaux en attente d’exécution, ou que les travaux en erreur). L’interface graphique permet également de relancer certains travaux si on le souhaite.

La question des performances

Je n’ai pas eu le temps de faire des benchmarks pour comparer les performances d’un script s’exécutant en mode CLI et via ZendJobQueue, mais j’ai trouvé des informations à ce sujet dans un webinar Zend réalisé par Shahar Evron, consacré spécifiquement à ZendJobQueue. D’après les informations indiquées par Shahar Evron dans son webinar, il semble que le lancement de script via ZendJobQueue procure des performances très supérieures à celles du mode CLI. Je pense que c’est un paramètre important à prendre en considération dans le choix de l’un ou l’autre mode.

En plus du webinar proposé par Zend sur ce sujet, je vous recommande la lecture d’un bon article en anglais de Alex Stetsenko sur PHPMaster.com, dans lequel vous trouverez quelques exemples d’utilisation intéressants.

Exemple d’utilisation

Pour clore ce dossier, je vous propose un petit script PHP vous permettant d’afficher la liste des travaux référencés dans la file d’attente de travaux de ZendJobQueue. Vous pourrez ainsi afficher cette liste sans nécessairement passer par l’interface graphique de Zend Server.

Script PHP à télécharger

A noter que, pour obtenir le contenu de la file d’attente de travaux, j’utilise la méthode getJobsList() de la classe ZendJobQueue. Depuis la version 5.6 de ZendJobQueue, il est devenu nécessaire de transmettre un tableau à cette méthode (même vide), alors que ce n’était pas nécessaire avant cette version. C’est la syntaxe que j’ai utilisée dans mon script PHP exemple :

$joblist = $queue->getJobsList(array());