Les machines à états sont un sujet très peu discuté, peut-être que le côté mathématique fait peur à certain. Pour faire suite à notre article sur les plugin, nous allons mettre en place une machine à état pour suivre son cycle de vie.
Le projet State Machine de Spring sera utilisé pour la suite de cet article.
Un plugin peut avoir différent état
- INSTALLED,
- ACTIVE,
- INACTIVE,
- UPDATING,
- FAILED,
- UNINSTALLED
Dans un système de gestion de plugin, différente action devrait être mis en place :
- Téléchargement du JAR
- Vérification des dépendances
- Initialisation du contexte Spring
- Enregistrement des routes ou services
- Nettoyage des ressources
Chaque transition doit être explicitement autorisée.
- On ne peut pas désactiver un plugin qui n’est pas activé.
- On ne peut pas réactiver un plugin désinstallé.
- Une erreur pendant l’activation doit mener à l’état FAILED.
Gérer cela avec des if/else mène à un code difficile à maintenir. Une machine à états permet de modéliser ce cycle clairement.
État et évènement
Liste des états
public enum PluginState {
INSTALLED,
ACTIVE,
INACTIVE,
UPDATING,
FAILED,
UNINSTALLED
}
Listes des évènements
public enum PluginEvent {
INSTALL,
ACTIVATE,
DEACTIVATE,
UPDATE,
UNINSTALL,
ERROR
}
Le plugin
public class Plugin {
private String id;
private PluginState state;
public Plugin(String id) {
this.id = id;
this.state = PluginState.INSTALLED; // Après installation
}
public String getId() { return id; }
public PluginState getState() { return state; }
public void setState(PluginState state) { this.state = state; }
}
Action du plugin
Activation du plugin
@Component
public class ActivatePluginAction implements Action<PluginState, PluginEvent> {
private static final Logger log = LoggerFactory.getLogger(ActivatePluginAction.class);
@Override
public void execute(StateContext<PluginState, PluginEvent> context) {
Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
log.info("Activation du plugin : " + plugin.getId());
}
}
Désactivation du plugin
@Component
public class DeactivatePluginAction implements Action<PluginState, PluginEvent> {
private static final Logger log = LoggerFactory.getLogger(DeactivatePluginAction.class);
@Override
public void execute(StateContext<PluginState, PluginEvent> context) {
Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
log.info("Plugin désactivé : " + plugin.getId());
}
}
Erreur du plugin
@Component
public class FailPluginAction implements Action<PluginState, PluginEvent> {
private static final Logger log = LoggerFactory.getLogger(FailPluginAction.class);
@Override
public void execute(StateContext<PluginState, PluginEvent> context) {
Plugin plugin = (Plugin) context.getMessage().getHeaders().get("plugin");
log.warn("Plugin en échec : " + plugin.getId());
plugin.setState(PluginState.FAILED);
}
}
Configuration de la machine à états
- Les états possibles du plugin
- Les transitions autorisées entre ces états
- Les événements qui déclenchent ces transitions
- Les actions à exécuter lors de certaines transitions
@Configuration
@EnableStateMachineFactory
public class PluginStateMachineConfig extends StateMachineConfigurerAdapter<PluginState, PluginEvent> {
private final ActivatePluginAction activateAction;
private final DeactivatePluginAction deactivateAction;
private final FailPluginAction failAction;
public PluginStateMachineConfig(ActivatePluginAction activateAction,
DeactivatePluginAction deactivateAction,
FailPluginAction failAction) {
this.activateAction=activateAction;
this.deactivateAction=deactivateAction;
this.failAction=failAction;
}
@Override
public void configure(StateMachineStateConfigurer<PluginState, PluginEvent> states) throws Exception {
states
.withStates()
.initial(PluginState.INSTALLED)
.states(EnumSet.allOf(PluginState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<PluginState, PluginEvent> transitions) throws Exception {
transitions
.withExternal()
.source(PluginState.INSTALLED)
.target(PluginState.ACTIVE)
.event(PluginEvent.ACTIVATE)
.action(activateAction)
.and()
.withExternal()
.source(PluginState.ACTIVE)
.target(PluginState.INACTIVE)
.event(PluginEvent.DEACTIVATE)
.action(deactivateAction)
.and()
.withExternal()
.source(PluginState.INACTIVE)
.target(PluginState.ACTIVE)
.event(PluginEvent.ACTIVATE)
.action(activateAction)
.and()
.withExternal()
.source(PluginState.ACTIVE)
.target(PluginState.UPDATING)
.event(PluginEvent.UPDATE)
.and()
.withExternal()
.source(PluginState.UPDATING)
.target(PluginState.ACTIVE)
.event(PluginEvent.ACTIVATE)
.action(activateAction)
.and()
.withExternal()
.source(PluginState.INSTALLED)
.target(PluginState.UNINSTALLED)
.event(PluginEvent.UNINSTALL)
.and()
.withExternal()
.source(PluginState.ACTIVE)
.target(PluginState.FAILED)
.event(PluginEvent.ERROR)
.action(failAction)
.and()
.withExternal()
.source(PluginState.INACTIVE)
.target(PluginState.FAILED)
.event(PluginEvent.ERROR)
.action(failAction)
.and()
.withExternal()
.source(PluginState.FAILED)
.target(PluginState.UNINSTALLED)
.event(PluginEvent.UNINSTALL);
}
}
Diagramme du workflow
Ce diagramme affiche les différents états et les transitions possible.
Cycle de vie du plugin
Cette classe crée, stocke et orchestre les transitions d'états des plugins via Spring StateMachine. Une map sert de stockage temporaire des plugins.
Lors de l'activation d'un plugin, on
- Récupère le plugin via son id.
- Crée une instance de machine à états via la factory.
- Construit un message contenant l’événement ACTIVATE, avec le plugin comme contexte.
- Envoie l’événement à la machine.
- Si la transition échoue, une exception est levée (ex. : INSTALLED → DEACTIVATE n’est pas autorisé).
- Met à jour l’état du plugin avec le nouvel état de la machine.
- La machine à états garantit que seul ce qui est autorisé se produit.
@Service
public class PluginService {
private final Map<String, Plugin> plugins = new ConcurrentHashMap<>();
@Autowired
private StateMachineFactory<PluginState, PluginEvent> stateMachineFactory;
public Plugin createPlugin(String id) {
Plugin plugin = new Plugin(id);
plugins.put(id, plugin);
return plugin;
}
public Plugin getPlugin(String id) {
return plugins.get(id);
}
public void activate(String id) {
Plugin plugin = getPlugin(id);
if (plugin == null) {
throw new IllegalArgumentException("Plugin non trouvé : " + id);
}
StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();
Message<PluginEvent> message = MessageBuilder
.withPayload(PluginEvent.ACTIVATE)
.setHeader("plugin", plugin)
.build();
stateMachine.sendEvent(message);
plugin.setState(stateMachine.getState().getId());
}
public void deactivate(String id) {
Plugin plugin = getPlugin(id);
if (plugin == null) {
throw new IllegalArgumentException("Plugin non trouvé : " + id);
}
StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();
Message<PluginEvent> message = MessageBuilder
.withPayload(PluginEvent.DEACTIVATE)
.setHeader("plugin", plugin)
.build();
stateMachine.sendEvent(message);
plugin.setState(stateMachine.getState().getId());
}
public void uninstall(String id) {
Plugin plugin = getPlugin(id);
if (plugin == null) {
throw new IllegalArgumentException("Plugin non trouvé : " + id);
}
StateMachine<PluginState, PluginEvent> stateMachine = stateMachineFactory.getStateMachine();
Message<PluginEvent> message = MessageBuilder
.withPayload(PluginEvent.UNINSTALL)
.setHeader("plugin", plugin)
.build();
stateMachine.sendEvent(message);
plugin.setState(stateMachine.getState().getId());
plugins.remove(id); // Supprimer du stockage
}
}
La machine à états valide les transitions. Si un plugin est dans l’état INSTALLED, une tentative de DEACTIVATE échouera.
Test
Un controlleur a été mis en place pour pouvoir tester plus aisément les états.
@RestController
@RequestMapping("/plugins")
public class PluginController {
private final PluginService pluginService;
public PluginController(PluginService pluginService) {
this.pluginService=pluginService;
}
@PostMapping("/create/{id}")
public String create(@PathVariable String id) {
Plugin plugin = pluginService.createPlugin(id);
return "Plugin créé : " + id + ", état initial : " + plugin.getState();
}
@PostMapping("/{id}/activate")
public String activate(@PathVariable String id) {
pluginService.activate(id);
Plugin plugin = pluginService.getPlugin(id);
return "Plugin activé : " + id + ", état : " + plugin.getState();
}
@PostMapping("/{id}/deactivate")
public String deactivate(@PathVariable String id) {
pluginService.deactivate(id);
Plugin plugin = pluginService.getPlugin(id);
return "Plugin désactivé : " + id + ", état : " + plugin.getState();
}
@PostMapping("/{id}/uninstall")
public String uninstall(@PathVariable String id) {
pluginService.uninstall(id);
return "Plugin désinstallé : " + id;
}
@GetMapping("/{id}")
public String getStatus(@PathVariable String id) {
Plugin plugin = pluginService.getPlugin(id);
if (plugin == null) {
return "Plugin non trouvé";
}
return "Plugin " + id + ", état : " + plugin.getState();
}
}
Par la suite, vous pouvez utiliser ce controller pour tester différents scénarios
Aucun commentaire:
Enregistrer un commentaire