Multitheading con Drush

Come aggiornare il path alias di oltre 200K nodi in meno di 30min.

22 October, 2016

L’anno scorso mi sono trovato di fronte ad un problema alquanto scomodo: ristrutturare gli URL dei contenuti presenti nel sito di un cliente per migliorarne la leggibilità e l’indicizzazione nei motori di ricerca. Tralasciando tutte le questioni inerenti ai redirect 301 da gestire, voglio concentrare questo post sulle performance raggiunte per aggiornare più di 200.000 path alias.

Il mio primo approccio è stato quello di utilizzare il “gancio” hook_update_N() come ho descritto nell’articolo di qualche settimana fa. La procedura faceva correttamente il suo dovere, ma con tempi di esecuzione inproponibili al cliente, poiché tenere offline per due ore e mezza un sito che fa circa 6,5K di visite al giorno non lo aiuta a mantenere una buona reputazione nel web.

La prima cosa che mi è balzata in mente è stata quella di suddividere l’intera mole di dati in porzioni più piccole da processare in modo concorrente: il cosiddetto multithreading. L’ articolo di John Ennew (Techical Lead in Deeson) è stato provvidenziale, poiché mi è bastato personalizzare il suo drush multithread manager per raggiungere il mioscopo. Per vostra conoscenza, potete trovare il codice completo scritto da John su GitHub.

Vediamo allora come implementare l’aggiornamento dei path alias con un comando Drush multithreading.

Creare il comando Drush

La prima cosa che ho fatto è stata quella di creare il mio comando Drush. Per essere più precisi ho creato due comandi:

  • update-alias, che esegue l'aggiornamento del path alias per l'insieme di nodi che gli verrà passato dal gestore del multithread.
  • mt-command, che eseguirà il comando update-alias in modalità multithread;
  /**
   * Implements of hook_drush_command().
   */
  function my_module_drush_command() {
    $items = array();

    $items['mt-command'] = array(
      'description' => 'This command manages all multithread processes.',
      'arguments' => array(
        'limit' => 'Total number of jobs to up - use 0 for all.',
        'batch_size' => 'Number of jobs each thread will work on.',
        'threads' => 'Number of threads',
      ),
      'options' => array(
        'offset' => 'A starting offset should you want to start 1000 records in',
      ),
      'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT,
    );

    $items['update-alias'] = array(
      'description' => 'Update the node url alias.',
      'arguments' => array(
        'name' => 'The name of this process, this will be the thread id',
        'limit' => 'Number of jobs each thread will work on.',
        'offset' => 'A starting offset should you want to start 1000 records in',
      ),
      'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT,
    );

    return $items;
  }

Vediamo in dettaglio il contenuto delle funzioni eseguite da questi due comandi. A proposito, per associare un conmando ad una funzione basta il nome di quest'ultima sia formato dal nome del comando sostituendo il 'meno' (-, hyphen) con il 'trattino basso' (_, underscore) ed aggiungere come prefisso drush_. Quindi se il comando è mt-command la funzione sarà drush_mt_command.

Aggiornare i path alias

Partiamo dall funzione più semplice, quella che esegue l'aggiornamento degli URL.

  /**
   * Updates url alias.
   */
  function drush_update_alias($thread, $limit, $offset, $node_types = '') {
    # code
  }

Per prima cosa recuperiamo l'insieme di nodi da aggiornare. La cardinalità di questo insieme è definita dal paramentro $limit, mentre il punto di partenza da cui iniziare l'aggiornamento lo troviamo nel parametro $offset. Il parametro $thread infine contiene l'identificativo del thread in esecuzione su questo insieme di dati.

  // Retrieve the next group of nids.
  $query = db_select('node', 'n');

  $result = $query->fields('n', array('nid'))
      ->orderBy('n.nid', 'ASC')
      ->condition('type', $node_types_array, 'IN')
      ->range($offset, $limit)->execute();

