Статьи

Lua как язык распределенного рабочего процесса

В последнее время я трачу много времени на размышления о способах организации долгосрочных рабочих процессов в сервис-ориентированной архитектуре. Я говорил об этом на Брайтонской ALT NET в прошлый вторник,   когда  Джей Каннан , разработчик игр, среди прочего упомянул, что  Lua  является популярным выбором для сценариев игровых платформ. Может быть, я должен проверить это. Так я и сделал. И это оказывается очень интересным.

Если вы еще не слышали о Lua, это «мощный, быстрый, легкий, встраиваемый язык сценариев», изначально задуманный командой из католического университета Рио-де-Жанейро в Бразилии. Это ведущий язык сценариев для игровых платформ, который также появляется в других интересных местах, включая Photoshop и  Wikipedia . У него есть простой C API, который делает относительно простым вызов p-invoke из .NET, и действительно есть   библиотека LuaInterface, которая предоставляет управляемый API.

Я получил исходный код из  репозитория кода Google svn  и собрал его в VS 2012, но есть также и пакеты NuGet.

Оказалось, что использовать Lua для написания распределенного рабочего процесса очень просто. Lua имеет  сопрограммы первого класса,  что означает, что вы можете приостановить и продолжить скрипт Lua по своему желанию. Библиотека LuaInterface позволяет вам вводить функции C # и вызывать их как функции Lua, так что это просто случай вызова асинхронной функции C # ‘begin’, приостановки скрипта путем выдачи сопрограммы, ожидания возврата асинхронной функции, установки возвращаемого значения и снова запустить скрипт.

Позвольте мне показать вам, как.

Сначала вот небольшой скрипт Lua:

a = 5
b = 6

print('doing remote add ...')

r1 = remoteAdd(a, b)

print('doing remote multiply ...')

r2 = remoteMultiply(r1, 4)

print('doing remote divide ...')

r3 = remoteDivide(r2, 2)

print(r3)

Три функции «remoteAdd», «remoteMultiply» и «remoteDivide» являются асинхронными. За кулисами сообщение отправляется через RabbitMQ на удаленный сервер OperationServer, где выполняется расчет и возвращается сообщение.

Скрипт работает в моем классе LuaRuntime. Это создает и настраивает среду Lua, в которой выполняется скрипт:

public class LuaRuntime : IDisposable
{
private readonly Lua lua = new Lua();
private readonly Functions functions = new Functions();

public LuaRuntime()
{
lua.RegisterFunction("print", functions, typeof(Functions).GetMethod("Print"));
lua.RegisterFunction("startOperation", this, GetType().GetMethod("StartOperation"));

lua.DoString(
@"
function remoteAdd(a, b) return remoteOperation(a, b, '+'); end
function remoteMultiply(a, b) return remoteOperation(a, b, '*'); end
function remoteDivide(a, b) return remoteOperation(a, b, '/'); end

function remoteOperation(a, b, op)
startOperation(a, b, op)
local cor = coroutine.running()
coroutine.yield(cor)

return LUA_RUNTIME_OPERATION_RESULT
end
");
}

public void StartOperation(int a, int b, string operation)
{
functions.RunOperation(a, b, operation, result =>
{
lua["LUA_RUNTIME_OPERATION_RESULT"] = result;
lua.DoString("coroutine.resume(co)");
});
}

public void Execute(string script)
{
const string coroutineWrapper =
@"co = coroutine.create(function()
{0}
end)";
lua.DoString(string.Format(coroutineWrapper, script));
lua.DoString("coroutine.resume(co)");
}

public void Dispose()
{
lua.Dispose();
functions.Dispose();
}
}

Когда этот класс создается, он создает новую среду LuaInterface (класс Lua) и новый экземпляр класса Functions, который я объясню ниже.

Конструктор — это место, где происходит большинство интересных настроек. Сначала мы регистрируем две функции C #, которые мы хотим вызвать из Lua: «print», которая просто печатает с консоли, и «startOperation», которая запускает асинхронную математическую операцию.

Затем мы определяем наши три функции: «remoteAdd», «remoteMultiply» и «remoteDivide», которые в свою очередь вызывают общую функцию «remoteOperation». RemoteOperation вызывает зарегистрированную функцию C # ‘startOperation’, а затем выдает текущую сопрограмму. Фактически сценарий остановится здесь, пока не будет запущен снова После его запуска, результат асинхронной операции доступен из переменной LUA_RUNTIME_OPERATION_RESULT и возвращается вызывающей стороне.

Функция C # StartOperation вызывает RunOperation для нашего класса Functions, который имеет асинхронный обратный вызов. В обратном вызове мы устанавливаем значение результата в среде Lua и выполняем «coroutine.resume», который перезапускает скрипт Lua с того места, где он был получен.

