Les acteurs simplifient la programmation multithread en évitant un état mutable partagé et partagé. Chaque acteur possède ses propres données qui ne sont visibles par personne. Les acteurs n'interagissent que par le biais de messages asynchrones. Par conséquent, les horreurs les plus terrifiantes du multithreading sous forme de races et de blocages lors de l'utilisation d'acteurs ne sont pas effrayantes (bien que les acteurs aient leurs problèmes, mais ce n'est plus le cas maintenant).
En général, l'écriture d'applications multi-thread à l'aide d'acteurs est facile et agréable. Y compris parce que les acteurs eux-mêmes s'écrivent facilement et naturellement. Vous pourriez même dire que l'écriture de code d'acteur est la partie la plus simple du travail. Mais quand l'acteur est écrit, une très bonne question se pose: "Comment vérifier l'exactitude de son travail?"
La question est vraiment très bonne. On nous demande régulièrement quand on parle d'acteurs en général et de
SObjectizer en particulier. Et jusqu'à récemment, nous ne pouvions répondre à cette question qu'en termes généraux.
Mais la
version 5.5.24 a été publiée , dans laquelle il y avait un support expérimental pour la possibilité de tests unitaires des acteurs. Et dans cet article, nous allons essayer de parler de ce que c'est, comment l'utiliser et avec quoi il a été implémenté.
À quoi ressemblent les tests d'acteurs?
Nous considérerons les nouvelles fonctionnalités de SObjectizer sur quelques exemples, en passant ce qui est quoi. Le code source des exemples discutés se trouve
dans ce référentiel .
Tout au long de l'histoire, les termes "acteur" et "agent" seront utilisés de manière interchangeable. Ils désignent la même chose, mais dans le SObjectizer, le terme «agent» est historiquement utilisé, par conséquent, le terme «agent» sera utilisé plus souvent.
L'exemple le plus simple avec Pinger et Ponger
L'exemple des acteurs Pinger et Ponger est probablement l'exemple le plus courant pour les frameworks d'acteurs. On peut dire un classique. Eh bien, si c'est le cas, commençons par les classiques.
Nous avons donc un agent Pinger qui, au début de son travail, envoie un message Ping à l'agent Ponger. Et l'agent Ponger renvoie un message Pong. Voici à quoi cela ressemble dans le code C ++:
Notre tâche consiste à écrire un test qui vérifierait qu'en enregistrant ces agents avec SObjectizer, Ponger recevra un message Ping et Pinger recevra un message Pong en réponse.
Ok Nous écrivons un tel test en utilisant le framework de test unitaire
doctest et obtenons:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include <doctest/doctest.h> #include <ping_pong/agents.hpp> #include <so_5/experimental/testing.hpp> namespace tests = so_5::experimental::testing; TEST_CASE( "ping_pong" ) { tests::testing_env_t sobj; pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>()); sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>()); sobj.scenario().run_for(std::chrono::milliseconds(100)); REQUIRE(tests::completed() == sobj.scenario().result()); }
Cela semble facile. Voyons ce qui se passe ici.
Tout d'abord, nous téléchargeons les descriptions des outils de support des tests d'agent:
#include <so_5/experimental/testing.hpp>
Tous ces outils sont décrits dans l'espace de noms so_5 :: experimental :: testing, mais afin de ne pas répéter un nom aussi long, nous introduisons un alias plus court et plus pratique:
namespace tests = so_5::experimental::testing;
Ce qui suit est une description d'un cas de test unique (et nous n'en avons pas besoin ici).
À l'intérieur du scénario de test, il y a plusieurs points clés.
Tout d'abord, il s'agit de la création et du lancement d'un environnement de test spécial pour SObjectizer:
tests::testing_env_t sobj;
Sans cet environnement, le «test run» pour les agents ne peut pas être terminé, mais nous en parlerons un peu plus tard.
La classe testing_env_t est très similaire à la classe wrapped_env_t dans SObjectizer. De la même manière, le SObjectizer démarre dans le constructeur et s'arrête dans le destructeur. Ainsi, lors de l'écriture de tests, vous n'avez pas à penser à démarrer et arrêter SObjectizer.
Ensuite, nous devons créer et enregistrer des agents Pinger et Ponger. Dans ce cas, nous devons utiliser ces agents pour déterminer ce que l'on appelle. "Scénario de test." Par conséquent, nous stockons séparément les pointeurs vers les agents:
pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); });
Et puis nous commençons à travailler avec le «scénario de test».
Un scénario de test est une pièce constituée d'une séquence directe d'étapes qui doivent être exécutées du début à la fin. L'expression «à partir d'une séquence directe» signifie que dans SObjectizer-5.5.24, les actions de script «fonctionnent» strictement séquentiellement, sans branchement ni boucle.
L'écriture d'un test pour les agents est la définition d'un script de test qui doit être exécuté. C'est-à-dire toutes les étapes du scénario de test doivent fonctionner, de la première à la dernière.
Par conséquent, dans notre scénario de test, nous définissons un scénario en deux étapes. La première étape vérifie que l'agent Ponger recevra et traitera le message Ping:
sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>());
La deuxième étape vérifie que l'agent Pinger reçoit un message Pong:
sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>());
Ces deux étapes sont tout à fait suffisantes pour notre cas de test, donc, après leur détermination, nous procédons à l'exécution du script. Nous exécutons le script et lui permettons de ne pas fonctionner plus de 100 ms:
sobj.scenario().run_for(std::chrono::milliseconds(100));
Une centaine de millisecondes devrait être plus que suffisante pour que les deux agents échangent des messages (même si le test est exécuté à l'intérieur d'une machine virtuelle très lente, comme c'est parfois le cas avec Travis CI). Eh bien, si nous avons fait une erreur en écrivant des agents ou décrit de manière incorrecte un script de test, alors attendre la fin d'un script erroné pendant plus de 100 ms n'a aucun sens.
Ainsi, après son retour de run_for (), notre script peut être terminé avec succès ou non. Par conséquent, nous vérifions simplement le résultat du script:
REQUIRE(tests::completed() == sobj.scenario().result());
Si le script ne s'est pas terminé correctement, cela entraînera l'échec de notre scénario de test.
Quelques clarifications et ajouts
Si nous exécutons ce code dans un SObjectizer normal:
pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); });
puis, très probablement, les agents Pinger et Ponger parviendraient à échanger des messages et à terminer leur travail avant de revenir d'introd_coop (les miracles du multithreading sont tels). Mais dans l'environnement de test, créé grâce à testing_env_t, cela ne se produit pas, les agents Pinger et Ponger attendent patiemment que nous exécutions notre script de test. Comment cela se produit-il?
Le fait est qu'à l'intérieur de l'environnement de test, les agents semblent être dans un état gelé. C'est-à-dire après l'enregistrement, ils sont présents dans SObjectizer, mais ils ne peuvent traiter aucun de leurs messages. Par conséquent, même so_evt_start () n'est pas appelé pour les agents avant l'exécution du script de test.
Lorsque nous exécutons le script de test à l'aide de la méthode run_for (), le script de test décongèle d'abord tous les agents gelés. Et puis le script commence à recevoir des notifications du SObjectizer sur ce qui arrive aux agents. Par exemple, que l'agent Ponger a reçu le message Ping et que l'agent Ponger a traité le message, mais ne l'a pas rejeté.
Lorsque de telles notifications commencent à arriver au script de test, le script essaie de les «essayer» jusqu'à la toute première étape. Donc, nous avons une notification que Ponger a reçu et traité Ping - est-ce intéressant pour nous ou non? Il s'avère que c'est intéressant, car la description de l'étape dit exactement cela: cela fonctionne lorsque Ponger réagit à Ping. Ce que nous voyons dans le code:
.when(*ponger & tests::reacts_to<ping>())
Ok Donc, la première étape a fonctionné, passez à l'étape suivante.
Vient ensuite une notification que l'agent Pinger a réagi à Pong. Et c'est exactement ce dont vous avez besoin pour que la deuxième étape fonctionne:
.when(*pinger & tests::reacts_to<pong>())
Ok Donc, la deuxième étape a fonctionné, avons-nous autre chose? Non. Cela signifie que l'intégralité du script de test est terminée et que vous pouvez retourner le contrôle à partir de run_for ().
Voici, en principe, comment fonctionne le script de test. En fait, tout est un peu plus compliqué, mais nous aborderons des aspects plus complexes lorsque nous considérerons un exemple plus complexe.
Exemples de philosophes de la restauration
Des exemples plus complexes d'agents de test peuvent être vus en résolvant la tâche bien connue "Les philosophes du dîner". Sur les acteurs, ce problème peut être résolu de plusieurs manières. Ensuite, nous considérerons la solution la plus triviale: les acteurs et les philosophes sont représentés sous la forme d'acteurs, pour lesquels les philosophes doivent se battre. Chaque philosophe réfléchit un moment, puis essaie de prendre la fourchette à gauche. Si cela réussit, il essaie de prendre la fourche à droite. Si cela réussit, le philosophe mange pendant un certain temps, après quoi il pose les fourchettes et commence à réfléchir. S'il n'était pas possible de prendre la fiche à droite (c'est-à-dire qu'elle a été prise par un autre philosophe), alors le philosophe renvoie la fiche à gauche et réfléchit encore un peu. C'est-à-dire ce n'est pas une bonne solution dans le sens où un philosophe peut mourir de faim trop longtemps. Mais alors c'est très simple. Et a la capacité de démontrer la capacité de tester des agents.
Les codes sources avec l'implémentation des agents Fork et Philosopher peuvent être trouvés
ici , dans l'article, nous ne les considérerons pas pour économiser de l'espace.
Test pour Fork
Le premier test pour les agents des philosophes de la restauration sera pour l'agent Fork.
Cet agent fonctionne selon un schéma simple. Il a deux états: libre et pris. Lorsque l'agent est à l'état Libre, il répond à un message Take. Dans ce cas, l'agent entre dans l'état Pris et répond avec un message de réponse Pris.
Lorsque l'agent est à l'état Pris, il répond différemment au message Take: l'état de l'agent ne change pas et Occupé est envoyé comme message de réponse. Toujours à l'état Pris, l'agent répond au message Put: l'agent revient à l'état Libre.
Dans l'état Libre, le message Put est ignoré.
Nous allons essayer de tester celui-ci au moyen du cas de test suivant:
TEST_CASE( "fork" ) { class pseudo_philosopher_t final : public so_5::agent_t { public: pseudo_philosopher_t(context_t ctx) : so_5::agent_t{std::move(ctx)} { so_subscribe_self() .event([](mhood_t<msg_taken>) {}) .event([](mhood_t<msg_busy>) {}); } }; tests::testing_env_t sobj; so_5::agent_t * fork{}; so_5::agent_t * philosopher{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { fork = coop.make_agent<fork_t>(); philosopher = coop.make_agent<pseudo_philosopher_t>(); }); sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>()); sobj.scenario().define_step("take_when_free") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>() & tests::store_state_name("fork"), *philosopher & tests::reacts_to<msg_taken>()); sobj.scenario().define_step("take_when_taken") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>(), *philosopher & tests::reacts_to<msg_busy>()); sobj.scenario().define_step("put_when_taken") .impact<msg_put>(*fork) .when( *fork & tests::reacts_to<msg_put>() & tests::store_state_name("fork")); sobj.scenario().run_for(std::chrono::milliseconds(100)); REQUIRE(tests::completed() == sobj.scenario().result()); REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork")); REQUIRE("free" == sobj.scenario().stored_state_name("put_when_taken", "fork")); }
Il y a beaucoup de code, nous allons donc le traiter par parties, en ignorant les fragments qui devraient déjà être clairs.
La première chose dont nous avons besoin ici est de remplacer le véritable agent philosophe. Un agent Fork doit recevoir des messages de quelqu'un et répondre à quelqu'un. Mais nous ne pouvons pas utiliser le vrai philosophe dans ce cas de test, car le véritable agent philosophe a sa propre logique de comportement, il envoie lui-même des messages et cette indépendance va nous interférer ici.
Par conséquent, nous nous
moquons , c'est-à-dire au lieu du vrai philosophe, nous allons lui en substituer un: un agent vide qui n'envoie rien lui-même, mais ne reçoit que les messages envoyés, sans aucun traitement utile. Voici le pseudo-philosophe implémenté dans le code:
class pseudo_philosopher_t final : public so_5::agent_t { public: pseudo_philosopher_t(context_t ctx) : so_5::agent_t{std::move(ctx)} { so_subscribe_self() .event([](mhood_t<msg_taken>) {}) .event([](mhood_t<msg_busy>) {}); } };
Ensuite, nous créons une collaboration entre l'agent Fork et l'agent PseudoPhilospher et commençons à déterminer le contenu de notre cas de test.
La première étape du script consiste à vérifier que Fork, étant à l'état Libre (et c'est son état initial), ne répond pas au message Put. Voici comment ce chèque est écrit:
sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>());
La première chose qui attire l'attention est la construction d'impact.
Elle est nécessaire car notre agent Fork ne fait rien lui-même, il ne réagit qu'aux messages entrants. Par conséquent, quelqu'un doit envoyer un message à l'agent. Mais qui?
Mais l'action de script elle-même envoie un impact. En fait, l'impact est un analogue de la fonction d'envoi habituelle (et le format est le même).
Eh bien, l'action de script elle-même enverra un message via l'impact. Mais quand le fera-t-il?
Et il le fera quand le tour viendra à lui. C'est-à-dire si l'étape du script est la première, l'impact sera exécuté immédiatement après avoir entré run_for. Si l'étape du script n'est pas la première, l'impact sera exécuté dès que l'étape précédente aura fonctionné et le script passera à l'étape suivante.
La deuxième chose dont nous devons discuter ici est l'appel ignore. Cette fonction d'assistance indique que l'étape est déclenchée lorsque l'agent ne parvient pas à traiter le message. C'est-à-dire dans ce cas, l'agent Fork doit refuser de traiter le message Put.
Examinons plus en détail une étape du scénario de test:
sobj.scenario().define_step("take_when_free") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>() & tests::store_state_name("fork"), *philosopher & tests::reacts_to<msg_taken>());
Tout d'abord, nous voyons ici quand_all au lieu de quand. En effet, pour déclencher une étape, nous devons remplir plusieurs conditions à la fois. L'agent fork doit gérer Take. Et Philosophe doit gérer la réponse prise. Par conséquent, nous écrivons quand_all, pas quand. Soit dit en passant, il y a aussi quand_any, mais nous ne le rencontrerons pas dans les exemples examinés aujourd'hui.
Deuxièmement, nous devons également vérifier le fait qu'après le traitement Take, l'agent Fork sera dans l'état Taken. Nous effectuons la vérification comme suit: nous indiquons d'abord que dès que l'agent Fork a fini de traiter Take, le nom de son état actuel doit être enregistré à l'aide de la balise tag «fork». Cette construction préserve simplement le nom d'état de l'agent:
& tests::store_state_name("fork")
Et puis, lorsque le script est terminé avec succès, nous vérifions ce nom enregistré:
REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork"));
C'est-à-dire nous demandons au script: donnez-nous le nom qui a été enregistré avec la balise fork pour l'étape nommée take_when_free, puis comparez le nom avec la valeur attendue.
Voici peut-être tout ce qui pourrait être noté dans le cas de test pour l'agent Fork. Si les lecteurs ont des questions, posez-les dans les commentaires, nous y répondrons avec plaisir.
Test de script réussi pour le philosophe
Pour l'agent Philosophe, nous ne considérerons qu'un seul cas de test - pour le cas où Philosophe peut prendre les deux fourchettes et manger.
Ce cas de test ressemblera à ceci:
TEST_CASE( "philosopher (takes both forks)" ) { tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } }; so_5::agent_t * philosopher{}; so_5::agent_t * left_fork{}; so_5::agent_t * right_fork{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { left_fork = coop.make_agent<fork_t>(); right_fork = coop.make_agent<fork_t>(); philosopher = coop.make_agent<philosopher_t>( "philosopher", left_fork->so_direct_mbox(), right_fork->so_direct_mbox()); }); auto scenario = sobj.scenario(); scenario.define_step("stop_thinking") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_thinking>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); scenario.define_step("take_left") .when( *left_fork & tests::reacts_to<msg_take>() ); scenario.define_step("left_taken") .when( *philosopher & tests::reacts_to<msg_taken>() & tests::store_state_name("philosopher") ); scenario.define_step("take_right") .when( *right_fork & tests::reacts_to<msg_take>() ); scenario.define_step("right_taken") .when( *philosopher & tests::reacts_to<msg_taken>() & tests::store_state_name("philosopher") ); scenario.define_step("stop_eating") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_eating>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); scenario.define_step("return_forks") .when_all( *left_fork & tests::reacts_to<msg_put>(), *right_fork & tests::reacts_to<msg_put>() ); scenario.run_for(std::chrono::seconds(1)); REQUIRE(tests::completed() == scenario.result()); REQUIRE("wait_left" == scenario.stored_state_name("stop_thinking", "philosopher")); REQUIRE("wait_right" == scenario.stored_state_name("left_taken", "philosopher")); REQUIRE("eating" == scenario.stored_state_name("right_taken", "philosopher")); REQUIRE("thinking" == scenario.stored_state_name("stop_eating", "philosopher")); }
Assez volumineux, mais trivial. Tout d'abord, vérifiez que le philosophe a fini de penser et a commencé à se préparer à manger. Puis on vérifie qu'il a essayé de prendre la fourche gauche. Ensuite, il devrait essayer de prendre la bonne fourche. Ensuite, il devrait manger et arrêter cette activité. Ensuite, il doit mettre les deux fourches prises.
En général, tout est simple. Mais vous devez vous concentrer sur deux choses.
Tout d'abord, la classe testing_env_t, comme son prototype, wrapped_env_t, vous permet de personnaliser l'environnement SObjectizer. Nous allons l'utiliser pour activer le mécanisme de suivi de la remise des messages:
tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } };
Ce mécanisme vous permet de «visualiser» le processus de livraison des messages, ce qui aide à enquêter sur le comportement des agents (nous en avons déjà
parlé plus en détail ).
Deuxièmement, l'agent Philosophe effectue une série d'actions non pas immédiatement, mais après un certain temps. Ainsi, en commençant à fonctionner, l'agent doit s'envoyer un message StopThinking en attente. Ce message devrait donc arriver à l'agent après quelques millisecondes. Ce que nous indiquons en fixant la restriction nécessaire pour une certaine étape:
scenario.define_step("stop_thinking") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_thinking>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) );
C'est-à-dire ici, nous disons que nous ne sommes intéressés par aucune réaction de l'agent philosophe à StopThinking, mais seulement celle qui s'est produite au plus tôt 250 ms après le début du traitement de cette étape.
Une restriction du type not_before indique au script que tous les événements qui se produisent avant l'expiration du délai spécifié doivent être ignorés.
Il existe également une restriction de la forme not_after, cela fonctionne dans l'autre sens: seuls les événements qui se produisent jusqu'à l'expiration du délai spécifié sont pris en compte.
Les contraintes not_before et not_after peuvent être combinées, par exemple:
.constraints( tests::not_before(std::chrono::milliseconds(250)), tests::not_after(std::chrono::milliseconds(1250)))
mais dans ce cas, SObjectizer ne vérifie pas la cohérence des valeurs données.
Comment avez-vous réussi à mettre en œuvre cela?
Je voudrais dire quelques mots sur la façon dont tout cela a fonctionné. Après tout, dans l’ensemble, nous étions confrontés à une grande question idéologique: «Comment tester les agents en principe?» et une question plus petite, déjà technique: "Comment mettre en œuvre cela?"
Et si à propos de l'idéologie des tests, il était possible de sortir de votre esprit, alors à propos de la mise en œuvre, la situation était plus compliquée. Il était nécessaire de trouver une solution qui, tout d'abord, ne nécessiterait pas une modification radicale des intérieurs de SObjectizer. Et, deuxièmement, il était censé être une solution qui pourrait être mise en œuvre dans les délais prévisibles et très souhaitables.
À la suite du difficile processus de fumage du bambou, une solution a été trouvée. Pour cela, il fallait, en effet, n'apporter qu'une seule petite innovation dans le comportement régulier de SObjectizer. Et la base de la solution est le
mécanisme d'enveloppe de message, qui a été ajouté dans la version 5.5.23 et dont nous avons déjà parlé .
À l'intérieur de l'environnement de test, chaque message envoyé est enveloppé dans une enveloppe spéciale. Lorsqu'une enveloppe contenant un message est remise à l'agent pour traitement (ou, au contraire, rejetée par l'agent), le scénario de test s'en rend compte. Grâce aux enveloppes, le script de test sait ce qui se passe et peut déterminer les moments où les étapes de script «fonctionnent».
Mais comment faire en sorte que SObjectizer enveloppe chaque message dans une enveloppe spéciale?
C'était une question intéressante. Il a décidé comme suit: un concept tel que
event_queue_hook a été inventé. Il s'agit d'un objet spécial avec deux méthodes - on_bind et on_unbind.
Lorsqu'un agent est lié à un répartiteur spécifique, le répartiteur envoie une file d'attente d'événements à l'agent. Grâce à cette file d'attente d'événements, les demandes de l'agent entrent dans la file d'attente nécessaire et deviennent disponibles pour le répartiteur pour traitement. Lorsqu'un agent s'exécute dans un SObjectizer, il a un pointeur sur event_queue. Lorsqu'un agent est supprimé d'un SObjectizer, son pointeur sur event_queue est annulé.
Ainsi, à partir de la version 5.5.24, l'agent, à la réception de event_queue, doit appeler la méthode on_bind de event_queue_hook. Où l'agent doit passer le pointeur reçu à event_queue. Et event_queue_hook peut renvoyer le même pointeur ou un autre pointeur en réponse. Et l'agent doit utiliser la valeur retournée.
Lorsqu'un agent est supprimé d'un SObjectizer, il doit appeler on_unbind sur event_queue_hook. Dans on_unbind, l'agent transmet la valeur renvoyée par la méthode on_bind.
Toute cette cuisine est exécutée à l'intérieur du SObjectizer et l'utilisateur ne voit rien de tout cela. Et, en principe, vous ne le savez peut-être pas du tout. Mais l'environnement de test de SObjectizer, le même testing_env_t, exploite exactement event_queue_hook. Dans testing_env_t, une implémentation spéciale de event_queue_hook est créée.
Cette implémentation dans on_bind enveloppe chaque event_queue dans un objet proxy spécial. Et déjà cet objet proxy place les messages envoyés à l'agent dans une enveloppe spéciale.Mais ce n'est pas tout.
Vous vous souvenez peut-être que dans un environnement de test, les agents doivent être gelés. Ceci est également implémenté via les objets proxy mentionnés. Pendant que le script de test n'est pas en cours d'exécution, l'objet proxy stocke les messages envoyés à l'agent à domicile. Mais lorsque le script est exécuté, l'objet proxy transfère tous les messages précédemment accumulés vers la file d'attente de messages actuelle de l'agent.Conclusion
En conclusion, je veux dire deux choses.Tout d'abord, nous avons mis en œuvre notre point de vue sur la façon dont les agents peuvent être testés dans SObjectizer. Mon opinion parce qu'il n'y a pas tellement de bons modèles de rôle autour. Nous avons regardé vers Akka . Mais Akka et SObjectizer sont trop différents pour porter les approches qui fonctionnent dans Akka vers SObjectizer. Et C ++ n'est pas Scala / Java, dans lequel certaines choses liées à l'introspection peuvent se faire par réflexion. J'ai donc dû trouver une approche qui tomberait sur SObjectizer.Dans la version 5.5.24, la toute première implémentation expérimentale est devenue disponible. Vous pouvez sûrement faire mieux. Mais comment comprendre ce qui sera utile et quels sont les fantasmes inutiles? Malheureusement, rien. Vous devez prendre et essayer, voir ce qui se passe dans la pratique.Nous avons donc fait une version minimale que vous pouvez prendre et essayer. Ce que nous proposons de faire pour tout le monde: essayez, expérimentez et partagez vos impressions avec nous. Qu'est-ce que tu as aimé, qu'est-ce que tu n'as pas aimé? Peut-être qu'il manque quelque chose?Deuxièmement, les mots prononcés début 2017 sont devenus encore plus pertinents :… , , , . - — . . . : , .
, , , — , .
Par conséquent, mon conseil à ceux qui recherchent un cadre d'acteur prêt à l'emploi: faites attention non seulement à l'originalité des idées et à la beauté des exemples. Regardez également toutes sortes de choses auxiliaires qui vous aideront à comprendre ce qui se passe dans votre application: par exemple, découvrez combien d'acteurs sont à l'intérieur maintenant, quelles sont leurs tailles de file d'attente, si le message n'atteint pas le destinataire, alors où va-t-il ... Si le cadre le fait fournit quelque chose comme ça, ce sera plus facile pour vous. Si ce n'est pas le cas, vous aurez plus de travail.
Tout ce qui précède est encore plus important pour tester les acteurs. Par conséquent, lorsque vous choisissez un cadre d'acteur pour vous-même, faites attention à ce qu'il contient et à ce qui ne l'est pas. Par exemple, nous avons déjà dans notre boîte à outils pour simplifier les tests :)