Scripts utilisateur asynchrones dans Pure Rust sans frameworks et SMS

Bonjour, Habr!

Parfois, lors du développement de services réseau et d'interfaces utilisateur, il faut faire face à des scénarios d'interaction assez compliqués contenant des branches et des boucles. De tels scénarios ne rentrent pas dans une machine à états simple - il ne suffit pas de stocker toutes les données dans l'objet de session, il est également conseillé de suivre l'itinéraire du système pour entrer dans un état ou un autre, et dans certains cas, pouvoir revenir en arrière de quelques étapes, répéter le dialogue en boucle, etc. .d. Auparavant, à cette fin, vous deviez développer vos propres structures de données qui imitent la machine de pile, ou même utiliser des langages de script tiers. Avec l'avènement des capacités asynchrones dans presque tous les langages de programmation, il est devenu possible d'écrire des scripts dans le même langage dans lequel le service est écrit. Le script, avec sa propre pile et ses variables locales, est en fait une session utilisateur, c'est-à-dire qu'il stocke à la fois des données et un itinéraire. Par exemple, goroutine avec blocage de la lecture du canal résout facilement ce problème, mais d'une part, le thread vert n'est pas libre , et d'autre part, nous écrivons en Rust, où il n'y a pas de threads verts, mais il y a des générateurs et asynchrones / attendent .

Par exemple, nous allons écrire un simple http-bot qui affiche un formulaire html dans le navigateur, posant des questions à l'utilisateur jusqu'à ce qu'il réponde qu'il se sent bien. Le programme est un simple serveur http simple thread; nous écrivons le script bot sous la forme d'un générateur Rust. Permettez-moi de vous rappeler que les générateurs JavaScript permettent un échange de données bidirectionnel, c'est-à-dire qu'à l'intérieur du générateur, vous pouvez passer la question: my_generator.next (my_question);
et renvoyer la réponse: yield my_response;
Dans Rust, le transfert de valeurs à l'intérieur du générateur n'a pas encore été implémenté (la fonction resume () n'a pas de paramètres, bien qu'il y ait une discussion pour résoudre ce problème), nous organisons donc l'échange de données via une cellule partagée, dans laquelle se trouve la structure avec les données reçues et envoyées. Le script de notre bot est créé par la fonction create_scenario () , qui retourne une instance du générateur, essentiellement la fermeture dans laquelle le paramètre est déplacé - un pointeur vers la cellule de données udata . Pour chaque session utilisateur, nous stockons notre propre cellule avec des données et notre propre instance du générateur, avec son propre état de la pile et les valeurs des variables locales.

#[derive(Default, Clone)] struct UserData { sid: String, msg_in: String, msg_out: String, script: String, } type UserDataCell = Rc<RefCell<UserData>>; struct UserSession { udata: UserDataCell, scenario: Pin<Box<dyn Generator<Yield = (), Return = ()>>>, } type UserSessions = HashMap<String, UserSession>; fn create_scenario(udata: UserDataCell) -> impl Generator<Yield = (), Return = ()> { move || { let uname; let mut umood; udata.borrow_mut().msg_out = format!("Hi, what is you name ?"); yield (); uname = udata.borrow().msg_in.clone(); udata.borrow_mut().msg_out = format!("{}, how are you feeling ?", uname); yield (); 'not_ok: loop { umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, think carefully, maybe you're ok ?", uname); yield (); umood = udata.borrow().msg_in.clone(); if umood.to_lowercase() == "ok" { break 'not_ok; } udata.borrow_mut().msg_out = format!("{}, millions of people are starving, maybe you're ok ?", uname); yield (); } udata.borrow_mut().msg_out = format!("{}, good bye !", uname); return (); } } 

Chaque étape du script se compose d'actions simples - obtenez un lien vers le contenu de la cellule, enregistrez l'entrée utilisateur dans des variables locales, définissez le texte de la réponse et donnez le contrôle à l'extérieur via le rendement . Comme le montre le code, notre générateur renvoie un tuple vide (), et toutes les données sont transmises via une cellule commune avec un compteur de référence Ref <Cell <... >> . À l'intérieur du générateur, vous devez vous assurer que l'emprunt du contenu de la cellule borrow () ne franchit pas le seuil de rendement , sinon il sera impossible de mettre à jour les données de l'extérieur du générateur - par conséquent, vous ne pouvez malheureusement pas écrire une seule fois au début de l'algorithme laissez udata_mut = udata.borrow_mut () , et vous devez emprunter une valeur après chaque rendement.

Nous implémentons notre propre boucle d'événements (lecture à partir du socket), et pour chaque demande entrante, nous créons une nouvelle session utilisateur ou trouvons celle existante par Sid, en mettant à jour les données qu'elle contient:

 let mut udata: UserData = read_udata(&mut stream); let mut sid = udata.sid.clone(); let session; if sid == "" { //new session sid = rnd.gen::<u64>().to_string(); udata.sid = sid.clone(); let udata_cell = Rc::new(RefCell::new(udata)); sessions.insert( sid.clone(), UserSession { udata: udata_cell.clone(), scenario: Box::pin(create_scenario(udata_cell)), } ); session = sessions.get_mut(&sid).unwrap(); } else { match sessions.get_mut(&sid) { Some(s) => { session = s; session.udata.replace(udata); } None => { println!("unvalid sid: {}", &sid); continue; } } } 

Ensuite, nous transférons le contrôle à l'intérieur du générateur correspondant et nous mettons à jour les données mises à jour dans le socket. À la dernière étape, lorsque le script complet est terminé, nous supprimons la session de la table de hachage et masquons le champ de saisie de la page html à l'aide d'un script js.

 udata = match session.scenario.as_mut().resume() { GeneratorState::Yielded(_) => session.udata.borrow().clone(), GeneratorState::Complete(_) => { let mut ud = sessions.remove(&sid).unwrap().udata.borrow().clone(); ud.script = format!("document.getElementById('form').style.display = 'none'"); ud } }; write_udata(&udata, &mut stream); 

Code de travail complet ici:
github.com/epishman/habr_samples/blob/master/chatbot/main.rs

Je m'excuse pour l'analyse syntaxique http «ferme collective», qui ne prend même pas en charge la saisie cyrillique, mais tout se fait à l'aide d'outils de langage standard, sans frameworks, bibliothèques et sms. Je n'aime pas vraiment le clonage de chaînes, et le script lui-même n'a pas l'air assez compact en raison de l'utilisation intensive de borrow_mut () et clone () . Des rastamans probablement expérimentés pourront simplifier cela (par exemple, en utilisant des macros). L'essentiel est que le problème soit résolu par des moyens minimes, et j'espère que bientôt nous recevrons un ensemble complet d'outils asynchrones dans une version stable.

PS
Pour compiler, vous avez besoin d'une version nocturne:
 rustup default nightly rustup update 

Des camarades de l'anglais Stack Overflow m'ont aidé à gérer les générateurs:
stackoverflow.com/questions/56460206/how-can-i-transfer-some-values-into-a-rust-generator-at-each-step

Source: https://habr.com/ru/post/fr455010/


All Articles