Poi aggiorneremo l'URL utilizzando la funzione pathauto_node_update_alias() che troviamo all'interno del modulo Pathauto.

  // Load pathauto.module from the pathauto module.
  module_load_include('module', 'pathauto', 'pathauto');

  $current_node = 0;
  foreach ($result as $row) {
    // Create the nids array
    $node = node_load($row->nid);
    pathauto_node_update_alias($node, 'update');

    // Update our progress information.
    $current_node = $row->nid;
  }

Eseguire processi concorrenti

Quello che dobbiamo fare ora è riuscire ad eseguire contemporaneamente la funzione descritta qui sopra su porzioni di dati differenti. Per farlo abbiamo bisogno di definire le seguenti cinque funzioni:

  • drush_mt_command(), implementa il comando mt-command. Non fa altro che avviare la gestione multithread per l'aggiornamento di tutti i nodi;
  • _mt_command_setup(), costruisce il comando Drush da eseguire per ogni singolo thread. Nel nostro caso il comando sarà update-alias;
  • _mt_command_teardown(), se abbiamo bisogno di eseguire alcuni comandi al termine dell'esecuzione di un thread, li possiamo inserire qui;
  • _mt_monitor_process(), chiude in modo sicuro il processo quando ha terminato la sua esecuzione;
  • drush_thread_manager(), gestisce l'esecuzione di tutti i processi fino a completare l'aggiornamento del path alias per tutti i nodi.

Nel mio caso le funzioni che ho personalizzato sono le prime due e le vedremo in dettagli qui di seguito. Mentre le altre le ho praticamente copiate e lasciate invariate come potete trovare nel repository di GitHub. La funzione _mt_command_teardown() l'ho lasciata vuota visto che non c'è bisogno di utilizzarla.

Setup di ogni singolo job

Il compito di ogni singolo job è quello di aggiornare il path alias di un'insieme definito di nodi. Grazie ai parametri $limit e $offset posso identificare la prozione di nodi su cui lavorare. Il paramentro $thread_id l'ho aggiunto solo per poterlo stampare come log. La funzione _mt_command_setup alla fine restiuisce una stringa contentente la riga di comando drush che verrà eseguita dal gestore dei processi.

function _mt_command_setup($thread_id, $limit, $offset) {
  $node_types = $GLOBALS['node_types'];
  $cmd = "drush update-alias $thread_id $limit $offset $node_types";
  return $cmd;
}

Il gestore dei processi concorrenti

Chi è che inizializza e avvia il gestore di processi? Allora, ricollegandomi a quanto stavo dicendo qualche riga fa, questo compito è svolto dalla funzione drush_mt_command(). Per eseguire la funzione lanciamo il comando $ drush mt-command [limit] [batch_size] [threads] dove:

  • limit, è il numero totale dei nodi che vogliamo aggiornare;
  • batch_size, la quantità di nodi da elaborare per ogni job;
  • threads, il numero di processi che possono essere in esecuzione contemporaneamente.

Se vogliamo elaborare tutti i nodi presenti nel nostro database (nel mio caso ho deciso di aggiornare solo i contenuti di tipo 'article' e 'page'),

  // Choose the content-type you will update.
  $node_types = array(
    'page',
    'article',
  );

allora dobbiamo impostare a 0 il paramentro limit.

  if($limit == 0) {
    // Retrieve all records
    $query = db_select('node', 'n');
    $result = $query->fields('n', array('nid'))
      ->orderBy('n.nid', 'ASC')
      ->condition('type', $node_types, 'IN')
      ->execute();

    $limit = $result->rowCount();
  }

A questo punto è tutto pronto per avviare il gestore dei processi.

  drush_thread_manager($limit, $batch_size, $threads, '_mt_command_setup',
    '_mt_command_teardown', $starting_offset);

Conclusioni

Se dovete eseguire applicare qualche modifica a una grossa mole di dati del vostrp sito web in Drupal, penso che questo instroduzione alla programmazione multi-thread utilizzando Drush faccia al caso vostro.

Nel mio repository GitHub potete trovare il codice completo. Sono certo che si può far ancora meglio per velocizzare la procedura che ho descritto qui sopra, quindi se avete suggerimenti da darmi contattatemi, sarò felice di discuterne assieme.