sisoputnfrba / foro

Foro de consultas para el trabajo práctico
148 stars 7 forks source link

Duda sobre el handshake de los módulos en GO mediante el protocolo HTTP #4256

Closed ValenGriggio18 closed 1 month ago

ValenGriggio18 commented 1 month ago

🖋️ Descripción

Buenas, nosotros estamos teniendo algunas dudas sobre: 1- Hasta donde concretamente tenemos que llegar en el primer checkpoint, 2- Algo que nos ocurre con un grupo de funciones que manejan las requests y los endpoints http, arranco: En primer lugar, nosotros estamos interpretando que el objetivo de este checkpoint es que los 4 modulos funcionen como API´s que un cliente (en este caso alguno de los otros modulos) debe consumir. Esto lo hacemos literal utilizando el material quee nnos brinda la página de guía dee golang en la sección de "Protocolo HTTP". En nuestro caso, estamos haciendo que el cliente envíe un GET al endpoint del servidor manejado por la función "IniciarServidor()" (asi es como le llamamos nosotros) y ese GET le devuelva "Conexión ok con {modulo al que le haya enviado la petición}". Para levantar cada servidor, estamos usando obviamente la función que nos provee el paquete de http de golang que es ListenAndServe, es deecir hacemos que en todo los módulos haya un ListenAndServe porque se supone que en el tp los 4 módulos tienen que escucharse entre ellos y recibir sus peticiones mutuamente. Nuestro problema es que una vez que leevantamos los servidores con el ListenAndServe, si luego queremos que nuestro servidor previamente levantado ahora pasee a actuar como cliente de otro módulo levantado, no podemos.

📚 Búsqueda en documentación/foros

No response

📄 Código relevante

package utils

import (
    "encoding/json"
    "fmt"
    "io"
    "log/slog"
    "net/http"
)

func HandshakeCliente(moduloNombre string, puerto int) error {

    cliente := &http.Client{}
    url := fmt.Sprintf("http://localhost:%d/handshake/%s", puerto, moduloNombre)
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return err
    }

    req.Header.Set("Content-Type", "application/json")
    respuesta, err := cliente.Do(req)
    if err != nil {
        return err
    }

    // Verificar el código de estado de la respuesta
    if respuesta.StatusCode != http.StatusOK {
        return err
    }

    bodyBytes, err := io.ReadAll(respuesta.Body)
    if err != nil {
        return err
    }

    slog.Info(string(bodyBytes))

    fmt.Println(string(bodyBytes))

    return nil

}

