2010-02-15 18 views
5

Por ejemplo, hay API remota con las siguientes llamadas:Diseño/Arquitectura pregunta: reversiones con servicios remotos

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

La tarea es añadir algún elemento a algún grupo. Los grupos tienen capacidad. Primero debemos verificar si el grupo no está lleno. Si es así, aumente la capacidad, luego agregue el artículo. Algo como esto (por ejemplo, la API está expuesta con jabón):

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); 
} 

Ahora lo que si no addItemToGroup (artículo era malo)? Necesitamos revertir la capacidad del grupo.

Ahora imagine que tiene que agregar 10 elementos para agrupar y luego configurar elementos agregados con algunas propiedades, y todo esto en una sola transacción. Eso significa que si falla en algún lugar en el medio debe revertir todo al estado anterior.

¿Es posible sin un montón de IF y código de espagueti? ¿Cualquier decisión de biblioteca, marco, patrón o arquitectura que simplificará dichas operaciones (en PHP)?

UPD: SOAP es sólo un ejemplo. La solución debería adaptarse a cualquier servicio, incluso TCP sin procesar. El punto principal de la pregunta es cómo organizar el comportamiento transaccional con la API subyacente no transaccional.

UPD2: Supongo que este problema es bastante similar en todos los lenguajes de programación. Entonces, cualquier respuesta es bienvenida, no solo PHP.

¡Gracias de antemano!

Respuesta

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

Se puede encapsular las consultas de SOAP individuales en clases lanzar excepciones apropiadas.

Una solución más sucia sería crear una matriz de excepciones y agregar manualmente queryStatus = false o queryStatus = true a cada paso y luego verificar si la transacción propuesta es válida. Si es así, llama a un método de commitTransaction final.

+0

Lo siento, no puedo entender cómo esto ayudará. ¿Dónde debería detectar esas excepciones y colocar el código de reversión? Ejemplo, tal vez? :-) Gracias – Qwerty

0

Teóricamente, una de las familias de "WS-DeathStar" -protocol, es decir, WS-Transaction se ocupa precisamente de eso. Sin embargo, no estoy al tanto (aunque no soy un desarrollador de PHP) de ninguna implementación de este estándar en PHP.

+0

El servicio remoto no lo admite. SOAP es solo un ejemplo, necesito una solución más genérica. – Qwerty

1

Los servicios remotos generalmente no admiten transacciones. No sé PHP, pero en BPEL tienes algo llamado Compensation.

Compensación, o deshacer los pasos en el proceso de negocio que ya han completado con éxito, es uno de los conceptos más importantes en los procesos de negocio. El objetivo de la compensación es revertir los efectos de actividades anteriores que se han llevado a cabo como parte de un proceso comercial que se está abandonando.

Quizás puedas probar algo similar. Habrá algo de if/else.

+0

BPEL parece algo enorme, basado en WS- * stack. No estoy seguro, ¿funcionará algún servicio con eso? – Qwerty

+0

Le sugiero que tome el concepto de 'compensación' e implemente algo similar. – Padmarag

+0

OK, gracias. Buscaré más detalles más tarde. Como se entiende por lectura (http://rodin.cs.ncl.ac.uk/Publications/Coleman-ExaminingBPEL.pdf), el concepto es simplemente guardar el estado y tener bloqueos try/except en cada paso del proceso. Eso es algo bastante claro sin BPEL, pero la cuestión también es cómo hacerlo de manera inteligente sin escribir demasiado código. – Qwerty

0

Parece que necesita transacciones y/o bloqueo, al igual que una base de datos.su código de cliente podría decir algo como:

 
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(); 
} 

Por supuesto, usted necesita para manejar clientes mal comportamiento, como las que chocan antes de comprometerse/liberación o aquellas que bloquean demasiado, causando otros clientes fallen innecesariamente. Esto es posible con la inactividad y los tiempos de espera máximos, y el número máximo de bloqueos por cliente.

1

poner la lógica de transacción en el lado remoto. setGroupCapacity() debe estar encapsulado en addItemToGroup(). Este es un estado interno, algo de lo que la persona que llama no debería molestarse. Con esto puede agregar elemento por elemento y desenrollarlo fácilmente con deleteItemFromGroup().

Si usted tiene que vivir con una API de bajo nivel a continuación, el retroceso que se basa en el seguimiento de su flujo de acciones.

0

Gregor Hohpe escribió un buen resumen de los diferentes enfoques para el manejo de errores de forma remota:

Your Coffee Shop Doesn’t Use Two-Phase Commit

En resumen:

  • Cancelación: no hacer nada, o descartar el trabajo hecho.
  • reintento: reintentar las partes que fallaron. Más fácil si diseña su servicio para ser idempotent, de modo que pueda ejecutarse repetidamente con la misma entrada sin efectos nocivos.
  • Compensar la acción: ofrecer un servicio con una acción de compensación que le permite deshacer el trabajo hasta el momento.
  • coordinador de transacciones: el de dos fases tradicionales cometió. Teóricamente ideal, difícil de eliminar en la práctica, hay muchos middleware con errores por ahí.

Sin embargo, en su caso, es posible que la API remota sea demasiado fina. ¿Realmente necesita setGroupCapacity como un servicio aparte? ¿Qué le parece proporcionar addUserToGroup y dejar que el servicio gestione cualquier aumento de capacidad necesario internamente? De esta forma, toda la transacción podría estar contenida en una única llamada de servicio.

Su API actual también se abre para los problemas de concurrencia y las condiciones de carrera. ¿Qué pasa si, entre la llamada al getNumberOfItemsInGroup y setGroupCapacity, algún otro subproceso logra agregar un usuario? Su solicitud fallará porque el otro hilo "robó" su capacidad de aumento.

+0

Hola, gracias por el enlace. El servicio remoto es algo que no puedo cambiar. Sí, más de 1 cliente que usa el mismo servicio puede causar un desastre, porque el bloqueo no es compatible también. Incluso las transacciones no ayudarán aquí (o las reversiones deberían ser muy inteligentes :-)). – Qwerty

Cuestiones relacionadas