Конкурентность
Сервер многопоточный; ваш Lua - нет. Каждый обработчик каждого ресурса выполняется в одном рабочем потоке плагинов, по одному вызову за раз, а всё остальное, что предлагает API, существует, чтобы этот поток никогда не ждал. Эта страница - модель и правила; в справочнике Lua API есть сигнатура каждого вызова.
Один рабочий поток
Заголовок раздела «Один рабочий поток»Сетевые потоки принимают пакеты и ставят события в очередь; один рабочий поток разбирает очередь
и вызывает ваши обработчики. Таймеры, колбэки node.defer, возобновления корутин, доставки по
шине, завершения HTTP и заданий - всё приземляется в том же потоке. Два следствия:
- Вам никогда не нужна блокировка. Ни один обработчик не выполняется, пока выполняется другой, ни в вашем ресурсе, ни в любом другом, поэтому обычная таблица Lua - безопасное место для общего состояния.
- Блокировка блокирует всех. Обработчик, который крутится в цикле секунду, на эту секунду
останавливает обработчики всех остальных ресурсов, каждый таймер и
serverTick. Сервер за этим следит: каждый кусок кода плагина, который выполняет рабочий поток, - обработчики событий, шины и запросов, колбэки таймеров, отрезки корутиныnode.asyncмежду двумя приостановками, колбэки завершения HTTP, задач иnode.pg, приёмники лога - измеряется отдельно, и тот, что занял больше 250 мс, отмечается строкой с ресурсом и видом:plugin worker job stalled the thread for 699 ms (resource race, timer) — move heavy work to node.await/node.job. Отрезок, вложенный в другой (колбэк таймера, который запускает корутину, чей первый отрезок зависает), отмечается один раз - за внутренний. (До 1.2.1 измерялись только целые задания рабочего потока, так что медленный колбэк таймера или отрезок корутины оставался незамеченным, а строка не называла ресурс.) Когда очередь переполнена более чем 100 000 ожидающих заданий, новые события отбрасываются со строкойplugin job queue full (100000 jobs) -- dropping events (flood?).
Что выполняется в другом месте: фоновый пул (node.job, node.await, node.http), поток записи
файлов (node.fs.writeAsync) и сами сетевые потоки. Ни один из них никогда не трогает ваше
состояние Lua; результаты возвращаются в рабочий поток как простые данные и колбэк.
Таймеры
Заголовок раздела «Таймеры»node.after(ms, fn) -> idвыполняетfnодин раз черезmsмиллисекунд.node.every(ms, fn) -> idвыполняетfnкаждыеmsмиллисекунд до отмены. Медленныйfnзадерживает следующий запуск; запуски не накапливаются.node.cancel(id)отменяет любой из них; безопасен для идентификатора, который уже сработал.
Таймеры обслуживаются после каждой пачки обработчиков, поэтому таймер никогда не прерывает
обработчик; рабочий поток просыпается к ближайшему сроку, и serverTick срабатывает каждые 100 мс
рядом с ними. Таймер принадлежит ресурсу, который его поставил, и умирает при перезагрузке; при
остановке срабатывает serverShutdown, затем собственный resourceUnload("shutdown") каждого
ресурса, и таймеры больше не выполняются. Тот же хук срабатывает как resourceUnload("reload")
прямо перед тем, как перезагрузка сбросит состояние, - место, чтобы сохранить то, что накапливал
таймер; запись node.storage или обычный node.pg.exec (в форме с колбэком), сделанные там,
сохраняются, а колбэк, таймер или корутина, запущенные там, никогда не выполнятся
(Ресурсы).
local ticks = 0local intervalIdintervalId = node.every(60000, function() ticks = ticks + 1 node.log("minute %d, players: %d", ticks, node.players.count()) if ticks >= 60 then node.cancel(intervalId) endend)
node.after(500, function() node.log("half a second in, timer %d is ticking", intervalId)end)
-- expect: half a second in, timer \d+ is tickingПосле остальных: node.defer
Заголовок раздела «После остальных: node.defer»node.defer(fn) выполняет fn в рабочем потоке после того, как закончится текущая пачка
обработчиков. Это способ действовать, когда каждый другой обработчик события, в котором вы
находитесь, уже его увидел, - прелюдия использует его, чтобы забыть имя ушедшего игрока только
после того, как выполнился каждый обработчик playerLeft. Всё, что вы отправляете или меняете
внутри fn, происходит после события, а не во время него.
node.on("vehicleSpawned", function(vehicle) node.defer(function() if vehicle:exists() then -- another handler may have deleted it meanwhile vehicle:setTag("spawnedAt", tostring(node.server.unixTime())) node.log("%s tagged spawnedAt=%s", tostring(vehicle), vehicle:tag("spawnedAt")) end end)end)
-- expect: Vehicle#\d+ Alice tagged spawnedAt=\d+Ожидание без блокировки: node.async
Заголовок раздела «Ожидание без блокировки: node.async»node.async(fn, ...) выполняет fn(...) как кооперативную корутину в рабочем потоке и возвращает
идентификатор задачи. Внутри неё четыре вызова приостанавливают корутину, пока всё остальное
продолжает работать:
node.sleep(ms)- на заданное время.node.wait(msOrPredicate, intervalMs?)- число спит; функция приостанавливает до тех пор, пока не вернёт истину, с опросом каждыеintervalMs(по умолчанию 50).node.yield()- отдаёт рабочий поток на один ход внутри длинного цикла, который должен выполняться в нём. Справедливость, а не параллелизм.node.await(workFn, args?)- выполняет функцию в фоновом пуле и возобновляется с её результатом; см. ниже.node.http.fetchпостроен на той же идее.
Вне корутины эти вызовы падают: node.sleep called outside a node.async task в логе и ошибка Lua
«attempt to yield» в обработчике. Задача заканчивается, когда fn возвращает управление или
выбрасывает ошибку; ошибка пишется в лог как error in async task. Задачи сбрасываются при
перезагрузке и при остановке.
node.on("race:start", function(player, data) node.async(function() for i = 3, 1, -1 do node.broadcast("race:countdown", tostring(i)) node.sleep(1000) end node.broadcast("race:go") node.log("race started by %s", tostring(player)) end)end)
-- expect-client: Alice race:countdown 3-- expect-client: Alice race:go-- expect: race started by Player#\d+ AliceКорутина, которая спала, просыпается в изменившемся мире: игрок мог выйти, машина могла исчезнуть.
Перепроверяйте через player:isConnected() и vehicle:exists() после каждого node.sleep и
помните, что идентификаторы переиспользуются.
Настоящий параллелизм: node.job и node.await
Заголовок раздела «Настоящий параллелизм: node.job и node.await»Тяжёлым вычислениям не место в рабочем потоке. node.job(workFn, args?, doneFn) выполняет
workFn в потоке фонового пула в свежем, черновом состоянии Lua и вызывает doneFn(result, err)
в рабочем потоке, когда оно закончится. node.await(workFn, args?) - форма для корутин, только
внутри node.async: приостанавливается до завершения работы и возвращает result или nil, err.
Поскольку workFn выполняется в другом состоянии Lua, она должна быть самодостаточной: без
upvalue, без node, без глобальных переменных вашего ресурса - она сериализуется в байткод и
загружается в другом месте, а функция, которую нельзя выгрузить, падает со строкой
work function cannot be serialized (C function?). Она получает args и возвращает результат,
оба сериализуемые в JSON (таблицы, строки, числа, логические значения). Отдавайте ей копии того,
что ей нужно, и получайте обратно простые данные.
node.storage.set("scores", { { name = "Bob", time = 71.2 }, { name = "Alice", time = 64.9 } })
node.on("stats:request", function(player, data) node.async(function() local scores = node.storage.get("scores", {}) local top, err = node.await(function(args) table.sort(args.scores, function(a, b) return a.time < b.time end) local out = {} for i = 1, math.min(10, #args.scores) do out[i] = args.scores[i] end return out end, { scores = scores }) if not top then node.log.warn("ranking failed: %s", err) return end player:send("stats:top", top) node.log("%s leads with %.1f s", top[1].name, top[1].time) end)end)
-- expect: Alice leads with 64\.9 s-- expect-client: Alice stats:top \[\{В пуле по одному потоку на аппаратный поток, в пределах от 2 до 32; NODE_PLUGIN_POOL
переопределяет число. Его очередь вмещает 10 000 заданий; сверх того node.job возвращает
false, а node.await возобновляется с nil, "background pool is full".
node.http выполняет запросы в фоновом пуле и вызывает вас обратно в рабочем потоке.
node.http.request(method, url, opts?, cb)- общая форма (добавлена в сервере 1.2.0): любой метод -"GET","POST","PUT","PATCH","DELETE","HEAD"или свой токен, который понимает сервис; токен - только буквы A–Z, не более 16, и прелюдия сама переводит его в верхний регистр (всё остальное отвечает-1иinvalid HTTP method), - сopts.headers(таблица) иopts.body(таблица кодируется в JSON, строка отправляется как есть,nilне отправляет тела).cb(status, body, headers)выполняется в рабочем потоке.node.http.get(url, headers?, cb),node.http.post(url, body, headers?, cb),node.http.put(url, body?, headers?, cb),node.http.patch(url, body?, headers?, cb),node.http.delete(url, body?, headers?, cb)иnode.http.head(url, headers?, cb)- этоrequestс фиксированным методом; ответ наHEADнесёт статус и заголовки и пустое тело. Неудавшийся запрос вызывает колбэк со статусом-1и текстом ошибки вbody(resolve failed,connect failed,TLS handshake failed, …); каждая из них возвращаетfalseтолько тогда, когда запрос не удалось поставить в очередь, и тогдаcbне вызывается.headersв колбэке (и третье значениеfetch) - таблица с ключами в виде имён заголовков в нижнем регистре:headers["content-type"],headers["content-length"]. Заголовки запроса, которые вы отправляете, сохраняют тот регистр, в котором вы их написали (["Content-Type"] = "application/json"- нормально); имена заголовков ответа сервер приводит к нижнему регистру, так чтоheaders["Content-Type"]всегдаnil.node.http.fetch(url, opts?)- форма для корутин, только внутриnode.async:local status, body, headers = node.http.fetch(url, { method = "DELETE", body = t, headers = h }).opts.method- любой метод, который принимаетrequest(до 1.2.0 всё, кромеPOST, отправлялось какGET);statusравен0с телом"request not queued", если запрос не удалось поставить в очередь.
Примерно 15 с тайм-аута, предел тела 8 МБ, до пяти редиректов. Проверка TLS-сертификата
собеседника выключена, если хост не задал [Http] CaFile в server.toml
(Конфигурация): тогда каждый https://-запрос проверяется по
этому набору CA и по имени хоста, а сертификат, не прошедший проверку, - это -1, тело которого
начинается с TLS handshake failed (peer verification against [Http] CaFile), а редирект, который
увёл бы проверенный https://-запрос на обычный http://, не выполняется (-1,
redirect to plain http refused (verified request)). Без него не отправляйте секреты хостам,
которые вы не контролируете.
node.on("playerJoined", function(player) node.http.get("https://example.com/motd.txt", function(status, body) if status == 200 and player:isConnected() then player:tell(body) elseif status ~= 200 then node.log.warn("no message of the day: %s", status == -1 and body or ("HTTP " .. status)) end end)end)
-- expect: no message of the day: .+Файлы и хранилище вне рабочего потока
Заголовок раздела «Файлы и хранилище вне рабочего потока»node.fs.write синхронен: нормально для маленького файла, задержка для большого.
node.fs.writeAsync(path, data, cb?) передаёт байты потоку записи файлов и выполняет cb(ok) в
рабочем потоке, когда запись завершена; несколько записей в один путь до того, как поток до них
дойдёт, сворачиваются в последнюю. node.storage.set дописывает одну строку в журнал изменений
хранилища до возврата - цена значения, а не всего хранилища, - поэтому его можно вызывать внутри
обработчика; журнал сворачивается в снимок, когда перерастает его. Хранилище описано на странице
Ресурсы.
Правила
Заголовок раздела «Правила»- Держите обработчики короткими. Всё, что дольше нескольких миллисекунд, относится в
node.async(ожидание) илиnode.await/node.job(вычисления). - Никогда не ждите в цикле. Синхронных sleep или HTTP нет; используйте
node.sleep,node.waitиnode.http.fetchвнутриnode.async. - Не делите с фоновым заданием ничего, кроме JSON: оно выполняется в другом состоянии Lua и не
видит ни ваших таблиц, ни
node. - Когда вам нужны сразу многие машины, предпочитайте опрос геттера по таймеру потоковому событию
на 60 Гц:
node.vehicles.transforms()- одно чтение. - После любой приостановки перепроверяйте, что игрок и машина ещё существуют; идентификаторы переиспользуются.
- Всё, что вы регистрируете, умирает при перезагрузке; уже работающее задание или HTTP-запрос - нет, и его колбэк может застать мир изменившимся.
- Нативные модули следуют другому контракту - вызовы
NodeApiпотокобезопасны из любого потока, а фильтр ретрансляции и приёмник лога выполняются инлайн. См. Нативные модули.
- Справочник Lua API - вызовы таймеров и корутин с их сигнатурами.
- События - что вообще приходит в рабочий поток.
