Escribir X-docker-isolation-provider es difícil, pero no imposible / Sudo Null IT News

¿Alguna vez te has sentido pionero? Esto es exactamente lo que sentí cuando escribí docker-isolation-provider para la plataforma de programación asociativa profunda.

Fue así: un buen día en nuestro plataforma de comunicaciones decidido: sería bueno portar nuestro bot V Profundo. Y para esto tenias que escribir asi llamados proveedores.

Los proveedores solo se necesitan para un propósito: permitir al usuario ejecutar controladores personalizados en cualquier idioma. Entonces pensé que sería bueno ayudar a chicos que probablemente nunca se han oxidado en sus vidas. no vieron. Aquí está la lista en npm: https://www.npmjs.com/search?q=docker-isolation-provider

Oh, que equivocado estaba entonces…

Etapa 1: Negación

Como referencia, utilicé un ya preparado. Proveedor de JavaScript. Creé el proveedor de acuerdo con los chicos de Deep, pero sin ningún comentario específico. Me pidieron un código más corto, así que no agregué nada extra, simplemente lo reescribí al estilo del proveedor JS. Incluso tuve que comprar sandía. guión oxidado

Aquí está el enlace a la solicitud de extracción: https://github.com/deep-foundation/rust-docker-isolation-provider/pull/1

Para comprender por qué este código en particular, es necesario comprender qué debe poder hacer exactamente para que se le llame sagrado. X-docker-isolation-provider. Para empezar, fui a mirar el ya hecho. proveedor JS

Este es el cartel que nos saluda en la página de GitHub. El código es básicamente el mismo.

imagen

imagen

Por supuesto que llama la atención y Estibador en el título, por archivo acoplable Está claro que la imagen simplemente se ejecuta en la máquina del usuario, asegurando la ejecución del código que se envía al punto final deseado.

Bueno, no hay nada más que descubrir aquí por ahora; el resto se puede encontrar directamente a través de los desarrolladores en su servidor de Discord. Pero para escribir en canales de desarrollador, al menos debes tener papel de cadete – OK hecho.

imagen

imagen

Etapa 2: ira

Cuando hice estas pequeñas ediciones, de alguna manera no me importó lo que había en el título. estibador. Pero ahora la pregunta seguía dando vueltas en mi cabeza: “¿Por qué? estibador?”.
¿Por qué no utilizar, por ejemplo, algunos ERA M tiempo de ejecución con un punto de entrada estandarizado, entonces no habría necesidad de crear cada proveedor desde cero. A lo que pronto recibí la respuesta: “esto es para el administrador del cliente”. Está bien, menos preguntas, pensé, sólo necesito terminar con el proveedor. Solo para seguir adelante necesitaba descubrir qué tan bien mi PR cumplía con los requisitos.

Inmediatamente quedó claro que nadie de Deep estaba particularmente interesado en el código en sí, sino sólo en su funcionalidad, así que me concentré en él.

En general, toda la lógica estaba en estas líneas y el resto era solo código del servidor web.

fs::write(
    /* handler.rs */,
    format!(
        "fn main() -> Result<(), Box<dyn std::error::Error>> {{ \\
            let args = serde_json::from_str(r#\\"{raw}\\"#)?; \\
            {code}
            println!(\\"{{}}\\", serde_json::to_string(&main(args))?);
            Ok(())
        }}"
    ),
)?;

let out =
    Command::new("rust-script").arg("-d serde_json=1.0").arg(/* handler.rs */).output()?;

Donde la idea principal queda inmediatamente clara. El código del controlador simplemente se inserta en su lugar.{code}. Debe corresponder a este formato:

fn main(args: TYPE) -> RET {
    ...
}

En cambio TYPE cualquiera puede soportar impl Deserialize (transferido allí params desde el json llegado), y al lugar RET cualquier impl Serialize. Esto facilitó la aceptación de casi cualquier tipo sin esfuerzo, gracias a serde.

Aquí está una de las capturas de pantalla posteriores de Postman, pero transmite la idea.

img_1

img_1

Aquí tampoco hubo validación del código (y, en consecuencia, análisis sin sentido; toda la responsabilidad recaía en el compilador).

Por la misma razón, sólo se acepta un argumento (esto, por supuesto, se puede cambiar a macros procesalespero ¿por qué si funcionalidad serde resuelve este problema).

En general, este enfoque no fue muy apreciado y tuve que explicar durante mucho tiempo por qué es mejor que usar json dinámico a ciegas, como en el proveedor js.
Pero todavía quedaban muchas preguntas:

  • Si el proveedor no es para usuarios, ¿por qué necesita aislamiento y dockerización? Como mínimo, sería bueno permitir el uso del ajuste desde la máquina del usuario (si ya tiene Rust instalado)

  • por qué un formato tan extraño para los datos recibidos y devueltos (esto volverá a atormentarme más adelante)

  • y lo más extraño es que aprendí que no puedo simplemente devolver un resultado (en realidad puedo), porque resulta que solo puedo devolver una relación que se refiere a las creadas a través de DeepClient datos