Функция Execute фактически запускает скрипт. Сначала он встраивает его в вызов coroutine.create, чтобы весь скрипт создавался как сопрограмма, затем просто запускает сопрограмму, вызывая coroutine.resume.

Класс Functions является просто оболочкой для функции, которая поддерживает соединение  EasyNetQ  с RabbitMQ и отправляет запрос EasyNetQ на удаленный сервер где-нибудь еще в сети.

public class Functions : IDisposable
{
private readonly IBus bus;

public Functions()
{
bus = RabbitHutch.CreateBus("host=localhost");
}

public void Dispose()
{
bus.Dispose();
}

public void RunOperation(int a, int b, string operation, Action<int> resultCallback)
{
using (var channel = bus.OpenPublishChannel())
{
var request = new OperationRequest()
{
A = a,
B = b,
Operation = operation
};
channel.Request<OperationRequest, OperationResponse>(request, response =>
{
Console.WriteLine("Got response {0}", response.Result);
resultCallback(response.Result);
});
}
}

public void Print(string msg)
{
Console.WriteLine("LUA> {0}", msg);
}
}

Вот пример запуска скрипта:
DEBUG: Trying to connect
DEBUG: OnConnected event fired
INFO: Connected to RabbitMQ. Broker: 'localhost', Port: 5672, VHost: '/'
LUA> doing remote add ...
DEBUG: Declared Consumer. queue='easynetq.response.143441ff-3635-4d5d-8e42-6b379b3f8356', prefetchcount=50
DEBUG: Published to exchange: 'easy_net_q_rpc', routing key: 'Mike_DistributedLua_Messages_OperationRequest:Mike_DistributedLua_Messages', correlationId: '50560dd9-2be1-49a1-96f6-9c62641080ae'
DEBUG: Recieved
RoutingKey: 'easynetq.response.143441ff-3635-4d5d-8e42-6b379b3f8356'
CorrelationId: '50560dd9-2be1-49a1-96f6-9c62641080ae'
ConsumerTag: '101343d9-9497-4893-88e6-b89cc1de29a4'
Got response 11
LUA> doing remote multiply ...
DEBUG: Declared Consumer. queue='easynetq.response.f571f6d7-b963-4a88-bf62-f05785009e39', prefetchcount=50
DEBUG: Published to exchange: 'easy_net_q_rpc', routing key: 'Mike_DistributedLua_Messages_OperationRequest:Mike_DistributedLua_Messages', correlationId: '0ea7e1c3-6f12-4cb9-a861-2f5de8f2600d'
DEBUG: Model Shutdown for queue: 'easynetq.response.143441ff-3635-4d5d-8e42-6b379b3f8356'
DEBUG: Recieved
RoutingKey: 'easynetq.response.f571f6d7-b963-4a88-bf62-f05785009e39'
CorrelationId: '0ea7e1c3-6f12-4cb9-a861-2f5de8f2600d'
ConsumerTag: '2c35f24e-7745-4475-885a-d214a1446a70'
Got response 44
LUA> doing remote divide ...
DEBUG: Declared Consumer. queue='easynetq.response.060f7882-685c-4b00-a930-aa4f20f7c057', prefetchcount=50
DEBUG: Published to exchange: 'easy_net_q_rpc', routing key: 'Mike_DistributedLua_Messages_OperationRequest:Mike_DistributedLua_Messages', correlationId: 'ea9a90cc-cd7d-4f05-b171-c6849026ac4a'
DEBUG: Model Shutdown for queue: 'easynetq.response.f571f6d7-b963-4a88-bf62-f05785009e39'
DEBUG: Recieved
RoutingKey: 'easynetq.response.060f7882-685c-4b00-a930-aa4f20f7c057'
CorrelationId: 'ea9a90cc-cd7d-4f05-b171-c6849026ac4a'
ConsumerTag: '90e6b024-c5c4-440a-abdf-cb9a000c131c'
Got response 22
LUA> 22
DEBUG: Model Shutdown for queue: 'easynetq.response.060f7882-685c-4b00-a930-aa4f20f7c057'
Completed
DEBUG: Connection disposed

Вы можете видеть, что операторы печати Lua чередуются с инструкциями EasyNetQ DEBUG, показывающими публикуемые и потребляемые сообщения.

Итак, все готово, механизм сценариев распределенного рабочего процесса содержит менее 100 строк кода.
Все, что мне теперь нужно сделать, это сериализовать среду Lua при каждом выходе и затем перезапустить ее снова из ее сериализованного состояния. Это возможно согласно небольшому поиску в Google вчера днем. Смотреть это пространство.

Вы можете найти код для всего этого на GitHub здесь: