Bonjour,
Le noyau de Yodii tournicote (il reste des choses à faire). Parmi ces choses, le Host n’a jamais été revu depuis CiviKey. Une question de Maxime m’a poussée à mettre mon nez là-dedans.
Le résultat de ces deux jours de revue et réflexions (en parallèle des étudiants) est ci-dessous.
Le host de CiviKey n’était clairement pas carré…
Rappel le Host a une méthode Apply qui prend en paramètres 3 listes : les Plugins à démarrer, les plugins à arrêter et ceux à désactiver (Disabled).
La séquence des opérations dans CiviKey était la suivante :
1. Pour tous les plugins à démarrer :
o Ils sont instanciés s’ils n’existent pas déjà (ils n’ont jamais été activés ou, si ils sont IDisposable ont été détruits et oubliés).
o Si une erreur survient lors de l’instanciation de l’un quelconques de ces plugins, le processus s’arrête immédiatement en erreur (LoadError).
2. Pour tous les plugins à arrêter :
o Leur InternalStatus passe à Stopping et s’ils implémentent un ou plusieurs Services, le statut des Services est lui aussi mis à Stopping et les évènements correspondants sont émis sur les IService<T>.
o La méthode Stop() est appelée.
o Si une erreur survient, elle est logguée et ajoutée à une liste d’erreur mais le processus continue.
3. Pour tous les plugins à arrêter :
o Leur InternalStatus passe à Stopped et s’ils implémentent un ou plusieurs Services, le statut des Services est lui aussi mis à Stopped et les évènements correspondants sont émis sur les IService<T>.
o La méthode Teardown() est appelée.
o Si une erreur survient, elle est logguée et ajoutée à une liste d’erreur mais le processus continue.
4. Pour tous les plugins à démarrer :
o S’ils implémentent un Service, l’implémentation du Service est branchée sur le plugin (l’état de ce service est nécessairement Disabled ou Stopped : on a arrêté les plugins qui devaient s’arrêter).
o Si le plugin était Disabled, son Status passe à Stopped. Les évènements correspondants sont émis si le plugin implémente un Service.
5. Pour tous les Plugins à désactiver (Disabled) :
o Leur statut est passé à Disabled. Un évènement Disabled est émis au niveau d’éventuels Services pour les plugins qui ne sont pas remplacés par une autre implémentation.
o S’ils supportent IDisposable, Dispose est appelé et le plugin est oublié.
o Si une erreur survient, elle est logguée et ajoutée à une liste d’erreur mais le processus continue.
6. Pour tous les plugins à démarrer :
o La méthode Setup est appelée.
o Si une erreur survient ou que Setup retourne false, l’erreur est logguée et on appelle Teardown sur les plugins dont la méthode Setup a précédemment été appelée.
o Le processus s’arrête là.
7. Pour tous les plugins à démarrer :
o Leur statut est passé à Started. L’évènement Disabled est émis par les éventuels Services.
o La méthode Start est appelée.
o En cas d’erreur, celle-ci est logguée et :
§ on appelle Stop sur tous les plugins dont la méthode Start a été appelée.
§ on appelle Teardown sur tous ces plugins.
Cette séquence n’est tout simplement pas cohérente :
- Le Setup a le droit d’échouer et pourtant, lorsque cela arrive, les plugins qui devaient être arrêtés l’ont déjà été.
- L’émission de l’évènement StatusChanged au niveau des Services est faite trop tôt dans le cas du Start.
- Et globalement, c’est le bordel et en cas d’exceptions, bien malin est celui qui peut dire dans quel état est le système ! (l’API n’est pas symétrique entre le Stop et le Start, et la remise en état du système est tout sauf garantit).
Yodii clarifie ces mécanismes (malheureusement en changeant les API).
On assume un changement d’état en 2 phases (comme un two-phases-commit). Les Setup/Teardown deviennent symétriques, on les nomme maintenant PreStart et PreStop. Le graphe de transition est le suivant :
[cid:image0...@01D00FCA.08CFC930]
PreStart et PreStop peuvent échouer. Le Setup de CiviKey retourne un booléen ET prend une petite classe en paramètre (PluginSetupInfo) qui permet de préciser deux messages d’erreurs (un FailedUserMessage et un FailedDetailedMessage) et une exception, ce qui est ambigüe.
Les méthodes PreStart/Stop de Yodii ne retournent pas de booléen : un échec doit être signalé par un message unique et/ou une exception via le paramètre.
Les mécanismes nécessaires de compensations sont malheureusement un peu plus complexes à mettre en œuvre, mais après moult réflexions, je n’ai pas trouvé plus simple… Le nouveau modèle permet une nouveauté : le hot swapping de plugins qui implémentent le même service a pour effet de masquer totalement les changements de statuts au niveau du Service.
En termes de gestion des erreurs, Yodii est beaucoup plus strict que son prédécesseur : toute exception survenant lors d’une transition est considérée comme une erreur fatale de l’Engine qui stoppe le moteur.
La séquence de Yodii devient :
- Activation de ceux qui doivent démarrer s’ils n’existent pas déjà.
- Appels des PreStop( IPreStopContext ) de tous ceux qui doivent s’arrêter
o Si l’un refuse, tous ceux qui ont subi un PreStop pour rien doivent compenser : la fonction IPreStopContext.RollbackAction est appelée.
§ Si cet RollbackAction n’a pas été explicitement renseigné, par le PreStop, c’est le Start( IStartContext ) qui est appelé.
- Appels des PreStart( IPreStartContext ) de tous ceux qui doivent démarrer.
o Si l’un refuse, tous ceux qui ont subi un PreStop et un PreStart pour rien doivent compenser : la fonction IPreStartContext.RollbackAction est appelée.
§ Si cet RollbackAction n’a pas été explicitement renseigné, par le PreStart, c’est le Stop( IStopContext ) qui est appelé.
- Emission des évènements de changements d’état Stopping au niveau des Services en remontant vers les généralisations.
- Emission des évènements de changements d’état Starting au niveau des Services en remontant vers les généralisations.
- Appels des Stop( IStopContext ) de tous ceux qui doivent s’arrêter
- Appels des Start( IStartContext ) de tous ceux qui doivent démarrer
- Emission des évènements de changements d’état Stopped au niveau des Services en remontant vers les généralisations.
- Emission des évènements de changements d’état Started au niveau des Services en remontant vers les généralisations.
- Désactivation des plugins qui supportent IDisposable et doivent être Disabled.
- Emission des évènements de changements d’état Disabled au niveau des Services en remontant vers les généralisations.
Cela donnerai cela et si certains d’entre vous pouvaient se pencher dessus, ça m’éviterait de partir sur des conneries :) :
/// <summary>
/// This interface defines the minimal properties and behavior of a plugin.
/// It implements a two-phases transition: plugin that should stop or start
/// can accept or reject the transition thanks to <see cref="PresStop"/> and <see cref="PreStart"/>.
/// If all of them aggreed, then <see cref="Stop"/> and <see cref="Start"/> are called.
/// </summary>
interface IYodiiPlugin
{
/// <summary>
/// Called before the actual <see cref="Stop"/> method.
/// Implementations must validate that this plugin can be stoppped: if not, the transition must
/// be canceled by calling <see cref="IPreStopContext.Cancel"/>.
/// </summary>
/// <param name="c">The context to use.</param>
void PreStop( IPreStopContext c );
/// <summary>
/// Called before the actual <see cref="Start"/> method.
/// Implementations must validate that the start is possible and, if unable
/// to start, cancels it by calling <see cref="IPreStartContext.Cancel"/> .
/// </summary>
/// <param name="c">The context to use.</param>
void PreStart( IPreStartContext c );
/// <summary>
/// Called after successful calls to all <see cref="PreStop"/> and <see cref="PreStart"/>.
/// This may also be called to cancel a previous call to <see cref="PreStart"/> if another
/// plugin rejected the transition.
/// </summary>
/// <param name="c">The context to use.</param>
void Stop( IStopContext c );
/// <summary>
/// Called after successful calls to all <see cref="PreStop"/> and <see cref="PreStart"/>.
/// This may also be called to cancel a previous call to <see cref="PreStop"/> if another
/// plugin rejected the transition.
/// </summary>
/// <param name="c">The context to use.</param>
void Start( IStartContext c );
}
/// <summary>
/// Transition context for <see cref="IYodiiPlugin.PreStop"/>.
/// </summary>
interface IPreStopContext
{
/// <summary>
/// Cancels the stop with an optional exception and/or message.
/// If for any reason a plugin can not or refuse to stop, this method must be called.
/// </summary>
/// <param name="message">Reason to reject the stop.</param>
/// <param name="ex">Optional exception that occurred.</param>
void Cancel( string message = null, Exception ex = null );
/// <summary>
/// Gets a shared storage that is available during the whole transition.
/// Used to exchange any kind of information during a transition: this typically
/// holds data that enables plugin hot swapping.
/// </summary>
IDictionary<object, object> SharedMemory { get; }
/// <summary>
/// Gets or sets the action that will be executed if any other PreStop or PreStart fails.
/// Note that this rollback action will not be called for the plugin that called <see cref="Cancel"/>.
/// Defaults to <see cref="IPlugin.Start"/>.
/// </summary>
Action<IStartContext> RollbackAction { get; set; }
}
/// <summary>
/// Transition context for <see cref="IYodiiPlugin.PreStart"/>.
/// </summary>
interface IPreStartContext
{
/// <summary>
/// Cancels the start with an optional exception and/or message.
/// </summary>
/// <param name="message">Reason to not start.</param>
/// <param name="ex">Optional exception that occurred.</param>
void Cancel( string message = null, Exception ex = null );
/// <summary>
/// Gets a shared storage that is available during the whole transition.
/// Used to exchange any kind of information during a transition: this typically
/// holds data that enables plugin hot swapping.
/// </summary>
IDictionary<object, object> SharedMemory { get; }
/// <summary>
/// Gets or sets the action that will be executed if any other PreStart fails.
/// Note that this rollback action will not be called for the plugin that called <see cref="Cancel"/>.
/// Defaults to <see cref="IPlugin.Stop"/>.
/// </summary>
Action<IStopContext> RollbackAction { get; set; }
/// <summary>
/// Gets the most specialized service that the starting plugin implements
/// and is also implemented by a plugin that is stopping.
/// </summary>
IYodiiService PreviousPluginCommonService { get; }
/// <summary>
/// Gets the previous plugin that also implements <see cref="PreviousPluginCommonService"/> that
/// has just been stopped: the starting plugin may set <see cref="PreviousHotSwapping"/> to true
/// to silently replace it.
/// </summary>
IYodiiPlugin PreviousPlugin { get; }
/// <summary>
/// Gets or sets whether the plugin silently replaces the <see cref="PreviousPlugin"/> if any.
/// Defaults to false. When set to true, observers of the <see cref="PreviousPluginCommonService"/>
/// will not receive any events.
/// </summary>
bool PreviousHotSwapping { get; set; }
}
/// <summary>
/// Transition context for <see cref="IYodiiPlugin.Stop"/>.
/// </summary>
interface IStopContext
{
/// <summary>
/// Gets whether this stop is from a cancelled <see cref="IPlugin.PreStart"/> rather
/// than a successful <see cref="IPlugin.PreStop"/>.
/// </summary>
bool CancellingPreStart { get; }
/// <summary>
/// Gets a shared storage that is available during the whole transition.
/// Used to exchange any kind of information during a transition: this typically
/// holds data that enables plugin hot swapping.
/// </summary>
IDictionary<object, object> SharedMemory { get; }
/// <summary>
/// Gets whether the plugin is silently replaced by another one.
/// </summary>
bool HotSwapping { get; }
}
/// <summary>
/// Transition context for <see cref="IYodiiPlugin.Start"/>.
/// </summary>
interface IStartContext
{
/// <summary>
/// Gets whether this stop is from a cancelled <see cref="IPlugin.PreStop"/> rather
/// than a successful <see cref="IPlugin.PreStart"/>.
/// </summary>
bool CancellingPreStop { get; }
/// <summary>
/// Gets a shared storage that is available during the whole transition.
/// Used to exchange any kind of information during a transition: this typically
/// holds data that enables plugin hot swapping.
/// </summary>
IDictionary<object, object> SharedMemory { get; }
/// <summary>
/// Gets whether the plugin silently replaces the <see cref="IPreStartContext.PreviousPlugin"/> if any.
/// </summary>
bool HotSwapping { get; }
}
Voilà.
Olivier Spinelli
Gérant
Tél. : 06.20.41.47.14
[signature]
10 rue Mercoeur - 75011 Paris
Tél. : 01.84.16.19.99
www.invenietis.com<
http://www.invenietis.com/>