2010-02-15 9 views
5

Per esempio, non c'è API remoto con le seguenti chiamate:design/architettura domanda: rollback con i servizi remoti

getGroupCapacity(group) 
setGroupCapacity(group, quantity) 
getNumberOfItemsInGroup(group) 
addItemToGroup(group, item) 
deleteItemFromGroup(group, item) 

Il compito è quello di aggiungere qualche elemento a qualche gruppo. I gruppi hanno capacità. Quindi per prima cosa dovremmo controllare se il gruppo non è pieno. In tal caso, aumentare la capacità, quindi aggiungere l'elemento. Qualcosa di simile (ad esempio API è esposta con sapone):

function add_item($group, $item) { 
    $soap = new SoapClient(...); 
    $capacity = $soap->getGroupCapacity($group); 
    $itemsInGroup = $soap->getNumberOfItemsInGroup($group); 
    if ($itemsInGroup == $capacity) { 
     $soap->setGroupCapacity($group, $capacity + 1); 
    } 
    $soap->addItemToGroup($group, $item); 
} 

Ora, cosa succede se addItemToGroup fallito (voce era male)? Abbiamo bisogno di ripristinare la capacità del gruppo.

Ora immagina di dover aggiungere 10 elementi al gruppo e quindi impostare gli elementi aggiunti con alcune proprietà e tutto ciò in un'unica transazione. Ciò significa che se fallisce da qualche parte nel mezzo è necessario eseguire il rollback di tutto allo stato precedente.

E 'possibile senza mazzo di codici IF e spaghetti? Qualunque libreria, struttura, modello o decisione di architettura che semplificherà tali operazioni (in PHP)?

UPD: SOAP è solo un esempio. La soluzione dovrebbe adattarsi a qualsiasi servizio, anche il TCP non elaborato. Il punto principale della domanda è come organizzare il comportamento delle transazioni con l'API non transazionale sottostante.

UPD2: Immagino che questo problema sia praticamente uguale in tutti i linguaggi di programmazione. Quindi tutte le risposte sono benvenute, non solo PHP.

Grazie in anticipo!

risposta

4
<?php 
// 
// Obviously better if the service supports transactions but here's 
// one possible solution using the Command pattern. 
// 
// tl;dr: Wrap all destructive API calls in IApiCommand objects and 
// run them via an ApiTransaction instance. The IApiCommand object 
// provides a method to roll the command back. You needn't wrap the 
// non-destructive commands as there's no rolling those back anyway. 
// 
// There is one major outstanding issue: What do you want to do when 
// an API command fails during a rollback? I've marked those areas 
// with XXX. 
// 
// Barely tested but the idea is hopefully useful. 
// 

class ApiCommandFailedException extends Exception {} 
class ApiCommandRollbackFailedException extends Exception {} 
class ApiTransactionRollbackFailedException extends Exception {} 

interface IApiCommand { 
    public function execute(); 
    public function rollback(); 
} 


// this tracks a history of executed commands and allows rollback  
class ApiTransaction { 
    private $commandStack = array(); 

    public function execute(IApiCommand $command) { 
     echo "EXECUTING " . get_class($command) . "\n"; 
     $result = $command->execute(); 
     $this->commandStack[] = $command; 
     return $result; 
    } 

    public function rollback() { 
     while ($command = array_pop($this->commandStack)) { 
      try { 
       echo "ROLLING BACK " . get_class($command) . "\n"; 
       $command->rollback(); 
      } catch (ApiCommandRollbackFailedException $rfe) { 
       throw new ApiTransactionRollbackFailedException(); 
      } 
     } 
    } 
} 


// this groups all the api commands required to do your 
// add_item function from the original post. it demonstrates 
// a nested transaction. 
class AddItemToGroupTransactionCommand implements IApiCommand { 
    private $soap; 
    private $group; 
    private $item; 
    private $transaction; 

    public function __construct($soap, $group, $item) { 
     $this->soap = $soap; 
     $this->group = $group; 
     $this->item = $item; 
    } 

    public function execute() { 
     try { 
      $this->transaction = new ApiTransaction(); 
      $this->transaction->execute(new EnsureGroupAvailableSpaceCommand($this->soap, $this->group, 1)); 
      $this->transaction->execute(new AddItemToGroupCommand($this->soap, $this->group, $this->item)); 
     } catch (ApiCommandFailedException $ae) { 
      throw new ApiCommandFailedException(); 
     } 
    } 

    public function rollback() { 
     try { 
      $this->transaction->rollback(); 
     } catch (ApiTransactionRollbackFailedException $e) { 
      // XXX: determine if it's recoverable and take 
      //  appropriate action, e.g. wait and try 
      //  again or log the remaining undo stack 
      //  for a human to look into it. 
      throw new ApiCommandRollbackFailedException(); 
     } 
    } 
} 


