2015-10-07 2 views
4

В моем приложении Meteor для создания многопользовательского игрового сервера с разворотом клиентов клиенты получают состояние игры через публикацию/подписку и могут вызывать метод Meteor sendTurn для отправки данных об обращении на сервер (они не могут напрямую обновлять коллекцию состояний игры).Параллелизм между Meteor.setTimeout и Meteor.methods

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>).

Вопросы:

  • насчет параллелизм? Я предполагаю, что я должен обновлять коллекции синхронно (без обратных вызовов) в sendTurn и endRound (?), Но этого было бы достаточно для устранения условий гонки? (Чтение 4-й комментария о принятом ответе на this SO question о синхронных операциях с базами данных также приносят, я сомневаюсь, что)

  • В этой связи, что делает «по запросу» означает, в Meteor docs в моем контексте (функция endRound наречено вызов клиентского метода и/или на сервере setTimeout)?

    В Meteor код сервера работает в одном потоке для каждого запроса, а не в асинхронном обратном вызове, типичном для Node.

  • В среде с несколькими серверами/кластерами, (как) это будет работать?

ответ

3

Отличный вопрос, и это сложнее, чем кажется.Во-первых, я хотел бы отметить, что я реализовал решение этой конкретной проблемы в следующих РЕПО:

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

Итак, проблема в основном имеет следующие свойства :

  • Каждый клиент посылает в какие-то действия на каждом раунде (вы называете это sendTurn)
  • Когда все клиенты отправили в своих действиях, запустить endRound
  • Каждый раунд имеет таймер, который, если он истекает, автоматически запускается endRound все равно
  • endRound должен выполнить только один раз за раунд, независимо от того, что клиенты делают

сейчас , рассмотрим свойства Метеор, что мы имеем дело с:

  • Каждый клиент может иметь ровно один выдающийся метод на сервере в то время (если this.unblock() не вызывается внутри метода). Следующие методы ждут первого.
  • Все тайм-аут и операции с базой данных на сервере могут принести к другим волокнам

Это означает, что всякий раз, когда вызов методы проходит через операцию, получ, значение в узле или в базе данных может измениться. Это может привести к следующим возможным условиям гонки (это только те, которые я фиксированные, но может быть и другими):

  • В 2-х игроках игры, к примеру, два клиента называет sendTurn в точно таким же время. Оба вызова выполняют операцию удержания для хранения данных поворота. Оба метода затем проверяют, отправили ли 2 игрока по очереди, найдя утвердительный, а затем endRound запускается дважды.
  • Игрок звонит sendTurn прямо в круглые сроки. В этом случае endRound вызывается как таймаутом, так и методом игрока, в результате чего выполняется снова дважды.
  • Неправильные исправления вышеуказанных проблем могут привести к голоду, когда endRound никогда не вызывается.

Вы можете обратиться к этой проблеме несколькими способами, либо синхронизируя в узле, либо в базе данных.

  • Поскольку только одно волокно может фактически изменять значения в узле за раз, если вы не вызываете уступчивую операцию, вам гарантировано избежать возможных условий гонки. Таким образом, вы можете кэшировать такие вещи, как состояния очереди в памяти, а не в базе данных. Однако для этого требуется, чтобы кеширование было выполнено правильно и не переносится в кластерные среды.
  • Переместите код endRound вне вызова метода, используя что-то еще, чтобы вызвать его. Это подход, который я сделал, который гарантирует, что только таймер или конечный игрок запускает конец раунда, а не оба (см. here для реализации с использованием observeChanges).
  • В кластерной среде вам придется синхронизировать, используя только базу данных, возможно, с операциями условного обновления и атомными операторами. Что-то вроде следующего:

    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 для некоторых других идей. Я собираюсь расширить его до полной настройки, которую вы описали, когда у меня есть время.

+0

Очень хорошо написанное резюме и ответ - спасибо! – thomers

+1

Я нашел этот шаблон «Обновить документ, если текущий», описанный в документах mongo https://docs.mongodb.org/manual/tutorial/update-if-current/ Просто голова - «Изменено в версии 2.6: Метод db.collection.update() теперь возвращает объект WriteResult(), который содержит статус операции. Предыдущим версиям требовался дополнительный вызов метода db.getLastErrorObj(). " – thomers

+0

@thomers это действительно влияет на Meteor API или он отвлечен? –