Batch API vs. Pro Drupal Development 2

Idézet a címbeli könyv 564-565. oldaláról:
A batch set can have the following keys in its associative array. Only the operations key is required.
(...)
* file: If the callback functions for operations and finished are not in scope during a normal Drupal request, the path of the file containing these functions must be given. The path is relative to the base_path() of the Drupal installation and can be conveniently built using drupal_get_path(). It is unnecessary to define file if the functions are already in scope.
Magyar nyersfordításban:
Egy batchet leíró asszociatív tömb az alábbi kulcsokkal rendelkezhet. Csak az operations kulcs kötelező.
(...)
* file: Ha az operations és finished kulcsoknál megadott callback függvények nem elérhetőek egy normál Drupal hívás során, meg kell adni az őket tartalmazó fájlok elérési útját. Ezen elérési út a Drupal telepítés base_path()-jához képest relatív, és kényelmesen előállítható a drupal_get_path() használatával. Szükségtelen megadni a file kulcsot, ha a függvények már elérhetőek.
Nos, ez a legutóbbi mondat nem (vagy nem teljesen így) igaz - de ez csak az egyik probléma, mely a Drupal Batch API-jával való ismerkedésem során felmerült.
Megírtam a hívandó függvényeket, a batchet elindító page callbacket, elhelyeztem mindet ugyanazon .inc fájlban (amit nyilván a hook_menu()-ben is megadtam), és bár az állapotjelző csík megjelent, a callback függvényeim nem futottak le - cserébe viszont hibaüzenetet sem kaptam. Néhány szál hajammal kevesebb lett, mire rájöttem: ezt a file kulcsot igenis meg kell adni, hogy működjön a kötegelt feldolgozás. (Lehet, hogy a Form API myform_submit()-jából indítva a batchet nem kell megadni, de ugye jelen esetben a batchnek nem volt szüksége semmilyen, a felhasználótól érkező paraméterre, tehát egyszerűen page callbackbe került a batchet indító kód.)
A másik probléma az egyes batch callbackek utolsó, &$context['sandbox'] paraméterével kapcsolatos. Idézet az 567. oldalról:
* sandbox: This area is open to use by callback functions. You can store anything you need in here and it will persist. In our example, we store some information about the number of users to import, the user currently being imported, and so forth. Use this instead of $_SESSION for storing information during batch processing. If you use $_SESSION and the user opens a new browser window, bad things could happen.
Magyar nyersfordításban:
* sandbox: Ez a terület szabadon használható a callback függvények számára. Itt bármit tárolhatunk, amire szükség van, s ezek az adatok meg is maradnak. A példánkban itt tárolunk információt az importálandó felhasználók számáról, az éppen importált felhasználóról, és így tovább. A kötegelt feldolgozás során ezt a területet kell használni információtárolásra a $_SESSION helyett. Ha a $_SESSION-t használjuk és a felhasználó egy új böngészőablakot nyit, rossz dolgok történhetnek.
Nos, ez is csak részben igaz. Lássuk az alábbi kódot.
function mymodule_batch_start() {
$operations = array();
$operations[] = array('mymodule_batch_first', array());
$operations[] = array('mymodule_batch_second', array());
$batch = array(
'operations' => $operations,
// This is definitely needed since we are called from a page callback, not from FAPI.
'file' => drupal_get_path('module', 'mymodule') .'/mymodule.batch.inc',
);
batch_set($batch);
// This is needed as well, for the same reason.
batch_process('mymodule_page');
}
function mymodule_batch_first(&$context) {
// Store some information to the sandbox.
$context['sandbox']['mymodule_info'] = t('Information stored by my module.');
}
function mymodule_batch_second(&$context) {
// Retrieve the stored information.
drupal_set_message(t('Stored information:') .' '. $context['sandbox']['mymodule_info']);
}
Lehet tippelni, mit fog kiírni a drupal_set_message(), de inkább elmondom, hogy csak ennyit: Stored information: - ugyanis a sandbox-ban tárolt információk csak ugyanazon callback többszöri meghívása között őrződnek meg, két callback között nem. Megoldásként a variable_set() és variable_get() párost használtam (valamint írtam egy finished callbacket, mely ugye a batch végén fut le, s többek között egy variable_del() hívást is tartalmaz a szemét eltakarítása végett), de egyáltalán nem vagyok benne biztos, hogy ez a legjobb megoldás.
- 1114 Budapest, Kosztolányi Dezső tér 12. II/1a.
- +36 20 3891634, +36 30 2995579
- info@kybest.hu