Etapa 3: Negociación

En ese momento, ya había comenzado a reelaborar la lógica del proveedor, preparándola gradualmente para la expansión, haciendo dos cambios principales. Para empezar, moví la plantilla de código del controlador a un archivo separado, porque había crecido bastante en tamaño.

¿Por qué? Porque se necesitaba un sistema que permitiera regresar Result<T> (o no regresar).

Puedes ver esto con más detalle aquí.

Pero si lo miras brevemente, todo se reduce a este código:

// Deep просит результат в таком json формате
// { "resolved": ок }
// { "rejected": не ок }
#(derive(Serialize))
#(serde(rename_all = "lowercase"))
enum Respond<T, E> {
    Resolved(T),
    Rejected(E),
}

 #(derive(Serialize))
pub struct Infallible {} // просто метафора на то, что T сериализуется как Result<T, Infallible>
// можно было просто мануально реализовать пустую реализацию, но компилятор и без этого должен нормально соптимизировать

pub trait Responder {
    fn respond_to<S: Serializer>(self, serializer: S) -> Option<S::Error>;
    // where (на верочку, так как всё равно сериалайзер юзер передать не может)
    //     S: Serializer<Ok = ()>;
}

impl<T: Serialize, E: Serialize> Responder for Result<T, E> {
    fn respond_to<S: Serializer>(self, serializer: S) -> Option<S::Error> {
        match self {
            Ok(ok) => Respond::Resolved(ok),
            Err(err) => Respond::Rejected(err),
        }
        .serialize(serializer)
        .err()
    }
}

impl<T: Serialize> Responder for T {
    default fn respond_to<S: Serializer>(self, serializer: S) -> Option<S::Error> {
        // Как говорилось выше, просто сериализуем `T` как `Result<T, Infallible>`
        Respond::<_, Infallible>::Resolved(self).serialize(serializer).err()
    }
}

También agregué streaming de todo. stderr (incluidos errores de compilación y errores regulares) impresión electrónica) en /stream punto final y, en consecuencia, una pequeña macro que le permitiría convertir el código al formato requerido para probar el proveedor.

macro_rules! rusty {
    (($($pats:tt)*) $(-> $ty:ty)? { $body:expr } $(where $args:expr)? ) => {{
        fn __compile_check() {
             fn main($($pats)*) $(-> $ty)? { $body }
        }
        json::json!({
            "main": stringify!(
                fn main($($pats)*) $(-> $ty)? { $body }
            ),
            $("args": $args)?
        })
    }};
}

Agregar streaming, en principio, no contiene una parte lógica importante, es solo código de la documentación. Cohete:

https://rocket.rs/v0.5-rc/guide/responses/#async-streams

Pero todos estos cambios en la “calidad de vida” carecieron en absoluto de importancia, porque se trataba sólo de regateos.

El principal problema seguía siendo una cosa. Para implementar un proveedor, este debe poder crear conexiones a través de DeepClient (lo cual sinceramente no entendí, porque siempre asocié controladores con funciones lógicas que simplemente reciben y devuelven datos). Y me negué obstinadamente a crear un DeepClient para el proveedor Rust, porque tal (generado hasura) la oscuridad No lo he visto todavía. Y en general la idea es extraña.

El proveedor es un pequeño servidor cuya lógica se encuentra literalmente en un par de docenas de líneas de código. Pero no DeepClient, sólo hay toneladas de código monótono (por razones obvias). Por eso me esforcé tanto en convencer a los chicos de que definitivamente no lo necesitas.

En resumen, todos se mantuvieron firmes, justificando esto por la etapa inicial de desarrollo, pero al final acordaron un compromiso ridículo:
Estoy rehaciendo el proveedor para la compilación invisible para el usuario en WASM y agregando js! macro para (por extraño que parezca) inserciones js directamente en el controlador de Rust. \

Según la primera idea, debería haberse visto así:

pub async fn add(a: i32, b: i32) -> i32 {
  js!((a: i32, b: i32) -> i32 {
      return a + b;
  })
  .await
}

Mostré el código, este enfoque fue aprobado y comencé a implementarlo. Aunque vale la pena señalar que ahora la macro está muy fea, por alguna razón todas las capturas son explícitas, e incluso con indicación del tipo (afortunadamente esto cambiará más adelante), e incluso el javascript insertado siempre es asíncrono. Decidí no cambiar esto por ahora, ya que la funcionalidad principal sigue siendo el acceso al cliente.
Aquí aquí en discordia Puedes leer más sobre el proceso de desarrollo de este

Etapa 4: Depresión

El desarrollo de este plan fue el más triste. No solo eso Denoal que le asigné la función de tiempo de ejecución WASM, simplemente no se inició debido a la falta de soporte para él en cliente-apoloy al reemplazarlo con un nodo, aparecieron errores desagradables en las implementaciones módulos ESM.
En general, sufrí exactamente así, pero hice el trabajo.

