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).
在我的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
}
});
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
.
要实现时间限制,我想等待一段时间(给客户端调用sendTurn的时间),然后确定舍入结果 - 但仅限于在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>)
.
我实现这个的天真方法是调用Meteor.setTimeout(endRound,
Questions:
问题:
-
What about concurrency? I assume I should update collections synchronously (without callbacks) in
sendTurn
andendRound
(?), 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)并发性怎么样?我假设我应该在sendTurn和endRound(?)中同步更新集合(没有回调),但这是否足以消除竞争条件? (阅读关于同步数据库操作的SO问题的接受答案的第4条评论也产生了,我怀疑)
-
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 serversetTimeout
)?在这方面,“每个请求”在我的上下文中的Meteor文档中意味着什么(函数endRound由客户端方法调用和/或服务器setTimeout调用)?
In Meteor, your server code runs in a single thread per request, not in the asynchronous callback style typical of Node.
在Meteor中,您的服务器代码在每个请求的单个线程中运行,而不是以Node的典型异步回调样式运行。
-
In a multi-server / clustered environment, (how) would this work?
在多服务器/集群环境中,(如何)这将起作用?
1 个解决方案
#1
3
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-*ers-dilemma https://github.com/HarvardEconCS/turkserver-meteor
https://github.com/ldworkin/meteor-*ers-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
) - 每个客户端在每一轮发送一些动作(你称之为sendTurn)
- When all clients have sent in their actions, run
endRound
- 当所有客户端都发送了他们的操作时,运行endRound
- Each round has a timer that, if it expires, automatically runs
endRound
anyway - 每一轮都有一个计时器,如果它到期,无论如何都会自动运行endRound
-
endRound
must execute exactly once per round regardless of what clients do - 无论客户端做什么,endRound必须每轮执行一次
Now, consider the properties of Meteor that we have to deal with:
现在,考虑我们必须处理的Meteor的属性:
- 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. - 每个客户端一次只能有一个未完成的服务器方法(除非在方法内调用this.unblock())。以下方法等待第一个。
- 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):
这意味着每当方法调用通过一个让步操作时,Node或数据库中的值都可以更改。这可能会导致以下潜在的竞争条件(这些只是我修复过的,但可能还有其他条件):
- 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 thenendRound
gets run twice. - 例如,在2人游戏中,两个客户端在同一时间调用sendTurn。两者都调用一个屈服操作来存储转弯数据。然后两种方法检查2个玩家是否已经轮流发送,发现是肯定的,然后endRound运行两次。
- 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. - 玩家在轮次超时时调用sendTurn。在这种情况下,endRound由超时和播放器的方法调用,从而再次运行两次。
- Incorrect fixes to the above problems can result in starvation where
endRound
never gets called. - 对上述问题的不正确修复可能导致饥饿,而endRound永远不会被调用。
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 usingobserveChanges
). - 将endRound代码移到方法调用本身之外,使用其他东西来触发它。这是我采取的方法,确保只有计时器或最终玩家触发回合结束,而不是两者(请参阅此处了解使用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.
您可能还希望查看此计时器代码以了解其他一些想法。一旦我有时间,我将把它扩展到你描述的完整设置。
#1
3
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-*ers-dilemma https://github.com/HarvardEconCS/turkserver-meteor
https://github.com/ldworkin/meteor-*ers-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
) - 每个客户端在每一轮发送一些动作(你称之为sendTurn)
- When all clients have sent in their actions, run
endRound
- 当所有客户端都发送了他们的操作时,运行endRound
- Each round has a timer that, if it expires, automatically runs
endRound
anyway - 每一轮都有一个计时器,如果它到期,无论如何都会自动运行endRound
-
endRound
must execute exactly once per round regardless of what clients do - 无论客户端做什么,endRound必须每轮执行一次
Now, consider the properties of Meteor that we have to deal with:
现在,考虑我们必须处理的Meteor的属性:
- 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. - 每个客户端一次只能有一个未完成的服务器方法(除非在方法内调用this.unblock())。以下方法等待第一个。
- 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):
这意味着每当方法调用通过一个让步操作时,Node或数据库中的值都可以更改。这可能会导致以下潜在的竞争条件(这些只是我修复过的,但可能还有其他条件):
- 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 thenendRound
gets run twice. - 例如,在2人游戏中,两个客户端在同一时间调用sendTurn。两者都调用一个屈服操作来存储转弯数据。然后两种方法检查2个玩家是否已经轮流发送,发现是肯定的,然后endRound运行两次。
- 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. - 玩家在轮次超时时调用sendTurn。在这种情况下,endRound由超时和播放器的方法调用,从而再次运行两次。
- Incorrect fixes to the above problems can result in starvation where
endRound
never gets called. - 对上述问题的不正确修复可能导致饥饿,而endRound永远不会被调用。
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 usingobserveChanges
). - 将endRound代码移到方法调用本身之外,使用其他东西来触发它。这是我采取的方法,确保只有计时器或最终玩家触发回合结束,而不是两者(请参阅此处了解使用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.
您可能还希望查看此计时器代码以了解其他一些想法。一旦我有时间,我将把它扩展到你描述的完整设置。