GoSuda

Riduzione del ritardo di risposta nell'upload di file HTTP in Go: implementazione di una coda di worker asincroni basata su canali

By Yunjin Lee views ...

Panoramica

Quando si impara Go, capita spesso di farlo implementando un server backend. In questo caso, facciamo come esempio la creazione di un programma che riceve uno stream di file tramite una RestAPI o simili e lo carica sul server. Il server net/http del linguaggio Go gestisce per impostazione predefinita più richieste contemporaneamente, quindi di per sé il caricamento simultaneo e concorrente non presenta problemi. Tuttavia, se tutte le operazioni successive alla ricezione dello stream vengono eseguite in modo sincrono, la risposta dell'endpoint viene ritardata. Vediamo una tecnica per prevenire questa situazione.

Causa

Anche la ricezione di uno stream richiede generalmente molto tempo e, nel caso di file di grandi dimensioni, una singola richiesta può richiedere diversi minuti per essere elaborata. In questi casi è importante elaborare il più rapidamente possibile le operazioni successive alla ricezione. Lo scenario di questo esempio è quello di ricevere uno stream, salvarlo come file temporaneo e inviarlo a un container. A questo punto, se la parte che invia il file temporaneo al container viene gestita tramite un worker pool, è possibile ridurre il ritardo della risposta.

 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" // Directory temporanea sull'host
14
15// UploadTask contiene i dati per l'invio asincrono del file.
16type UploadTask struct {
17	HostTempFilePath         string
18	ContainerName            string
19    HostFilename             string
20	ContainerDestinationPath string
21}
22
23// UploadHandler elabora i caricamenti dei file. Salva in un file temporaneo, poi accoda per l'invio a 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	// Crea un percorso univoco per il file temporaneo sull'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	// Crea il file temporaneo e copia il corpo della richiesta in esso (sincrono)
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	// Accoda l'attività per l'invio asincrono a 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	// Invia immediatamente la risposta 202 Accepted
83	wr.WriteHeader(http.StatusAccepted)
84	fmt.Fprintf(wr, "File '%s' queued for processing on container '%s'.\n", originalFilename, containerName)
85}

Guardando la parte indicata con THIS PART, si sarà notato che qui l'attività viene inserita in una coda.

Vediamo ora come funziona la coda delle attività.

 1package file_upload
 2
 3import (
 4	"log"
 5	"sync"
 6)
 7
 8var taskQueue chan UploadTask
 9var once sync.Once
10
11// InitWorkQueue inizializza la coda delle attività in memoria.
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 aggiunge un UploadTask alla coda.
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 un UploadTask dalla coda, bloccandosi se è vuota.
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 restituisce la dimensione attuale della coda.
39func GetQueueLength() int {
40	if taskQueue == nil {
41		return 0
42	}
43	return len(taskQueue)
44}

La coda delle attività fornita come esempio è implementata in modo semplice. Questa coda ha una struttura elementare che estrae dal canale le attività accodate.

Di seguito è riportato il metodo worker per inviare il file al container dopo il caricamento. Il metodo attuale utilizza un ciclo infinito per garantire una buona reattività e un'implementazione semplice, ma è possibile aggiungere algoritmi in base all'uso previsto.

 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		// Pianifica la pulizia del file temporaneo
 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		// Elabora l'attività con nuovi tentativi per gli errori transitori di 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" { // Esempio di errore 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}

Innanzitutto, questa funzione continua a girare in un ciclo cercando di prelevare le attività dalla coda. Successivamente, entro l'intervallo dei tentativi, tenta la fase di caricamento dal file temporaneo al container, non dallo stream al file temporaneo.

Vantaggi

Il vantaggio di questo approccio è che, purché il caricamento dello stream vada a buon fine, è possibile ridurre il ritardo delle operazioni successive e prevenire l'esaurimento delle risorse causato da operazioni concorrenti sui container. Come si vede nel codice attuale, il numero di operazioni sui container che possono essere eseguite contemporaneamente è limitato dal numero dei canali. In questo modo abbiamo esaminato un esempio pratico di come utilizzare il parallelismo di Go. Per vedere più esempi, visitate i link seguenti.Modulo contenente esempi Progetto che utilizza gli esempi Il progetto stesso contiene molti componenti accessori, quindi per studiare il worker in sé è sufficiente vedere brevemente come viene richiamata la funzione init del worker da main.go. Il modulo include anche altri tipi di worker, quindi vi invitiamo a consultarli.