草庐IT

php - 设计/架构问题 : rollbacks with remote services

coder 2024-04-05 原文

例如,有以下调用的远程 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。

提前致谢!

最佳答案

<?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.
}

关于php - 设计/架构问题 : rollbacks with remote services,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2263417/

有关php - 设计/架构问题 : rollbacks with remote services的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

  3. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  4. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  5. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  6. ruby - Fast-stemmer 安装问题 - 2

    由于fast-stemmer的问题,我很难安装我想要的任何ruby​​gem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=

  7. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

    当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

  8. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  9. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  10. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

随机推荐