Enzo Soares
Publicado em 16 de Abril de 2025 às 17:53
Ahoy!
O plano de hoje é aumentarmos as funcionalidades do nosso servidor. Tenho dois objetivos:
Vamos lá!
O conceito é simples: deve haver uma forma de passar valores e referências para uso de todos os callbacks. Um exemplo de uso seria uma conexão com um banco de dados ou cache, que deve ser definida uma vez ao se iniciar o servidor, e depois pode ser utilizada por todos os callbacks.
Primeiro, vamos alterar a definição do nosso servidor, para que ele seja na verdade uma função que recebe como argumento o tipo do contexto que será disponibilizado, retornando uma struct que tem um campo do mesmo tipo:
pub fn Server(comptime AppContext: type) type {
return struct {
allocator: *mem.Allocator,
config: Config = .{},
callback: callback_t(AppContext),
app_context: *AppContext,
const Self = @This();
pub fn init(allocator: *mem.Allocator, config: Config, callback: callback_t(AppContext), app_context: *AppContext) Self {
return Self{
.allocator = allocator,
.config = config,
.callback = callback,
.app_context = app_context,
};
}
pub fn start(self: Self) !void {
try libuv(AppContext).libuv_execute(self.allocator, self.config, self.callback, self.app_context);
}
};
}
Da mesma forma, vamos alterar a forma como a struct Context é definida:
pub fn context_t(comptime AppContext: type) type {
return struct {
allocator: *std.mem.Allocator,
callback: callback_t(AppContext),
buffer_allocator: *buffer_pool_t,
request_allocator: *request_pool_t,
response_allocator: *response_pool_t,
client_allocator: *client_pool_t,
write_req_allocator: *write_req_pool_t,
app_context: *AppContext
};
}
Agora, para mudar o módulo libuv:
pub fn libuv(comptime AppContext: type) type {
const Context = context_t(AppContext);
return struct {
// outras funções
pub fn libuv_execute(allocator: *mem.Allocator, config: Config, callback: callback_t(AppContext), app_context: *AppContext) !void {
const ctx: *Context = try allocator.*.create(Context);
ctx.*.allocator = allocator;
ctx.*.callback = callback;
ctx.*.app_context = app_context;
// só alterei a assinatura da função para receber um ponteiro para AppContext
}
}
}
Depois, devemos mudar o tipo callback_t para que os callbacks possam receber uma instância de AppContext:
pub fn callback_t(comptime AppContext: type) type {
return * const fn (mem.Allocator, *AppContext, *Request, *Response) anyerror!void;
}
Imagine só, você precisa executar uma escrita grande no banco de dados. Simplesmente fazer uma requisição vai funcionar, mas vai segurar a thread atual até que a operação de I/O se conclua. Vamos implementar uma abstração para que esse trabalho não bloqueie a thread principal.
O primeiro passo é definir que existem dois tipos de tarefas assíncronas possíveis: onde devemos esperar o resultado para dar seguimento à execução da função (bloqueantes) e onde não precisamos esperar (não bloqueantes):
pub const thread_callback_t = ?* const fn(?*anyopaque) callconv(.C) void;
fn act_blocking(cb: thread_callback_t, context: ?*anyopaque) void {
var thread: uv.uv_thread_t = undefined;
_ = uv.uv_thread_create(&thread, cb, context);
_ = uv.uv_thread_join(&thread);
}
fn act_non_blocking(cb: thread_callback_t, context: ?*anyopaque) void {
var thread: uv.uv_thread_t = undefined;
_ = uv.uv_thread_create(&thread, cb, context);
_ = uv.uv_thread_detach(&thread);
}
Ao chamar act_blocking ou act_non_blocking, a função de retorno será chamada com context com argumento em uma thread diferente. No caso act_blocking, a thread atual vai entrar no estado de espera.
Deve se tomar muito cuidado para que as funções de retorno das threads não deem erro, ou todo o servidor pode cair.