Como lidar com o estado inicial em uma arquitetura orientada a eventos?

33

Em uma arquitetura orientada a eventos, cada componente atua apenas quando um evento é enviado pelo sistema.

Imagine um carro hipotético com um pedal de freio e uma luz de freio.

  • As voltas da luz de freio sobre quando ele recebe um brake_on evento, e fora quando ele recebe um brake_off evento.
  • O pedal do freio envia um evento brake_on quando é pressionado e um evento brake_off quando é liberado.

Tudo está bem, até que você tenha a situação em que o carro está ligado com o pedal do freio já pressionado . Como a luz do freio nunca recebeu um evento brake_on , ela permanecerá apagada - claramente uma situação indesejável. Ligar a luz do freio por padrão apenas reverte a situação.

O que poderia ser feito para resolver esse 'problema inicial de estado'?

EDIT: Obrigado por todas as respostas. Minha pergunta não era sobre um carro de verdade. Nos carros, eles resolviam esse problema enviando continuamente o estado - portanto, não há problema de inicialização nesse domínio. No meu domínio de software, essa solução usaria muitos ciclos desnecessários de CPU.

EDIT 2: Além da resposta de @ gbjbaanb , vou para um sistema em que:

  • o pedal de freio hipotético, após a inicialização, envia um evento com seu estado e
  • a luz de freio hipotética, após a inicialização, envia um evento solicitando um evento de estado do pedal do freio.

Com esta solução, não há dependências entre componentes, condições de corrida, filas de mensagens para ficar obsoletas e componentes 'mestre'.

Frank Kusters
fonte
2
A primeira coisa que vem à mente é gerar um evento "sintético" (chame-o initialize) que contém os dados necessários do sensor.
msw
O pedal não deve enviar um evento brake_pedal_on e o freio real envia o evento brake_on? Eu não gostaria que minha luz de freio acendesse se o freio não estivesse funcionando.
bdsl
3
Eu mencionei que era um exemplo hipotético? :-) É muito simplificado manter a pergunta curta e objetiva.
27468 Frank Kusters

Respostas:

32

Existem várias maneiras de fazer isso, mas eu prefiro manter um sistema baseado em mensagens o mais dissociado possível. Isso significa que o sistema geral não pode ler o estado de nenhum componente, nem nenhum componente lê o estado de qualquer outro (pois dessa forma estão os espaguetes das dependências).

Portanto, embora o sistema em execução se cuide, precisamos de uma maneira de dizer a cada componente para iniciar, e já temos isso no registro de componentes, ou seja, na inicialização, o sistema principal deve informar cada componente que é agora registrado (ou solicitará que cada componente retorne seus detalhes para que possa ser registrado). Esse é o estágio no qual o componente pode executar suas tarefas de inicialização e pode enviar mensagens como faria na operação normal.

Assim, o pedal do freio, quando a ignição é iniciada, receberia uma mensagem de registro / verificação da gerência do carro e retornaria não apenas a mensagem "Estou aqui e trabalhando", mas também verificaria seu próprio estado e enviaria a mensagem mensagens para esse estado (por exemplo, uma mensagem pressionada no pedal).

O problema passa a ser uma das dependências de inicialização, como se a luz do freio ainda não estivesse registrada, ela não receberá a mensagem, mas isso é facilmente resolvido enfileirando todas essas mensagens até o sistema principal concluir a rotina de inicialização, registro e verificação .

O maior benefício é que não há código especial necessário para lidar com a inicialização, exceto que você já precisa escrever (ok, se o envio de mensagens para eventos do pedal do freio estiver em um manipulador do pedal do freio, você também precisará chamar isso na inicialização. , mas isso geralmente não é um problema, a menos que você tenha escrito esse código fortemente vinculado à lógica do manipulador) e nenhuma interação entre os componentes, exceto aqueles que eles já enviam uns aos outros normalmente. As arquiteturas de passagem de mensagens são muito boas por causa disso!