// this wraps the setgroupcapacity api call and 
// provides a method for rolling back  
class EnsureGroupAvailableSpaceCommand implements IApiCommand { 
    private $soap; 
    private $group; 
    private $numItems; 
    private $previousCapacity; 

    public function __construct($soap, $group, $numItems=1) { 
     $this->soap = $soap; 
     $this->group = $group; 
     $this->numItems = $numItems; 
    } 

    public function execute() { 
     try { 
      $capacity = $this->soap->getGroupCapacity($this->group); 
      $itemsInGroup = $this->soap->getNumberOfItemsInGroup($this->group); 
      $availableSpace = $capacity - $itemsInGroup; 
      if ($availableSpace < $this->numItems) { 
       $newCapacity = $capacity + ($this->numItems - $availableSpace); 
       $this->soap->setGroupCapacity($this->group, $newCapacity); 
       $this->previousCapacity = $capacity; 
      } 
     } catch (SoapException $e) { 
      throw new ApiCommandFailedException(); 
     } 
    } 

    public function rollback() { 
     try { 
      if (!is_null($this->previousCapacity)) { 
       $this->soap->setGroupCapacity($this->group, $this->previousCapacity); 
      } 
     } catch (SoapException $e) { 
      throw new ApiCommandRollbackFailedException(); 
     } 
    } 
} 

// this wraps the additemtogroup soap api call 
// and provides a method to roll the changes back 
class AddItemToGroupCommand implements IApiCommand { 
    private $soap; 
    private $group; 
    private $item; 
    private $complete = false; 

    public function __construct($soap, $group, $item) { 
     $this->soap = $soap; 
     $this->group = $group; 
     $this->item = $item; 
    } 

    public function execute() { 
     try { 
      $this->soap->addItemToGroup($this->group, $this->item); 
      $this->complete = true; 
     } catch (SoapException $e) { 
      throw new ApiCommandFailedException(); 
     } 
    } 

    public function rollback() { 
     try { 
      if ($this->complete) { 
       $this->soap->removeItemFromGroup($this->group, $this->item); 
      } 
     } catch (SoapException $e) { 
      throw new ApiCommandRollbackFailedException(); 
     } 
    } 
} 


// a mock of your api 
class SoapException extends Exception {} 
class MockSoapClient { 
    private $items = array(); 
    private $capacities = array(); 

    public function addItemToGroup($group, $item) { 
     if ($group == "group2" && $item == "item1") throw new SoapException(); 
     $this->items[$group][] = $item; 
    } 

    public function removeItemFromGroup($group, $item) { 
     foreach ($this->items[$group] as $k => $i) { 
      if ($item == $i) { 
       unset($this->items[$group][$k]); 
      } 
     } 
    } 

    public function setGroupCapacity($group, $capacity) { 
     $this->capacities[$group] = $capacity; 
    } 

    public function getGroupCapacity($group) { 
     return $this->capacities[$group]; 
    } 

    public function getNumberOfItemsInGroup($group) { 
     return count($this->items[$group]); 
    } 
} 

// nested transaction example 
// mock soap client is hardcoded to fail on the third additemtogroup attempt 
// to show rollback 
try { 
    $soap = new MockSoapClient(); 
    $transaction = new ApiTransaction(); 
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group1", "item1")); 
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group1", "item2")); 
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group2", "item1")); 
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group2", "item2")); 
} catch (ApiCommandFailedException $e) { 
    $transaction->rollback(); 
    // XXX: if the rollback fails, you'll need to figure out 
    //  what you want to do depending on the nature of the failure. 
    //  e.g. wait and try again, etc. 
} 
0

PHP Exceptions

Si potrebbe incapsulare le singole query di sapone in classi gettando appropriate eccezioni.

Una soluzione più sporca sarebbe quella di creare un array di eccezioni e aggiungere manualmente queryStatus = false o queryStatus = true a ogni passaggio e quindi verificare se la transazione proposta è valida. In tal caso, si chiama un metodo di commitTransaction finale.

+0

Siamo spiacenti, non capisco come questo aiuterà. Dove devo prendere quelle eccezioni e inserire il codice di rollback? Esempio, forse? :-) Grazie – Qwerty

0

In teoria, uno della famiglia "WS-DeathStar" -protocollo, ovvero WS-Transaction si occupa precisamente di questo. Tuttavia, non sono a conoscenza (non sono un programmatore PHP, comunque) di qualsiasi implementazione di questo standard in PHP.

+0

Il servizio remoto non lo supporta. SOAP è solo un esempio, ho bisogno di una soluzione più generica. – Qwerty

