Meteor.setTimeout和Meteor.methods之间的并发性 [英] Concurrency between Meteor.setTimeout and Meteor.methods

查看:137
本文介绍了Meteor.setTimeout和Meteor.methods之间的并发性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的Meteor应用程序中实现一个基于turnbased的多人游戏服务器,客户端通过发布/订阅接收游戏状态,并可以调用Meteor方法 sendTurn 发送回合数据(他们不能直接更新游戏状态集合)。

  var endRound = function(gameRound){
//检查gameRound是否已经结束/
//如果循环结果已经确定
// - > yes:
do nothing
// - > no:
//确定轮次结果
//更新集合
//创建下一个gameRound
};

Meteor.methods({
sendTurn:function(turnParams){
//查找gameRound数据
//验证turnParams对gameRound
//存储(更新gameRound集合对象)
//让所有客户轮流发送本轮?
//是 - >调用endRound
// no - > ;等待其他客户端发送转弯
}
});

要实施时间限制,我想等待一段时间调用 sendTurn ),然后确定循环结果 - 但只有当循环结果尚未在 sendTurn



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



方法来实现这将是调用 Meteor.setTimeout(endRound,< roundTimeLimit>)



问题:




  • 我假设我应该在 sendTurn endRound (?)中同步更新集合(无回调)消除种族条件? (阅读对这个SO问题关于同步数据库操作也会产生,我怀疑)


  • 在这方面,每个请求在我的上下文中的 Meteor文档(函数 endRound 通过客户端方法调用和/或在服务器 setTimeout )中调用?


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



  • 在多服务器/集群环境中,


解决方案

伟大的问题,它比它看起来更棘手。首先,我想指出,我已经在下面的repos中实现了这个问题的解决方案:


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




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




  • 当所有客户端都发送了他们的动作时,运行 endRound

  • 每个回合都有一个计时器,如果它到期,自动运行 endRound >
  • endRound 必须每轮执行一次,无论客户端是什么



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




  • 每个客户端可以有一个未完成的方法到服务器(除非在方法中调用 this.unblock())。




b

这意味着每当一个方法调用进行yield操作时,Node或数据库中的值都可以改变。这可能会导致以下潜在的竞争条件(这些只是我已经修复,但可能有其他):




  • 一个双玩家游戏,例如,两个客户在同一时间调用 sendTurn 。两者都称为屈服操作以存储转弯数据。两种方法然后检查2个玩家是否轮流发送,找到肯定的,然后 endRound 运行两次。

  • 调用 sendTurn 右键作为循环超时。在这种情况下, endRound 由超时和播放器的方法调用,导致再次运行两次。

  • 问题可能导致饥饿, endRound 永远不会被调用。






  • 由于只有一个Fiber实际上可以一次更改Node中的值,如果你不调用yield操作,你可以保证避免可能的竞争条件。所以你可以缓存内存中的状态,而不是在数据库中。

  • endRound 代码移到外部方法调用自身,使用别的东西来触发它。这是我采取的方法,确保只有定时器或最终播放器触发回合的结束,而不是两者(参见此处使用 observeChanges )。

  • 在集群环境中,您只能使用数据库进行同步,可能需要使用条件更新操作和原子操作符。类似如下:

      var currentVal; 

    while(true){
    currentVal = Foo.findOne(id).val; // yield

    if(Foo.update({_ id:id,val:currentVal},{$ inc:{val:1}})> 0){
    // Operation按预期运行
    //(您的代码在这里,例如endRound)
    break;
    }
    else {
    //检测到比赛条件,请重试
    }
    }




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



你也可能想看到这个计时器代码以了解其他一些想法。我将把它扩展到您描述的完整设置,一旦我有一些时间。


In my Meteor application to implement a turnbased multiplayer game server, the clients receive the game state via publish/subscribe, and can call a Meteor method sendTurn to send turn data to the server (they cannot update the game state collection directly).

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

To implement a time limit, I want to wait for a certain time period (to give clients time to call sendTurn), and then determine the round result - but only if the round result has not already been determined in sendTurn.

How should I implement this time limit on the server?

My naive approach to implement this would be to call Meteor.setTimeout(endRound, <roundTimeLimit>).

Questions:

  • What about concurrency? I assume I should update collections synchronously (without callbacks) in sendTurn and endRound (?), but would this be enough to eliminate race conditions? (Reading the 4th comment on the accepted answer to this SO question about synchronous database operations also yielding, I doubt that)

  • In that regard, what does "per request" mean in the Meteor docs in my context (the function endRound called by a client method call and/or in server setTimeout)?

    In Meteor, your server code runs in a single thread per request, not in the asynchronous callback style typical of Node.

  • In a multi-server / clustered environment, (how) would this work?

解决方案

Great question, and it's trickier than it looks. First off I'd like to point out that I've implemented a solution to this exact problem in the following repos:

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

To summarize, the problem basically has the following properties:

  • Each client sends in some action on each round (you call this sendTurn)
  • When all clients have sent in their actions, run endRound
  • Each round has a timer that, if it expires, automatically runs endRound anyway
  • endRound must execute exactly once per round regardless of what clients do

Now, consider the properties of Meteor that we have to deal with:

  • Each client can have exactly one outstanding method to the server at a time (unless this.unblock() is called inside a method). Following methods wait for the first.
  • All timeout and database operations on the server can yield to other fibers

This means that whenever a method call goes through a yielding operation, values in Node or the database can change. This can lead to the following potential race conditions (these are just the ones I've fixed, but there may be others):

  • In a 2-player game, for example, two clients call sendTurn at exactly same time. Both call a yielding operation to store the turn data. Both methods then check whether 2 players have sent in their turns, finding the affirmative, and then endRound gets run twice.
  • A player calls sendTurn right as the round times out. In that case, endRound is called by both the timeout and the player's method, resulting running twice again.
  • Incorrect fixes to the above problems can result in starvation where endRound never gets called.

You can approach this problem in several ways, either synchronizing in Node or in the database.

  • Since only one Fiber can actually change values in Node at a time, if you don't call a yielding operation you are guaranteed to avoid possible race conditions. So you can cache things like the turn states in memory instead of in the database. However, this requires that the caching is done correctly and doesn't carry over to clustered environments.
  • Move the endRound code outside of the method call itself, using something else to trigger it. This is the approach I've taken which ensures that only the timer or the final player triggers the end of the round, not both (see here for an implementation using observeChanges).
  • In a clustered environment you will have to synchronize using only the database, probably with conditional update operations and atomic operators. Something like the following:

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

The above approach is primitive and probably results in bad database performance under high loads; it also doesn't handle timers, but I'm sure with some thinking you can figure out how to extend it to work better.

You may also want to see this timers code for some other ideas. I'm going to extend it to the full setting that you described once I have some time.

这篇关于Meteor.setTimeout和Meteor.methods之间的并发性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