gbjbaanb
fonte
1
Gosto da sua resposta, pois mantém todos os componentes separados - foi o motivo mais importante para escolher essa arquitetura. No entanto, atualmente não há nenhum componente 'mestre' real que decida que o sistema está em um estado 'inicializado' - tudo começa a funcionar. Com o problema na minha pergunta como resultado. Depois que o mestre decide que o sistema está em execução, ele pode enviar um evento 'inicializado pelo sistema' para todos os componentes, após o qual cada componente começa a transmitir seu estado. Problema resolvido. Obrigado! (Agora eu estou acabado de sair com o problema como decidir se o sistema é inicializado ...)
Frank Kusters
Que tal fazer com que o distribuidor da atualização de status acompanhe a atualização mais recente recebida de cada objeto e, sempre que uma nova solicitação de inscrição é recebida, ele envia ao novo assinante as atualizações mais recentes que recebeu das fontes de eventos registradas?
supercat 12/02
Nesse caso, você também deve acompanhar quando os eventos expiram. Nem todos os eventos são passíveis de manutenção para sempre para quaisquer novos componentes que possam ser registrados.
Frank Kusters
@spaceknarf: bem, no caso em que "tudo começa a correr", você não pode criar dependência nos componentes para que o pedal comece após a luz, você só precisa iniciá-los nessa ordem, embora eu imagine que algo os faça funcionar, então corra eles na ordem 'certa' (por exemplo, scripts de inicialização do linux init antes do systemd, onde o serviço para iniciar primeiro é chamado 1.xxx e o segundo é chamado 2.xxx etc.).
Gbjbaanb
Scripts com uma ordem como essa são frágeis. Ele contém muitas dependências implícitas. Em vez disso, eu estava pensando que se você tiver um componente 'mestre', que tenha uma lista configurada estaticamente de componentes que devem ser executados (como mencionado por @Lie Ryan), poderá transmitir um evento 'pronto' assim que todos esses componentes forem carregados. Em resposta a isso, todos os componentes transmitem seu estado inicial.
27468 Frank Kusters
4

Você pode ter um evento de inicialização que defina os estados adequadamente após o carregamento / inicialização. Isso pode ser desejável para sistemas ou programas simples, que não incluem várias peças de hardware; no entanto, para sistemas mais complicados, com vários componentes físicos, você corre o mesmo risco de não inicializar - se um evento de "frenagem" for perdido ou perdido durante a comunicação. sistema (por exemplo, um sistema baseado em CAN), você pode inadvertidamente ajustar o sistema para trás, como se o tivesse iniciado com o freio pressionado. Quanto mais controladores você tiver, como em um carro, maior a probabilidade de que algo se perca.

Para explicar isso, você pode fazer com que a lógica do "freio ligado" envie repetidamente eventos "freio no". Talvez a cada 1/100 de segundo ou algo assim. Seu código que contém o cérebro pode escutar esses eventos e acionar o "freio" enquanto os recebe. Após 1/10 s de não receber sinais de "freio ligado", ele dispara um evento interno "freio_off".

Eventos diferentes terão requisitos de tempo consideravelmente diferentes. Em um carro, sua luz de freio precisa ser muito mais rápida do que a luz de verificação de combustível (onde um atraso de vários segundos é provavelmente aceitável) ou outros sistemas menos importantes.

A complexidade do seu sistema físico determinará qual dessas abordagens é mais apropriada. Dado que o seu exemplo é um veículo, você provavelmente desejaria algo semelhante ao último.

De qualquer forma, com um sistema físico, você NÃO deseja confiar em um único evento sendo recebido / processado corretamente. Microcontroladores conectados em um sistema em rede geralmente têm um tempo limite "Estou vivo" por esse motivo.

enderland
fonte
em um sistema de física que você iria correr um fio e usar a lógica binária: ALTA é freio deprimido e LOW é freio não deprimido
aberração catraca
@ratchetfreak, existem muitas possibilidades para esse tipo de coisa. Talvez um switch possa lidar com isso. Existem muitos outros eventos do sistema que não são tratados com simplicidade.
enderland
1

Nesse caso, eu não modelaria o freio como um simples ligar / desligar. Em vez disso, eu enviaria eventos de "pressão de freio". Por exemplo, uma pressão de 0 indicaria desligada e uma pressão de 100 seria totalmente deprimida. O sistema (nó) envia constantemente eventos de pressão de interrupção (em um determinado intervalo) ao (s) controlador (es), conforme necessário.

Quando o sistema foi iniciado, ele começava a receber eventos de pressão até ser desligado.

Jon Raynor
fonte
1

Se o seu único meio de transmitir informações de estado é através de eventos, você está com problemas. Em vez disso, você precisa ser capaz de:

  1. consultar o estado atual do pedal do freio e
  2. registre-se para eventos "estado alterado" no pedal do freio.

