Maggio
29

Ieri è stato il secondo giorno di Summer of Code per Drupal, io e chx dovevamo valutare come realizzare un algoritmo che permettesse a più crawler di catturare contemporaneamente tutti i links presenti in una pagina e di memorizzarli nel database. Abbiamo previsto a tal scopo due tabelle: una di nome crawler contenente solo un campo “id” di tipo intero auto_increment e una di nome “crawler_links” contenente “id”, “path”, “status” e il campo “has_form”, che in futuro servirà a segnalare la presenza di una form nella pagina.

L’idea era quella di fare la scansione di una pagina e di salvarne i link nel db. Dopodichè si doveva procedere all’estrazione del primo link che ha ha come status il valore 0 e come crawler_id il valore 0 dal database per farne la scansione con il crawler e salvarne nuovamente i link in esso contenuti. In sostanza nel database veniva salvata una coda di link estratti dalle pagine e pronti per essere processati. Dopo aver processato la pagina si doveva fare l’update del record riportante quella pagina nel database per segnalare status = 1 e crawler_id = al nome del crawler. Il nome del crawler per questo scopo doveva avere caratteristiche di atomicità, per far si che con crawler multipli non si finisse per contrassegnare due link nel database con lo stesso crawler. Ciò che abbiamo fatto è stato sfruttare l’auto_increment del campo “id” nella tabella crawler per garantire questa caratteristica. A questo punto l’algoritmo sarebbe potuto funzionare, ma occorreva immaginare completamente lo scenariocon la presenza di più crawler che contemporaneamente svolgevano questo algortmo per valutarne la correttezza.

Il problema della contemporaneità delle operazioni nasce dal fatto che myisam non ha il supporto alle transazioni, per cui occorre fare in modo che le operazioni non vadano a sovrapporsi.
Supponiamo che l’algoritmo alla base del nostro crawler sia questo:

db_query("SELECT id,path FROM {crawler_links} WHERE crawler_id = %d AND status = 0 LIMIT 1", $crawler_id));
db_query(”UPDATE {crawler_links} SET crawler_id = %d WHERE crawler_id = 0 LIMIT 1″, $crawler_id);

E’ evidente in questo caso che se partissero due crawler in contemporanea, può capitare che nella SELECT venga estratto per due volte lo stesso crawler_id. Questo succede quando l’operazione di UPDATE è ancora in esecuzione mentre l’operazione di SELECT viene terminata dal secondo crawler. Per risolvere questa problematica abbiamo invertito l’ordine delle operazioni assegnando eseguendo prima l’update del campo in questo modo:

db_query("UPDATE {crawler_links} SET crawler_id = %d WHERE crawler_id = 0 LIMIT 1", $crawler_id);

poi selezionando il link dalla tabella:

$page_to_visit = db_fetch_array(db_query("SELECT id,path FROM {crawler_links} WHERE crawler_id = %d AND status = 0 LIMIT 1", $crawler_id));

e infine facendo nuovamente update sul campo status, assegnandogli un valore per rimuoverlo dai valori estratti dalla select precedente:

db_query("UPDATE {crawler_links} SET status = 1 WHERE crawler_id = %d and status = 0 LIMIT 1", $crawler_id);

Questo permetterà a due o più crawler di lavorare contemporaneamente, senza rischiare sovrapposizioni.
E questo è il bello del SoC, imparare a ragionare per risolvere i problemi nel modo migliore possibile.

Commenti