Hozzászólások
Nekem is hiányzik pár szál hajam
Sajnos tudomásul kell venni, hogy az $operations[] tömbben a callback függvények el vannak szeparálva egymástól. Tehát nem használhatnak közös adatot.
Én a varibale_*() függvények helyett, inkább egy közös process-t indítanék, ami meghívja a mymodule_batch_first mymodule_batch_second és függvényeket.
Nem néztem bele a kódba, de Drupal telepítésnél illetve egyszerre több modul bekapcsolásnál is egyszerre több process van elindítva a *.po fájlok importálására. Ez nagyon kényelmes, de szerintem az az egy látványos baja van hogy 1 db http request elég 1 folyamat befejezéséhez, így a progressbar 0-ról rögtön a végére ugrik. Fölösleges a progressbár mert elég a számlálót figyelni.
Gondolom te csak a példa kedvéért használtad a drupal_set_message() függvényt. Tapasztalataim szerint nem jelenik meg az üzenet csak a kötegelt feladatok befejezése után. Vagy egy másik böngésző ablakban.
Egyébként a Batch process-ben pont az a lényeg, hogy használni kell a $context['finished'] állapotjelzőt.
már rég aludnom kéne
Közös processz nem játszik
Jelen projektnél legalábbis, két ok miatt. Egyrészt van olyan processz, aminek végig kell futnia, mielőtt egy másik elkezdődik; másrészt ezek mindkettője olyan, hogy PHP időtúllépésbe ütköznének (együtt tuti biztosan, de nagy valószínűséggel külön-külön is).
A
drupal_set_message()valóban csak a példa kedvéért van itt; éles környezetben talán célszerűbb a$context['results']használata.Ugyanakkor a
$context['finished']használata csak akkor "kötelező", ha egy processz nem tud (vagy csak nem akar) egy HTTP hívásból befejeződni.Erre gondoltam
Közös sandbox-hoz kell a közös process
function mymodule_menu() { $items = array(); $items['mymodule/batch'] = array( 'title' => 'MyModule Batch', 'page callback' => 'mymodule_batch_start', 'access arguments' => array('administer site configuration'), 'type' => MENU_NORMAL_ITEM, ); return $items; } function mymodule_batch_start() { $args_to_first = array(); $args_to_second = array(); $operations[] = array( 'mymodule_batch_process', array( array( array( 'callback' => 'mymodule_batch_first', 'args' => $args_to_first, ), array( 'callback' => 'mymodule_batch_second', 'args' => $args_to_second, ), ), ), ); $batch = array( 'operations' => $operations, 'finished' => 'mymodule_batch_finished', 'title' => t('Processing Example Batch'), 'init_message' => t('Example Batch is starting.'), 'progress_message' => t('Processed @current out of @total.'), 'error_message' => t('Example Batch has encountered an error.'), ); batch_set($batch); batch_process(''); } function mymodule_batch_process($processes, &$context) { if (!isset($context['sandbox']) OR !array_key_exists('current', $context['sandbox'])) { $context['sandbox']['current'] = 0; } $c = $context['sandbox']['current']; $result = call_user_func_array($processes[$c]['callback'], array(&$context, $processes[$c]['args'])); $context['finished'] = ($context['sandbox']['current'] + $result) / count($processes); if ($result == 1) { $context['sandbox']['current']++; } } function mymodule_batch_finished($success, $results, $operations) { drupal_set_message("');
drupal_set_message("
');
drupal_set_message("
');
}
function mymodule_batch_first(&$context, $args) {
$f = __FUNCTION__;
if (!array_key_exists($f, $context['sandbox']) OR !array_key_exists('state', $context['sandbox'][$f])) {
$context['sandbox'][$f]['state'] = 0;
}
$context['sandbox'][$f]['state'] += 0.2;
$context['results'][$f]['state'][] = $context['sandbox'][$f]['state'];
$context['message'] = "Function = {$f} state = {$context['sandbox'][$f]['state']}";
sleep(1);
return $context['sandbox'][$f]['state'];
}
function mymodule_batch_second(&$context, $args) {
$f = __FUNCTION__;
if (!array_key_exists($f, $context['sandbox']) OR !array_key_exists('state', $context['sandbox'][$f])) {
$context['sandbox'][$f]['state'] = 0;
}
$context['sandbox'][$f]['state'] += 0.2;
$context['results'][$f]['state'][] = $context['sandbox'][$f]['state'];
$context['message'] = "Function = {$f} state = {$context['sandbox'][$f]['state']}";
sleep(1);
return $context['sandbox'][$f]['state'];
}
Foglaljuk össze
Ha jól értem, azt javasolod, hogy minden feldolgozást egyazon Batch API processz végezzen úgy, hogy ő oldja meg a tényleges processzekre bontást: azaz ami a valóságban több processz, azt a Batch API szempontjából nézve egyetlen processzben aggregálod.
A tényleges processzek közötti paraméterátadást, illetve annak eldöntését, hogy mely processzt kell hívnia az aggregátornak, trükkösen oldottad meg, riszpekt.
Egy kérdés felmerül bennem ezzel az egésszel kapcsolatban: mi történik időtúllépéskor? Megfelelően észreveszi-e a megfelelő függvény, hogy honnan kell folytatnia a munkát?
ezt a process függvényben kell figyelni.
Az idő túl lépés figyelést, nehéz _jól_ megoldani.
Én általában darabra szoktam meghatározni a végrehajtandó feladatokat. pl.: 1 könyvtárból kell beimportálni képeket (node|galléria)
És csinálok hozzá admin felületet ahol lehet állítani, hogy mennyi legyen az darabszám, így az Administrator dönthet a PHP beállítások figyelembe vételével. Ugyan ezt meg lehet csinálni másodperc megadásával, vagy a max_execution_time bizonyos százalékának kihasználásával.
Önmagában nézve mindegyik, jó megoldás.
Feladat függő a dolog, de ha lehet, akkor a konkrét műveletet elvégző függvényeket úgy írom meg, hogy cron-ból és batch process-ből is meghívhatóak legyenek.
A cron bonyolítja a helyzetet, mert mindenképpen figyelni kell a hátralévő időt.
ini_get('max_execution_time')
$_SERVER['REQUEST_TIME']
function process(&$context) { if (!isset($context['sandbox']['state'])) { $context['sandbox']['state'] = 'initialize'; } switch ($context['sandbox']['state']) { case 'initialize' : $context['sandbox']['items'] = array(/* ezt fel kell tölteni */); $context['sandbox']['items_count'] = count($context['sandbox']['items']); $context['finished'] = 0; //Lépés a következő fázisra $context['sandbox']['state'] = 'doit-1'; break; case 'doit-1' : //Jellemzően nem probléma ha másik böngésző ablakban //variable_set('mymodule_limt', 8) hívás történik a folyamat közben. $limit = variable_get('mymodule_limt', 2); while ($limit AND $item = array_pop($context['sandbox']['items'])) { item_bizergáló($item); $limit--; } //Ha több doit-# van akkor a finished kiszámolása bonyolultabb. $context['finished'] = ($context['sandbox']['items_count'] - count($context['sandbox']['items'])) / $context['sandbox']['items_count']; break; } }Ez a változat akkor (lehet) jó ha az összes végre hajtandó feladat ismert.
Még soha nem próbáltam de amit a másik hozzászólásban írtam az akkor lehet jó, ha a proccess-ek egy module_invoke_all()-lal vannak begyűjtve.
(De akkor valószinüleg, nem szükséges a közös sandbox)
Egyébként a PHPmanual importálóban ezt a módszert használom