В последнее время я трачу много времени на размышления о способах организации долгосрочных рабочих процессов в сервис-ориентированной архитектуре. Я говорил об этом на Брайтонской 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 здесь: