Servidor HTTP - Parte 7

Aumentando as funcionalidades

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:

  1. Permitir acesso a um contexto global
  2. Permitir o usuário desenvolvedor acesso às capacidades assíncronas do servidor.

Vamos lá!

Contexto global

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;
}

Trabalhos assíncronos

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.