Fue muy triste decir adiós a Rust-Script, porque ahora tenía que usarlo manualmente. paquete de basuray convierta el script en el nodo en el punto de entrada.

La lógica todavía estaba en un par de líneas más importantes:

// шаблоном теперь выступает целый готовый проект, который каждый раз копируется для нового хэндлера
fs_extra::dir::copy(env::current_dir()?.join("template"), &dir, &options())?;

let dir = dir.join("template");
fs::write(dir.join("src/lib.rs"), expand(TEMPLATE, ("#{main}", &code)))?;

macro_rules! troo {
    // этот макрос вызывает указанную программу с нужными аргументами,
    // которые можно удобно передать как просто строками, так и переменными
}

let _ = troo! { "wasm-pack" => "build" "--target" "nodejs" "--dev" dir };
let _ = troo! { "npm" => "install" "-g" "@deep-foundation/deeplinks" };

let out = troo! {
    "node" => dir.join("mod.mjs") data.get()
};
Ok(String::from_utf8(out)?)

Los detalles de implementación se distribuyen convenientemente en dos confirmaciones:

https://github.com/deep-foundation/rust-docker-isolation-provider/pull/8/commits/5983efb8dea3a62c3106e825427391ec4f29f3b2

Pero esto no es lo más triste. Lo peor fue cambiar constantemente de proveedor para adaptarse estándar imaginario.

Además, esto no se hizo todo de una vez; los propios desarrolladores de Deep aún no los conocen, por lo que tuvieron que hacer todo durante las pruebas en el entorno profundo.

Éstos son algunos de ellos:

  • El puerto en Docker no se especifica a través de p XXXX:YYYYy mediante p XXXX:XXXX -e PORT=XXXX – frío, la variable no es difícil de contar

  • Los datos se envían al controlador en el formato { "params": ... } del cual me olvidé por completo – está bien, envuelto

  • Resulta en "rejected": ... puede haber no solo un error de usuario, sino también un error de compilación (etc.) – ok, revisemos el sistema

  • También insistieron en que DeepClient se pasó inmediatamente listo al proveedor, lo que no rompió muy bien el sistema actual, ya que debería haber permitido realizar pruebas sin una instancia de cliente.
    Por lo tanto, originalmente se transmitió precisamente jwt: Option<String> y luego el cliente fue creado por el usuario a través de la función deep.
    Parecería: reemplace un poco la lógica (tal como lo hice yo aquí) y ese es el final. Pero luego surge inmediatamente el problema de la especialización incompleta en Rust (no hay implementación para JsValue Serialize). En general, la solución es la esperada: solo necesita bifurcar el repositorio. wasm-bidngen y implementar las cosas que necesitamos allí mismo.
    Y la abundancia de tales cosas destruyó todos mis planes para una pequeña y hermosa implementación del proveedor.

  • También insistieron en que los argumentos del controlador, que simplemente acepté en orden, ahora se pasaran como una estructura.

    // было (да, теперь в хэндлере не функция, а лямбда)
    async |((a, b), deep): ((i32, i32), _)| -> i32
    // стало
    |Ctx { data: (a, b), deep, .. }: Ctx<(i32, i32)>| -> i32

También durante este período implementé varios cambios más extensos en la calidad de vida:

  • Tuvimos que agregar el almacenamiento en caché desde cero, cuya lógica cambió dos veces. Al principio era una implementación regular basada en esto. caja. Luego, además de almacenar en caché controladores idénticos (por cierto, esta también es una parte divertida de la interfaz del proveedor, ella misma debe recordar los mismos controladores), tuvimos que almacenar en caché el ensamblado y sus dependencias. Aquí ya tuvimos que confiar en las herramientas de construcción del propio Rust, es decir espacio de trabajo'ы.
    Es decir, de hecho, todos los controladores compilados están esperando mientras uno de ellos compila las dependencias para todos los demás.

  • Finalmente también implementé dependencias adecuadas. Ya que también los proporcioné antes rust-script. Ahora he implementado un analizador simple basado en (winnow)(https://crates.io/crates/winnow)que a su vez se basa en (nom)( (sin embargo, más tarde lo reemplacé con chumskyya que tuve una experiencia positiva al usarlo, y sus errores de la caja es algo con algo)
    No hay nada inusual en el código. Esta extensión de sintaxis se parece a esto:

    where cargo: {
      // код отсюда отправится прямо в `Cargo.toml` хэндлера
    }
    
    // остальной код хэндлера

    Para los curiosos, el código está oculto.
    aquí: https://github.com/deep-foundation/rust-docker-isolation-provider/blob/main/src/parse.rs

    Etapa 5: Aceptación

Entonces, hemos llegado a las últimas líneas de nuestra historia sobre el camino hacia la creación de un proveedor dentro de Deep. Deep ahora tiene un proveedor en funcionamiento que le permite ejecutar scripts personalizados en crecimiento. Y una vez más me convencí de que es mejor escribir código que solo te guste a ti en tu propio Github.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *