Meteor.setTimeout和Meteor.methods之间的并发

时间:2015-10-07 08:36:47

标签: node.js multithreading mongodb meteor concurrency

在我的Meteor应用程序中实现基于转向的多人游戏服务器,客户端通过发布/订阅接收游戏状态,并可以调用Meteor方法sendTurn将转弯数据发送到服务器(他们无法更新游戏国家集合直接)。

var endRound = function(gameRound) {
    // check if gameRound has already ended / 
    // if round results have already been determined
    //     --> yes: 
                  do nothing
    //     --> no:
    //            determine round results
    //            update collection
    //            create next gameRound
};

Meteor.methods({
    sendTurn: function(turnParams) {
        // find gameRound data 
        // validate turnParams against gameRound
        // store turn (update "gameRound" collection object) 
        // have all clients sent in turns for this round?
        //      yes --> call "endRound"
        //      no  --> wait for other clients to send turns
    }
});

要实现时间限制,我想等待一段时间(让客户有时间调用sendTurn),然后确定舍入结果 - 但仅在圆形结果尚未确定时在sendTurn

我应该如何在服务器上实现此时间限制?

我实施此方法的天真方法是致电Meteor.setTimeout(endRound, <roundTimeLimit>)

问题:

  • 并发性怎么样?我假设我应该在sendTurnendRound(?)中同步更新集合(没有回调),但这是否足以消除竞争条件? (阅读关于同步数据库操作的this SO question的已接受答案的第4条评论也产生,我怀疑)

  • 在这方面,“每个请求”在我的上下文中的Meteor docs中意味着什么(由客户端方法调用和/或服务器{{1}调用的函数endRound })?

      

    在Meteor中,您的服务器代码在每个请求的单个线程中运行,而不是以Node的典型异步回调样式运行。

  • 在多服务器/集群环境中,(如何)这会起作用?

1 个答案:

答案 0 :(得分:3)

很棒的问题,它看起来比它看起来更棘手。首先,我想指出我已经在以下回购中实施了这个问题的解决方案:

  

https://github.com/ldworkin/meteor-prisoners-dilemma   https://github.com/HarvardEconCS/turkserver-meteor

总而言之,该问题基本上具有以下属性:

  • 每个客户在每轮发送一些操作(您称之为sendTurn
  • 当所有客户都发送了他们的操作后,请运行endRound
  • 每轮都有一个计时器,如果它到期,自动运行endRound
  • endRound必须每轮执行一次,无论客户做什么

现在,考虑我们必须处理的Meteor的属性:

  • 每个客户端一次只能有一个未完成的服务器方法(除非在方法中调用this.unblock())。以下方法等待第一个。
  • 服务器上的所有超时和数据库操作都可以产生其他光纤

这意味着只要方法调用经过一个让步操作,Node或数据库中的值就会发生变化。这可能导致以下潜在的竞争条件(这些只是我已经修复过的,但可能还有其他竞争条件):

  • 例如,在2人游戏中,两个客户在同一时间呼叫sendTurn。两者都调用一个屈服操作来存储转弯数据。然后两种方法检查2个玩家是否已经轮流发送,发现是肯定的,然后endRound运行两次。
  • 玩家在轮次超时时调用sendTurn。在这种情况下,超时和玩家的方法都会调用endRound,导致再次运行两次。
  • 对上述问题的错误修正可能导致饥饿,endRound永远不会被调用。

您可以通过多种方式解决此问题,无论是在Node中还是在数据库中进行同步。

  • 由于一次只有一个光纤可以实际更改节点中的值,如果您没有调用屈服操作,则可以保证避免可能的竞争条件。因此,您可以在内存中而不是在数据库中缓存转向状态之类的内容。但是,这要求缓存正确完成,并且不会延续到群集环境。
  • endRound代码移到方法调用本身之外,使用其他东西来触发它。这是我采取的方法,确保只有计时器或最终玩家触发回合结束,而不是两者(使用observeChanges参见here)。
  • 在集群环境中,您必须仅使用数据库进行同步,可能需要使用条件更新操作和原子操作符。如下所示:

    var currentVal;
    
    while(true) {
      currentVal = Foo.findOne(id).val; // yields
    
      if( Foo.update({_id: id, val: currentVal}, {$inc: {val: 1}}) > 0 ) {
        // Operation went as expected
        // (your code here, e.g. endRound)
        break;
      }
      else {
        // Race condition detected, try again
      }
    } 
    

上述方法是原始的,可能导致高负载下的数据库性能不佳;它也没有处理计时器,但我确信你可以想出如何扩展它以更好地工作。

您可能还希望看到此timers code其他一些想法。我有时间将它扩展到你描述的完整设置。