A luz do freio pode ser vista como um observador do pedal do freio. Em outras palavras, o pedal do freio não sabe nada sobre a luz do freio e pode operar sem ela. (Isso significa que qualquer noção do pedal do freio enviando proativamente um evento de "estado inicial" para a luz do freio é mal concebida.)

Após a instanciação do sistema, a luz do freio se registra no pedal do freio para receber notificações de frenagem, e também lê o estado atual do pedal do freio e liga ou desliga automaticamente.

Em seguida, as notificações de frenagem podem ser implementadas de uma das três maneiras:

  1. como eventos de "estado do pedal de frenagem alterados" sem parâmetros
  2. como um par de eventos "pedal de freio agora pressionado" e "pedal de freio agora liberado"
  3. como um evento "novo estado de pedal de freio" com um parâmetro "pressionado" ou "liberado".

Prefiro a primeira abordagem, o que significa que, ao receber a notificação, a luz do freio simplesmente fará o que já sabe: ler o estado atual do pedal do freio e ligar ou desligar.

Mike Nakis
fonte
0

Em um sistema orientado a eventos (que atualmente uso e amo), acho importante manter as coisas o mais dissociadas possível. Então, com essa ideia em mente, vamos nos aprofundar.

É importante ter algum estado padrão. Sua luz de freio levaria o estado padrão de 'desligado' e seu pedal de freio levaria o estado padrão de 'ativo'. Qualquer alteração depois disso seria um evento.

Agora, para responder à sua pergunta. Imagine o seu pedal de freio sendo inicializado e pressionado, o evento dispara, mas ainda não há luzes de freio para receber o evento. Achei mais fácil separar a criação dos objetos (onde os ouvintes do evento seriam inicializados) como uma etapa separada antes de inicializar qualquer lógica. Isso impedirá qualquer condição de corrida, como você descreveu.

Também acho estranho usar dois eventos diferentes para o que é efetivamente a mesma coisa . brake_offe brake_onpoderia ser simplificado em e_brakecom um parâmetro bool on. Você pode simplificar seus eventos dessa maneira adicionando dados de suporte.

Thebluefish
fonte
0

O que você precisa é de um evento de transmissão e caixas de entrada de mensagens. Uma transmissão é uma mensagem publicada para um número não especificado de ouvintes. Um componente pode se inscrever em eventos de transmissão para receber apenas eventos nos quais está interessado. Isso fornece dissociação, pois o remetente não precisa saber quem são os destinatários. A tabela de inscrição precisa ser configurada estaticamente durante a instalação do componente (em vez de quando a inicialização). A caixa de entrada faz parte do roteador de mensagens que atua como um buffer para reter mensagens quando o componente de destino está offline.

O uso de faturas traz um problema, que é o tamanho da caixa de entrada. Você não deseja que o sistema retenha um número crescente de mensagens para componentes que nunca estarão mais online. Isso é importante, especialmente no sistema incorporado com restrições estritas de memória. Para superar o limite de tamanho da caixa de entrada, todas as mensagens transmitidas precisam seguir algumas regras. As regras são:

  1. todos os eventos de transmissão requerem um nome
  2. a qualquer momento, o remetente de um evento de transmissão pode ter apenas uma transmissão ativa com um nome especificado
  3. o efeito causado pelo evento deve ser idempotente

O nome da transmissão precisa ser declarado durante o tempo de instalação do componente. Se um componente envia uma segunda transmissão com o mesmo nome antes que o receptor processe a anterior, a nova transmissão substitui a anterior. Agora você pode ter um limite de tamanho de caixa de entrada estático, que pode garantir nunca exceder um determinado tamanho e pode ser pré-calculado com base nas tabelas de inscrição.

Por fim, você também precisa de um arquivo de transmissão. O arquivo de transmissão é uma tabela que contém o último evento de cada nome de transmissão. Os novos componentes recém-instalados terão sua caixa de entrada preenchida previamente com mensagens do arquivo de transmissão. Como a caixa de entrada de mensagens, o arquivo de transmissão também pode ter tamanho estático.

Além disso, para lidar com situações em que o roteador de mensagens está offline, você também precisa de caixas de saída de mensagens. A caixa de saída da mensagem faz parte do componente que retém a mensagem de saída temporariamente.

Lie Ryan
fonte