1

I servizi remoti in genere non supportano le transazioni. Non conosco PHP, ma in BPEL hai qualcosa chiamato come Compensation.

Compensazione o annullamento di passaggi nel processo aziendale già completati correttamente, è uno dei concetti più importanti nei processi aziendali. L'obiettivo del risarcimento è di invertire gli effetti delle attività precedenti che sono state svolte nell'ambito di un processo aziendale che viene abbandonato.

Forse potresti provare qualcosa di simile. Ce ne saranno alcuni se/else.

+0

BPEL sembra qualcosa di enorme, basato sullo stack WS- *. Non sono sicuro, funzionerà qualche servizio con esso? – Qwerty

+0

Ti sto suggerendo di prendere il concetto di "compensazione" e attuare qualcosa di simile. – Padmarag

+0

OK, grazie. Cercherò maggiori dettagli in seguito. Come ottenere l'idea dalla lettura (http://rodin.cs.ncl.ac.uk/Publications/Coleman-ExaminingBPEL.pdf), il concetto sta solo salvando lo stato e dopo aver provato/tranne i blocchi su ogni fase del processo. Questo è qualcosa di abbastanza chiaro senza BPEL, ma la domanda è anche come farlo in modo intelligente senza scrivere troppo codice. – Qwerty

0

Sembra che siano necessarie transazioni e/o blocchi, proprio come un database.il codice cliente sarebbe leggere qualcosa di simile:

 
function add_item($group, $item) { 
    $soap = new SoapClient(...); 
    $transaction = $soap->startTransaction(); 
    # or: 
    # $lock = $soap->lockGroup($group, "w"); 
    # strictly to prevent duplication of the rest of the code: 
    # $transaction = $lock; 
    $capacity = $soap->getGroupCapacity($transaction, $group); 
    $itemsInGroup = $soap->getNumberOfItemsInGroup($transaction, $group); 
    if ($itemsInGroup == $capacity) { 
     $soap->setGroupCapacity($transaction, $group, $capacity + 1); 
    } 
    $soap->addItemToGroup($transaction, $group, $item); 
    $transaction->commit(); 
    # or: $lock->release(); 
} 

Naturalmente, sarà necessario per gestire i clienti comportamento anomalo, come quelli che si scontrano prima di commettere/rilasciare o quelli che si bloccano troppo, causando altri clienti a fallire inutilmente. Ciò è possibile con inattività e timeout massimi e il numero massimo di blocchi per client.

1

Inserire la logica di transazione sul lato remoto. setGroupCapacity() dovrebbe essere incapsulato in addItemToGroup(). Questo è lo stato interno, qualcosa a cui il chiamante non dovrebbe preoccuparsi. Con questo puoi aggiungere articoli per articolo e facilmente svolgerlo con deleteItemFromGroup().

Se si deve convivere con un'API di basso livello, il rollback si basa sul monitoraggio del flusso di azioni.

0

Gregor Hohpe ha scritto un bel riassunto dei vari approcci per la gestione degli errori in remoto:

Your Coffee Shop Doesn’t Use Two-Phase Commit

In breve:

  • Storno: non fare nulla, o scartare il lavoro fatto.
  • Riprovare: riprovare le parti non riuscite. Più facile se si progetta il servizio come idempotente, in modo che possa essere eseguito ripetutamente con lo stesso input senza effetti negativi.
  • Azione compensativa: fornire un servizio con un'azione di compensazione che consente di annullare il lavoro fino a quel momento.
  • Coordinatore transazioni: il tradizionale commit a due fasi. Teoricamente ideale, difficile da usare nella pratica, un sacco di middleware bacato là fuori.

Tuttavia, nel tuo caso, potrebbe essere che l'API remota sia troppo fine. Hai davvero bisogno di setGroupCapacity come servizio separato? Che ne dici di fornire semplicemente addUserToGroup e lasciare che il servizio gestisca l'eventuale aumento di capacità necessario internamente? In questo modo, l'intera transazione potrebbe essere contenuta in un'unica chiamata di servizio.

L'API corrente si apre anche per problemi di concorrenza e condizioni di gara. Cosa succede se, tra la chiamata a getNumberOfItemsInGroup e setGroupCapacity, qualche altro thread riesce ad aggiungere un utente? La tua richiesta fallirà perché l'altro thread "ruba" il tuo aumento di capacità.

+0

Ciao, grazie per il link. Il servizio remoto è qualcosa che non posso cambiare. Sì, più di 1 client che utilizzano lo stesso servizio possono causare disastri, perché il blocco non è supportato. Anche le transazioni non saranno d'aiuto (i rollback dovrebbero essere molto intelligenti :-)). – Qwerty

Problemi correlati