func IniciarServidor(w http.ResponseWriter, r *http.Request) {
    modulo := r.PathValue("modulo")

    respuesta, err := json.Marshal(fmt.Sprintf("Conexion Ok con %s", modulo))
    if err != nil {
        http.Error(w, "Error al codificar los datos como JSON", http.StatusInternalServerError)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(respuesta)
}
package main

import (
    "fmt"
    "log/slog"
    "net/http"
    "strconv"

    "github.com/sisoputnfrba/tp-golang/utils"
)

type Config struct {
    Port            int    `json:"port"`
    MemorySize      int    `json:"memory_size"`
    InstructionPath string `json:"instruction_path"`
    ResponseDelay   int    `json:"response_delay"`
    IPKernel        string `json:"ip_kernel"`
    PortKernel      int    `json:"port_kernel"`
    IPCPU           string `json:"ip_cpu"`
    PortCPU         int    `json:"port_cpu"`
    IPFilesystem    string `json:"ip_filesystem"`
    PortFilesystem  int    `json:"port_filesystem"`
    Scheme          string `json:"scheme"`
    SearchAlgorithm string `json:"search_algorithm"`
    Partitions      []int  `json:"partitions"`
    LogLevel        string `json:"log_level"`
}

func main() {
    // Config
    var config Config
    err := utils.IniciarConfiguracion("config.json", &config)
    if err != nil {
        fmt.Println("Error al cargar la configuración de Memoria:", err)
        return
    }

    // Logger
    slog.SetLogLoggerLevel(utils.LogLevels[config.LogLevel])
    utils.IniciarLogger()

    // Conexion Handshake
    fmt.Println("Servidor escuchando en: ", config.Port)
    http.HandleFunc("/handshake/{modulo}", utils.IniciarServidor)

    err = http.ListenAndServe(":"+strconv.Itoa(config.Port), nil)
    if err != nil {
        return
    }
    err = utils.HandshakeCliente("filesystem", config.PortFilesystem)
    if err != nil {
        return
    }

}
package main

import (
    "fmt"
    "log/slog"
    "net/http"
    "strconv"

    "github.com/sisoputnfrba/tp-golang/utils"
)

type Config struct {
    Port             int    `json:"port"`
    IPMemory         string `json:"ip_memory"`
    PortMemory       int    `json:"port_memory"`
    MountDir         string `json:"mount_dir"`
    BlockSize        int    `json:"block_size"`
    BlockCount       int    `json:"block_count"`
    BlockAccessDelay int    `json:"block_access_delay"`
    LogLevel         string `json:"log_level"`
}

func main() {
    // Config
    var config Config
    err := utils.IniciarConfiguracion("config.json", &config)
    if err != nil {
        fmt.Println("Error al cargar la configuración del FileSystem:", err)
        return
    }

    // Definir nivel de los logs
    slog.SetLogLoggerLevel(utils.LogLevels[config.LogLevel])
    utils.IniciarLogger()

    // Conexion
    fmt.Println("Servidor escuchando en: ", config.Port)
    http.HandleFunc("GET /handshake/{modulo}", utils.IniciarServidor)

    err = http.ListenAndServe(":"+strconv.Itoa(config.Port), nil)
    if err != nil{
        return
    }

}

Bueno voy a usar los módulos de memoria y filesystem como ejemplo para ver si me puedo hacer entender un poco mejor. Yo estoy parado en el punto inicial del tp, y pongamosle que quiero que mi primer handshake (que tengo entendido que tienen que ser secuenciales) ocurra entre memoria y filesystem. Para eso en el módulo de filesystem (tercer bloque de codigo copiado) hago un http.ListenAndServe con el puerto que aloja a filesystem. Esto se supone que nos deeja al filesystem escuchando, en el HandleFunc pusimos que la función que tiene que administrar el endpoint "/handshake/{modulo}" es la de IniciarServidor (pueden ver su lógica en el primer bloque de código copiado, pero es basicamente la logica que nos provee la cátedra en la página de Go). Bueno ahora quee tenemos el módulo de filesystem listo para recibir la petición y con un endpoint ya creado para que eesa peticiónn vaya, vamos al módulo de memoria (segundo bloque de código copiado). En memoria repetimos la lógica de levantar el servidor con eel ListenAndServe, solo que debajo de eeste ultimo, utilizamos la función "HandShakeCliente", hacia el módulo "filesystem" y hacia el puerto de filesystem (porfavor vean la función que eestá en el primer bloque de codigo). El problema que tenemos es que luego de que memoria es levantada, no llega nunca a enviar el handshake ya que se queda siempre esperando peticiones y bloquea el flujo principal de ejecución.

🐛 Cómo reproducir el error

PS C:\Users\Valen\OneDrive\Documentos\tp-2024-2c-BlackSisOps\filesystem> go run filesystem.go
Servidor escuchando en:  8006

Acá levantamos filesystem, y ahora queremos levantar memoria y que a su vez le eenvíe un handshake a filesystem:

PS C:\Users\Valen\OneDrive\Documentos\tp-2024-2c-BlackSisOps\memoria> go run memoria.go
Servidor escuchando en:  8002

Sin embargo solamente lo levanta y no nos envía el handshake, esto lo sabemos porque de enviar el handshake debería figurarnos un mensaje que diga "Conexion OK filesystem". Nuestra primera idea es obviamente invertir eel orden de ejecución, y que primero memoria envíe el handshake y luego se levante, y ahi si sucede lo quee nosotros queriamos:

PS C:\Users\Valen\OneDrive\Documentos\tp-2024-2c-BlackSisOps\memoria> go run memoria.go
Servidor escuchando en:  8002
"Conexion Ok con filesystem"

Nos dice conexion ok con filesystem y se queda escuchando. Nuestra pregunta es: ¿Esto está bien en líneas generales? porque si lo pensamos a futuro, suponiendo que memoria quiera enviarle montones de peticiones a filesystem, en nuestro código debeerían ir todas las peticiones y la última línea deberia ser la de levantar memoria? esto me suena raro, ya que además cada vez que levantemos memoria va a intentar enviar esos handshakes a filesystem y quizás filesystem ni siquiera está levantado, pero bueno creo que ese es otro tema, pero el punto es quee si, realiza eel handshake y se queda escuchando, sin embargo no nos cierra que la lógica usada sea la indicada, esto confirmen ustedes porfi. Por otro lado dijimos "Bueno, si poniendo primero el ListenAndServe se nos queda trabado el flujo de ejecución mandemoslo por un hilo y que quede escuchando en segundo plano", pero claro, si hacemos eso, va a mandar al servidor a levantarse a segundo plano y una vez que termine de enviar todas las peticiones para consumir a otros módulos, el programa finaliza, y no se queda escuchando memoria sinno que se cierra, y se esupone que queremos que quede eescuchando, no? Está bien que una vez quee realizó todas las peticiones siga escuchando? o lo que está bienn es que eescuche en segundo plano y luego una vez que envió todas las peticiones se cierre? en fin, creo que ya hice todas las preguntas y traté de exponerles de la meejor manera cómo funciona nuestro código y qué dudas tenemos, quizás si een algun lado me expresé mal o dije una burrada es porque todavía no termino de entender al 100% el leenguaje ya que es nuevo para mi, pero CREO que me hice entender. Dejo la foto de como nos queda la consola si mandamos el ListenAndServe mediante una goroutine y termino:

PS C:\Users\Valen\OneDrive\Documentos\tp-2024-2c-BlackSisOps\memoria> go run memoria.go
Servidor escuchando en:  8002
"Conexion Ok con filesystem"
PS C:\Users\Valen\OneDrive\Documentos\tp-2024-2c-BlackSisOps\memoria> 

💻 Logs

No response

📝 Normas del foro

RaniAgus commented 1 month ago

¡Buenas! Al igual que ocurre con el main() en C, cuando dicha función finaliza en Go el proceso completo también lo hace.

Entonces, revisando el código de FileSystem, para que el proceso se mantenga ejecutando debería haber algo que bloquee el hilo principal, y eso es exactamente lo que http.ListenAndServe hace en caso de levantar el servidor exitosamente. El manejo de errores existe únicamente por si ocurre un error al intentar iniciarlo. De hecho, siempre retorna un error no nil.

Entonces, desde el lado de Memoria, lo razonable sería primero hacer todo lo que el proceso necesita para iniciar y recién ahí exponer la API a posibles clientes:

    err = utils.HandshakeCliente("filesystem", config.PortFilesystem)
    if err != nil {
        panic(err)
    }
    panic(http.ListenAndServe(":"+strconv.Itoa(config.Port), nil))

TIP: panic(err) imprime un mensaje con el detalle del error y luego aborta la ejecución de todo el proceso. A diferencia de un simple return en el main(), tiene la ventaja de darnos un toque más de info que nos diga por qué finalizó.

Saludos

ValenGriggio18 commented 1 month ago

Buenass. Gracias por responder, tiene sentido lo que decís, nosotros capaz nos fuimos más allá pero consideeramos la posibilidad de qué pasaría si no solo memoria le quiere enviar un mensaje a filesystem sino que también filesystem quiere enviarle un mensaje a memoria, es decir, que estabamos conteemplando una conexión bidireccional de los módulos. En este caso si tanto en memoria como en filesystem uso la lógica:

err = utils.HandshakeCliente("filesystem", config.PortFilesystem)
    if err != nil {
        panic(err)
    }
    panic(http.ListenAndServe(":"+strconv.Itoa(config.Port), nil))

estaría enviando el handhsake desde memoria a filesystem sin que fileesystem eesté ready, ya que estará esperando para enviarle hanndshake a memoria sin quee memoria esté ready, es decir se produciría una especie de deadlock, por lo que no podríiamos ni levantar los servidores, cuando ejecutamos en terminal no sucede nada ya que no llegan ni a ejecutarse. Por eso decidimos que el handshake o mejor dicho la petición de memoria a filesystem o filesystem a memoria (o en realidad de cualquier módulo a cualquier módulo) tiene que ir por una rutina, para que así ejecute en paralelo al flujo principal del main() y este pueda llegar hasta la función que levanta el servidor. Después lo que nos pasaba era que sí, habíamos logrado que aunque ambos se enviaran un handhsake se pudieran levantar los servidores en cualquier orden, sin embargo el servidor quee primero levantabas se quedaba eescuchando y no lograba hacer handshake, ya que si por ejemplo levantabamos memoria primero el hilo en el que se eenviaba el handshake iba a morir ya que no se le puede enviar handshake a un servidor que todavía no estaba leevantado, pero como si bien el hilo moría la eejeecución principal seguía, quedaba escuchando y una vez que levantabamos el segundo sí que hacía el handshake con el primero que ya estaba en modo eescucha. Entonces se nos ocurrió crear un endpoint en cada uno de los módulos y dentro de la go routine del handhsake en el que se enviaba la petición en cada módulo hicimos un bucle infinito en el que constantemente pregunta si el status del endpoint es Ok, y si es Ok significa que eel seervidor al que ebusca enviarle la petición está en modo escucha y ahí la envía y sale del bucle, mientras el status no sea Ok va a preguntarlo para siempre hasta que pueeda enviar el handshake. Una vez que hicimos esto sí, no importaba cuál de los dos módulos levantaramos primero, iba a queedar en escucha y el handshake (la petición de handshake) no se iba a enviar, pero una vez que levantaramos el segundo, ahi si que la petición de handhsake del primero see iba a enviar ya que estaba esperando que eel segundo estuviera en modo escucha (obviamente la petición de handshake del que levantamos segundo se enviaba al instante). Fuimos muy lejos con este razonamiento?

ValenGriggio18 commented 1 month ago

El código de filesystem queda así:

//creo eel endpoint que checkea si está levantado o no
    http.HandleFunc("/status", utils.CheckeoServidor)
    //tiro el hilo que hace envia la petición
    go func() {
        for {
            resp, err := http.Get("http://localhost:" + strconv.Itoa(config.PortMemory) + "/status")
            if err == nil && resp.StatusCode == http.StatusOK {
                err = utils.EnviarPeticion("memoria", config.PortMemory)
                if err != nil {
                    fmt.Println("Error al realizar el handshake con Memoria:", err)
                }
                break
            }

        }
    }()

    // Levanto el servidor
    fmt.Println("Servidor escuchando en: ", config.Port)
    http.HandleFunc("GET /handshake/{modulo}", utils.CrearEndPoint)

    err = http.ListenAndServe(":"+strconv.Itoa(config.Port), nil)
    if err != nil {
        return
    }

El de memoria así:

http.HandleFunc("/status", utils.CheckeoServidor)
    go func() {
        for {
            resp, err := http.Get("http://localhost:" + strconv.Itoa(config.PortFilesystem) + "/status")
            if err == nil && resp.StatusCode == http.StatusOK {
                err = utils.EnviarPeticion("memoria", config.PortFilesystem)
                if err != nil {
                    fmt.Println("Error al realizar el handshake con Memoria:", err)
                }
                break
            }

        }
    }()

    // Configuro servidor
    fmt.Println("Servidor escuchando en: ", config.Port)
    http.HandleFunc("/handshake/{modulo}", utils.CrearEndPoint)

    err = http.ListenAndServe(":"+strconv.Itoa(config.Port), nil)
    if err != nil {
        return
    }

Y la función con la que checkeamos así:

func CheckeoServidor(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
}
ValenGriggio18 commented 1 month ago

Bueno al final encontramos una manera de hacerlo y que todos se pueedan conectar con todos sin importar el orden, era mas o menos como lo habiamos planteado pero con una diferencia de een qué parte mandabamos el hilo.

iago64 commented 1 month ago

Buenas! Cómo va?

Ojo con tener hadrcodeado localhost o 127.0.0.1, ya que la idea es que el TP luego se pruebe en varias PCs y si apuntan a localhost no les va a funcionar.

Saludos.-

RaniAgus commented 1 month ago

¡Buenas! Dado que mencionás el término "handshake" (que no se menciona en el enunciado ni en ninguna parte de la guía de golang), entiendo que habrán cursado anteriormente y, por ende, estamos mezclando conceptos de la comunicación por sockets TCP (que se utiliza para realizar el TP en C) con el protocolo HTTP (que van a utilizar para comunicar los procesos en Go).

El protocolo HTTP está situado a más alto nivel que TCP y viene a servir como una abstracción que, a cambio de restricciones más estrictas, nos permite establecer un contrato más simple y más sencillo de entender y cumplir.

Haciendo un paralelismo con sockets, un servidor se queda escuchando requests en un puerto (socket, bind, listen) y luego el ciclo de una sola HTTP request consiste en:

Hago énfasis en el último ítem porque la principal restricción que nos da el protocolo HTTP es que las conexiones se abren y cierran por petición, por ende HTTP no está pensado para establecer una conexión continua entre ambos módulos. Es simplemente 1 request, 1 response y se cerró.

Ya no existe un protocolo de comunicación customizado que deban desarrollar ustedes (como sí ocurría con los sockets, comenzando por el handshake, y demás), sino que el protocolo ya está definido por HTTP. Entonces, como bien menciona Nahue en https://github.com/sisoputnfrba/foro/issues/4251, a la hora de comunicarnos por HTTP ya dejamos de pensar en conexiones y sockets y en su lugar pensamos en en APIs, es decir, en la interfaz (del inglés Application Programming Interface) que exponen dichos módulos.

Cada endpoint que conforma la interfaz está compuesto por:

Una request con:

Y una response con:

En definitiva, por la simpleza del protocolo de comunicación, el escenario que planteás de "qué pasaría si no solo memoria le quiere enviar un mensaje a filesystem sino que también filesystem quiere enviarle un mensaje a memoria" ya está resuelto: el servidor siempre va a estar escuchando nuevas conexiones, simplemente alcanza con pasarle una nueva request al cliente y cuando ésta se ejecute va a hacer un nuevo connect- send - recv - close. De hecho hasta podrían crear un nuevo http.Client cada vez.

Pasando al enunciado, no va a ocurrir nunca una dependencia circular (o sea, que el módulo A requiera del módulo B para iniciar su servidor y viceversa). A lo sumo en algunos casos puede ser que un módulo le tenga que consultar a otro su configuración (un GET /config), pero, al igual que en C, siempre va a fluir en una dirección para que puedan levantar los módulos en un cierto orden.

Es por esto que inicialmente no vi problema en que envíen una request antes de iniciar su servidor, aunque creo que deberían preguntarse... ¿realmente Memoria necesita de FileSystem para arrancar?

Saludos

ValenGriggio18 commented 1 month ago

Perfecto, muchas gracias por la claridad, nos habíamos re pasado y pudimos entender mejor el checkpoint y terminó siendo más sencillo de lo que pensábamos.