Как обрабатывать условие гонки экспресс-запроса NodeJS

javascript #node.js #express #webserver

#javascript #node.js #экспресс #веб-сервер

Вопрос:

Допустим, у меня есть эта конечная точка на экспресс-сервере:

 app.get('/', async (req, res) => {
    var foo = await databaseGetFoo();
    if (foo == true) {
        foo = false;
        somethingThatShouldOnlyBeDoneOnce();
        await databaseSetFoo(foo);
    }

})
 

Я думаю, что это создает условие гонки, если конечная точка вызывается дважды одновременно?
Если да, то как я могу предотвратить возникновение этого условия гонки?

Комментарии:

1. somethingThatShouldOnlyBeDoneOnce Действительно ли нужно вызывать только один раз в жизненном цикле приложения или, например, оно выполняет что-то асинхронное, и вы просто не хотите, чтобы второй вызов выполнялся до тех пор, пока не будет выполнен первый?

2. Пример чрезмерно упрощен. Представьте себе, что пользователь должен иметь возможность добавлять только один комментарий к сообщению, а переменная foo имеет значение true, если им разрешено публиковать комментарии.

Ответ №1:

Хорошо, итак, основываясь на комментариях, я немного лучше понимаю, чего вы здесь хотите.

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

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

 let processingMap = {};

app.get('/', async (req, res, next) => {
    if (!processingMap[req.user.userId]) {
        // add the user to the processing map
        processingMap = {
            ...processingMap,
            [req.user.userId]: true
        };

        const hasUserAlreadySubmittedComment = await queryDBForCommentByUser(req.user.userId);

        if (!hasUserAlreadySubmittedComment) {
            // we now know we're the only comment in process
            // and the user hasn't previously submitted a comment,
            // so submit it now:

            await writeCommentToDB();

            delete processingMap[req.user.userId];

            res.send('Nice, comment submitted');
        } else {
            delete processingMap[req.user.userId];

            const err = new Error('Sorry, only one comment per user');
            err.statusCode = 400;

            next(err)
        }
    } else {
        delete processingMap[req.user.userId];

        const err = new Error('Request already in process for this user');
        err.statusCode = 400;

        next(err);
    }

})
 

Поскольку вставка в processingMap выполняется синхронно, и узел может выполнять только одно действие одновременно, первый запрос пользователя на этот обработчик маршрута будет по существу заблокирован для этого пользователя до тех пор, пока блокировка не будет снята, когда мы закончим обработку запроса.

НО … это наивное решение, и оно нарушает правила для 12-факторного приложения. В частности, правило 6, которое заключается в том, что ваши приложения должны быть процессами без состояния. Теперь мы ввели состояние в ваше приложение.

Если вы уверены, что будете запускать это только как один процесс, все в порядке. Однако, как только вы переходите к горизонтальному масштабированию путем развертывания нескольких узлов (любым способом — PM2, process.cluster узла, Docker, K8s и т. Д.), Вы получаете вышеупомянутое решение. Сервер узла 1 не имеет представления о локальном состоянии сервера узла 2, и поэтому несколько запросов, поступающих в разные экземпляры вашего многоузлового приложения, не могут совместно управлять состоянием карты обработки.

Более надежным решением было бы реализовать какую-то систему очередей, вероятно, используя отдельную часть инфраструктуры, такую как Redis. Таким образом, все ваши узлы могут использовать один и тот же экземпляр Redis для совместного использования состояния, и теперь вы можете масштабировать до многих, многих экземпляров вашего приложения, и все они могут обмениваться информацией.

На самом деле у меня нет всех подробностей о том, как именно это сделать, и в любом случае это выходит за рамки этого вопроса, но, надеюсь, я дал вам хотя бы одно решение и некоторое представление о том, о чем думать на более широком уровне.