2010-02-15 4 views
5

와 롤백은 다음 호출로 원격 API있다 :디자인/건축 질문 : 원격 서비스 예를 들어

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

작업은 어떤 그룹에 일부 항목을 추가하는 것입니다. 그룹에는 용량이 있습니다. 먼저 그룹이 가득 차 있지 않은지 확인해야합니다. 그럴 경우 용량을 늘린 다음 항목을 추가하십시오. (예를 들어, API는 SOAP과 노출) 이런 식으로 뭔가 :

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

이제 addItemToGroup이 실패한 경우 (항목이 나쁜) 무엇을? 그룹의 역량을 롤백해야합니다.

이제는 그룹에 10 개의 항목을 추가 한 다음 일부 속성이있는 항목을 추가해야합니다.이 모든 것을 단일 트랜잭션으로 추가해야합니다. 즉, 중간에 오류가 발생하면 이전 상태로 모든 것을 롤백해야합니다.

IF 및 스파게티 코드가 없어도 가능합니까? 이러한 작업을 단순화하는 라이브러리, 프레임 워크, 패턴 또는 아키텍처 결정 (PHP)

UPD : SOAP은 하나의 예입니다. 솔루션은 TCP가 아닌 모든 서비스에도 적합해야합니다. 문제의 핵심은 비 트랜잭션 API를 사용하여 트랜잭션 동작을 구성하는 방법입니다.

UPD2 :이 문제는 모든 프로그래밍 언어에서 동일하다고 생각합니다. 따라서 PHP뿐 아니라 모든 대답을 환영합니다.

미리 감사드립니다.

답변

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

당신은 적절한 예외를 던지는 클래스의 개별 SOAP 쿼리를 캡슐화 할 수있다.

더러운 솔루션은 예외 배열을 만들고 각 단계에 queryStatus = false 또는 queryStatus = true를 수동으로 추가 한 다음 제안 된 트랜잭션이 유효한지 확인하는 것입니다. 그렇다면 최종 commitTransaction 메소드를 호출합니다.

+0

죄송합니다. 이것이 도움이되는 방식을 이해할 수 없습니다. 어디에서 예외를 찾아서 롤백 코드를 사용해야합니까? 예, 어쩌면? :-) 감사합니다 – Qwerty

0

이론적으로 "WS-DeathStar"- 프로토콜 계열 중 하나 인 WS-Transaction은이를 정확히 처리합니다. 그러나, 나는이 표준을 PHP로 구현 한 것을 (나는 PHP 개발자가 아니다) 알지 못한다.

+0

원격 서비스가 지원하지 않습니다. SOAP은 단지 예일 뿐이며, 좀 더 일반적인 솔루션이 필요합니다. – Qwerty

1

원격 서비스는 일반적으로 트랜잭션을 지원하지 않습니다. PHP는 모르지만 BPEL에서는 Compensation이라는 이름을 가지고 있습니다.

이미 성공적으로 완료된 비즈니스 프로세스의 단계를 취소하거나 취소하는 것은 비즈니스 프로세스에서 가장 중요한 개념 중 하나입니다. 보상 목표는 버려지는 비즈니스 프로세스의 일부로 수행 된 이전 활동의 영향을 취소하는 것입니다.

아마도 당신은 비슷한 것을 시도 할 수 있습니다. if/else가있을 것입니다.

+0

BPEL은 WS- * stack을 기반으로 무언가 거대 해 보입니다. 확실하지 않습니다. 서비스가 작동합니까? – Qwerty

+0

나는 '보상'이라는 개념을 취하고 비슷한 것을 구현하라고 제안합니다. – Padmarag

+0

좋습니다, 감사합니다. 나중에 자세한 내용을 찾아 보겠습니다. 독서 (http://rodin.cs.ncl.ac.uk/Publications/Coleman-ExaminingBPEL.pdf)에서 아이디어를 얻으면서 개념은 단지 상태를 저장하고 프로세스의 각 단계에서 블록을 시도/제외하는 것입니다. 이는 BPEL이 없으면 확실한 것이지만 너무 많은 코드를 작성하지 않고도 똑똑하게 처리하는 방법에 대한 질문입니다. – Qwerty

0

데이터베이스처럼 트랜잭션 및/또는 잠금이 필요한 것 같습니다.클라이언트 코드가 같은 것을 읽을 것입니다 : 물론

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

을, 당신은/커밋 해제 전에 그 충돌하는 또는 다른 클라이언트가 불필요하게 실패하는 원인이 너무 많이 잠 것과 같은 오작동 클라이언트를 처리해야합니다. 이것은 비활성 및 최대 시간 초과 및 클라이언트 당 최대 잠금 수를 사용하면 가능합니다.

1

원격지에 트랜잭션 로직을 넣으십시오. setGroupCapacity()는 addItemToGroup()에 캡슐화되어야합니다. 내부 상태이므로 발신자가 신경 쓰지 않아도됩니다. 이렇게하면 항목별로 항목을 추가하고 deleteItemFromGroup()을 사용하여 항목을 쉽게 풀 수 있습니다.

낮은 수준의 API로 실행해야하는 경우 롤백은 사용자의 동작 흐름을 추적합니다.

0

그레고르 Hohpe 원격으로 오류를 처리하기위한 다양한 방법의 좋은 요약을 썼다 :

Your Coffee Shop Doesn’t Use Two-Phase Commit

간단히 :

  • 쓰기 오프 : 아무것도하지 않고, 또는 일을 폐기 끝난.
  • 다시 시도 : 실패한 부분을 다시 시도하십시오. 멱등호이되도록 서비스를 설계하면 쉽게 동일 입력으로 반복적으로 실행할 수 있습니다.
  • 보상 작업 : 지금까지 작업을 취소 할 수있는 보완 조치를 서비스에 제공하십시오.
  • 트랜잭션 코디네이터 : 전통적인 2 단계 커밋. 이론적으로 이상적으로, 실전에서 잡아 당기는 것이 어렵다. 많은 버그 미들웨어가있다.

그러나 귀하의 경우 원격 API가 너무 세밀 할 수 있습니다. 별도의 서비스로 실제로 setGroupCapacity이 필요합니까? 그냥 addUserToGroup을 제공하고 내부적으로 필요한 용량을 처리하도록 서비스하는 방법은 무엇입니까? 그렇게하면 전체 트랜잭션이 단일 서비스 호출에 포함될 수 있습니다.

현재 API는 동시성 문제와 경쟁 조건을 위해 열립니다. getNumberOfItemsInGroupsetGroupCapacity 사이의 호출 사이에 다른 스레드가 사용자를 추가 할 수 있다면 어떻게 될까요? 다른 스레드가 용량 증가를 "훔치기"때문에 요청이 실패합니다.

+0

안녕하세요, 링크 주셔서 감사합니다. 원격 서비스는 내가 바꿀 수없는 것입니다. 그렇습니다. 동일한 서비스를 사용하는 1 대 이상의 클라이언트가 잠금 기능을 지원하지 않기 때문에 재앙이 발생할 수 있습니다. 트랜잭션조차 여기에서 도움이되지 않습니다 (또는 롤백은 매우 똑똑해야합니다 :-)). – Qwerty