Redução do tempo de resposta em uploads de arquivos HTTP em Go: implementação de uma fila de workers assíncronos baseada em canais

Visão geral
Ao aprender Go pela primeira vez, é comum aprendê-la implementando um servidor backend. Nesse caso, vamos considerar como exemplo a criação de um exemplo em que um arquivo é recebido como stream por meio de uma RestAPI e enviado ao servidor. O servidor net/http da linguagem Go processa, por padrão, várias requisições simultaneamente, portanto não há problema no próprio upload simultâneo e em grande quantidade. No entanto, se todas as ações após o recebimento do stream forem processadas de forma síncrona, a resposta do endpoint será atrasada. Vamos conhecer uma técnica para evitar essa situação.
Causa
Receber o stream geralmente também leva bastante tempo e, no caso de arquivos grandes, uma única requisição pode levar vários minutos para ser processada. Nesses casos, é importante processar as ações posteriores ao recebimento o mais rápido possível. O cenário deste exemplo é o de receber o stream, salvá-lo em um arquivo temporário e enviá-lo para um contêiner. Nesse caso, se a parte que envia o arquivo temporário ao contêiner for tratada por um pool de workers, é possível reduzir o atraso da resposta.
1package file_upload
2
3import (
4 "fmt"
5 "io"
6 "log"
7 "net/http"
8 "os"
9 "path/filepath"
10 "time"
11)
12
13const uploadTempDir = "/tmp/incus_uploads" // Diretório temporário do host
14
15// UploadTask mantém os dados para o envio assíncrono do arquivo.
16type UploadTask struct {
17 HostTempFilePath string
18 ContainerName string
19 HostFilename string
20 ContainerDestinationPath string
21}
22
23// UploadHandler processa uploads de arquivos. Salva em arquivo temporário e, em seguida, enfileira para o envio ao Incus.
24func UploadHandler(wr http.ResponseWriter, req *http.Request) {
25 if req.Method != http.MethodPost {
26 http.Error(wr, "POST method required.", http.StatusMethodNotAllowed)
27 return
28 }
29 originalFilePath := req.Header.Get("X-File-Path")
30 originalFilename := filepath.Base(req.Header.Get("X-Host-Path"))
31 containerName := req.Header.Get("X-Container-Name")
32 if originalFilePath == "" || containerName == "" {
33 http.Error(wr, "Missing X-File-Path or X-Container-Name header.", http.StatusBadRequest)
34 return
35 }
36
37 cleanContainerDestPath := filepath.Clean(originalFilePath)
38 if !filepath.IsAbs(cleanContainerDestPath) {
39 http.Error(wr, "File path must be absolute.", http.StatusBadRequest)
40 return
41 }
42
43 // Cria um caminho de arquivo temporário único no host
44 tempFileName := fmt.Sprintf("%d-%s", time.Now().UnixNano(), filepath.Base(originalFilePath))
45 hostTempFilePath := filepath.Join(uploadTempDir, tempFileName)
46
47 if err := os.MkdirAll(uploadTempDir, 0755); err != nil {
48 log.Printf("ERROR: Failed to create temp upload directory: %v", err)
49 http.Error(wr, "Server error.", http.StatusInternalServerError)
50 return
51 }
52
53 // Cria o arquivo temporário e copia o corpo da requisição para ele (síncrono)
54 outFile, err := os.Create(hostTempFilePath)
55 if err != nil {
56 log.Printf("ERROR: Failed to create temporary file: %v", err)
57 http.Error(wr, "Server error.", http.StatusInternalServerError)
58 return
59 }
60 defer outFile.Close()
61
62 bytesWritten, err := io.Copy(outFile, req.Body)
63 if err != nil {
64 outFile.Close()
65 os.Remove(hostTempFilePath)
66 log.Printf("ERROR: Failed to copy request body to temp file: %v", err)
67 http.Error(wr, "File transfer failed.", http.StatusInternalServerError)
68 return
69 }
70 log.Printf("Upload Info: Received %d bytes, saved to %s.", bytesWritten, hostTempFilePath)
71
72 // Enfileira a tarefa para o envio assíncrono ao Incus
73 task := UploadTask{
74 HostTempFilePath: hostTempFilePath,
75 ContainerName: containerName,
76 HostFilename : originalFilename,
77 ContainerDestinationPath: cleanContainerDestPath,
78 }
79 EnqueueTask(task) //THIS PART
80 log.Printf("Upload Info: Task enqueued for %s to %s.", originalFilePath, containerName)
81
82 // Envia imediatamente a resposta 202 Accepted
83 wr.WriteHeader(http.StatusAccepted)
84 fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
85}
Na parte marcada como THIS PART, você deve ter percebido que a tarefa é inserida em uma fila.
Agora, vejamos como a fila de tarefas funciona.
1package file_upload
2
3import (
4 "log"
5 "sync"
6)
7
8var taskQueue chan UploadTask
9var once sync.Once
10
11// InitWorkQueue inicializa a fila de tarefas em memória.
12func InitWorkQueue() {
13 once.Do(func() {
14 taskQueue = make(chan UploadTask, 100)
15 log.Println("Upload Info: Work queue initialized.")
16 })
17}
18
19// EnqueueTask adiciona uma UploadTask à fila.
20func EnqueueTask(task UploadTask) {
21 if taskQueue == nil {
22 log.Fatal("ERROR: Task queue not initialized.")
23 }
24 taskQueue <- task
25 log.Printf("Upload Info: Queue: Task enqueued. Size: %d", len(taskQueue))
26}
27
28// DequeueTask recupera uma UploadTask da fila, bloqueando se estiver vazia.
29func DequeueTask() UploadTask {
30 if taskQueue == nil {
31 log.Fatal("ERROR: Task queue not initialized.")
32 }
33 task := <-taskQueue
34 log.Printf("Upload Info: Queue: Task dequeued. Size: %d", len(taskQueue))
35 return task
36}
37
38// GetQueueLength retorna o tamanho atual da fila.
39func GetQueueLength() int {
40 if taskQueue == nil {
41 return 0
42 }
43 return len(taskQueue)
44}
A fila de tarefas fornecida como exemplo é implementada de forma simples. Essa fila possui uma estrutura básica que retira as tarefas enfileiradas do canal.
A seguir está o método worker responsável por enviar o arquivo ao contêiner após o upload. Para boa responsividade e implementação simples, o método atual utiliza um loop infinito, mas nada impede que sejam adicionados algoritmos conforme a finalidade.
1func StartFilePushWorker() {
2 for {
3 task := DequeueTask()
4 log.Printf("Upload Info: Worker processing task for %s from %s.", task.ContainerName, task.HostFilename)
5
6 // Agenda a limpeza do arquivo temporário
7 defer func(filePath string) {
8 if err := os.Remove(filePath); err != nil {
9 log.Printf("ERROR: Worker: Failed to remove temp file '%s': %v", filePath, err)
10 } else {
11 log.Printf("Upload Info: Worker: Cleaned up temp file: %s", filePath)
12 }
13 }(task.HostTempFilePath)
14
15 // Processa a tarefa com novas tentativas para erros transitórios do Incus
16 for i := 0; i <= MaxRetries; i++ {
17 err := processUploadTask(task) //separate upload task
18 if err == nil {
19 log.Printf("SUCCESS: Worker: Task completed for %s.", task.ContainerName)
20 break
21 }
22
23 isTransient := true
24 if err.Error() == "incus: container not found" { // Exemplo de erro permanente
25 isTransient = false
26 }
27
28 if isTransient && i < MaxRetries {
29 log.Printf("WARNING: Worker: Task failed for %s (attempt %d/%d): %v. Retrying.",
30 task.ContainerName, i+1, MaxRetries, err)
31 time.Sleep(RetryDelay)
32 } else {
33 log.Printf("ERROR: Worker: Task permanently failed for %s after %d attempts: %v.",
34 task.ContainerName, i+1, err)
35 break
36 }
37 }
38 }
39}
Primeiramente, esta função continua executando em loop, tentando obter as tarefas da fila. Em seguida, dentro do limite de novas tentativas, ela executa a etapa de upload do arquivo temporário para o contêiner, e não do stream para o arquivo temporário.
Benefícios
A vantagem desse modo de processamento é que, desde que o upload do stream esteja normal, é possível reduzir o atraso das operações processadas posteriormente, além de prevenir o esgotamento de recursos causado por operações simultâneas e em grande quantidade nos contêineres. Como se vê no código atual, a quantidade de operações de contêiner que podem ocorrer simultaneamente é limitada pelo tamanho do canal. Dessa forma, apresentamos um exemplo prático que utiliza o paralelismo do Go. Para ver mais exemplos, acesse os links abaixo.Módulo com exemplos Projeto que utiliza os exemplos O projeto em si possui muitos componentes auxiliares, então, para estudar o próprio worker, basta observar brevemente como a função init do worker é chamada no main.go e seguir adiante. O módulo também contém outros tipos de worker, então consulte-os como referência.