Como se recuperar de uma quebra de máquina em estado finito?


13

Minha pergunta pode parecer muito científica, mas acho que é um problema comum, e esperamos que desenvolvedores e programadores experientes tenham alguns conselhos para evitar o problema mencionado no título. Aliás, o que descrevo abaixo é um problema real que estou tentando resolver proativamente no meu projeto iOS. Quero evitá-lo a todo custo.

Por máquina de estado finito, eu quero dizer isso> Eu tenho uma interface do usuário com alguns botões, vários estados de sessão relevantes para essa interface e o que essa interface representa, tenho alguns dados cujos valores são parcialmente exibidos na interface do usuário, recebo e manipulo alguns gatilhos externos (representado por retornos de chamada dos sensores). Criei diagramas de estado para mapear melhor os cenários relevantes desejáveis ​​e permitidos nessa interface do usuário e aplicativo. À medida que eu lentamente implemento o código, o aplicativo começa a se comportar cada vez mais como deveria. No entanto, não estou muito confiante de que seja suficientemente robusto. Minhas dúvidas vêm de observar meu próprio processo de pensamento e implementação. Eu estava confiante de que tinha tudo coberto, mas foi o suficiente para fazer alguns testes brutos na interface do usuário e rapidamente percebi que ainda existem lacunas no comportamento. Eu as corrigi. Contudo, como cada componente depende e se comporta com base na entrada de algum outro componente, uma certa entrada do usuário ou de alguma fonte externa aciona uma cadeia de eventos, o estado muda .. etc. Eu tenho vários componentes e cada um se comporta como este Trigger recebido na entrada -> trigger e seu remetente analisado -> output algo (uma mensagem, uma mudança de estado) com base na análise

O problema é que isso não é completamente autônomo e meus componentes (um item de banco de dados, um estado de sessão, algum estado de botão) ... PODEM ser alterados, influenciados, excluídos ou modificados fora do escopo da cadeia de eventos ou cenário desejável. (o telefone trava, a bateria está vazia, o telefone fica repentinamente) Isso introduzirá uma situação não válida no sistema, da qual o sistema potencialmente não conseguiria se recuperar. Eu vejo isso (apesar de as pessoas não perceberem que esse é o problema) em muitos dos meus aplicativos concorrentes que estão na Apple Store, os clientes escrevem coisas como esta> "Adicionei três documentos e, depois de ir para lá e para cá, não consigo abri-los, mesmo que a veja. " ou "Eu gravei vídeos todos os dias, mas depois de gravar um vídeo em log demais, não consigo ativar as legendas .. e o botão para legendas não '

Estes são apenas exemplos abreviados, os clientes costumam descrevê-lo com mais detalhes .. pelas descrições e comportamentos descritos neles, presumo que o aplicativo em particular tenha uma falha no FSM.

Portanto, a questão final é como posso evitar isso e como proteger o sistema de se bloquear?

EDIT> Estou falando no contexto da visão de um controlador de exibição no telefone, quero dizer uma parte do aplicativo. Entendo o padrão MVC, tenho módulos separados para funcionalidades distintas ... tudo o que descrevo é relevante para uma tela na interface do usuário.


2
Parece um caso para testes de unidade!
Michael K

Respostas:


7

Tenho certeza que você já sabe disso, mas por precaução:

  1. Verifique se todos os nós no diagrama de estado possuem um arco de saída para TODOS os tipos legais de entrada (ou divida as entradas em classes, com um arco de saída para cada classe de entrada).

    Todo exemplo que eu vi de uma máquina de estado usa apenas um arco de saída para QUALQUER entrada incorreta.

    Se não houver resposta definitiva sobre o que uma entrada fará sempre, é uma condição de erro ou há outra entrada ausente (que deve resultar consistentemente em um arco para as entradas que vão para um novo nó).

    Se um nó não tiver um arco para um tipo de entrada, é uma suposição de que a entrada nunca ocorrerá na vida real (essa é uma condição de erro potencial que não seria tratada pela máquina de estado).

  2. Certifique-se de que a máquina de estado possa receber ou seguir apenas UM arco em resposta à entrada recebida (não mais que um arco).

    Se houver diferentes tipos de cenários de erro ou tipos de entradas que não puderem ser identificadas no tempo de design da máquina de estado, os cenários de erro e as entradas desconhecidas deverão se transformar em um estado com um caminho completamente separado e separado dos "arcos normais".

    IE, se um erro ou desconhecido for recebido em qualquer estado "conhecido", os arcos seguidos como resultado da entrada de tratamento de erros / entrada desconhecida não devem retornar a nenhum estado em que a máquina estaria se recebesse apenas entradas conhecidas.

  3. Depois de atingir um estado terminal (final), você não poderá retornar a um não terminal apenas para o estado inicial (inicial).

  4. Para uma máquina de estado, não deve haver mais de um estado inicial ou inicial (com base nos exemplos que eu vi).

  5. Com base no que vi, uma máquina de estado pode representar apenas o estado de um problema ou cenário.
    Nunca deve haver vários estados possíveis ao mesmo tempo em um diagrama de estados.
    Se eu vejo o potencial de vários estados simultâneos, isso indica que eu preciso dividir o diagrama de estados em duas ou mais máquinas de estados separadas que têm o potencial de cada estado ser modificado independentemente.


9

O objetivo de uma máquina de estados finitos é que ela possui regras explícitas para tudo o que pode acontecer em um estado. É por isso que é finito .

Por exemplo:

if a:
  print a
elif b:
  print b

Não é finito, porque poderíamos obter informações c. Este:

if a:
  print a
elif b:
  print b
else:
  print error

é finito, porque toda a entrada possível é contabilizada. Isso explica as possíveis entradas para um estado , que podem ser separadas da verificação de erros. Imagine uma máquina de estados com os seguintes estados:

No money state. 
Not enough money state.
Pick soda state.

Dentro dos estados definidos, todas as entradas possíveis são tratadas pelo dinheiro inserido e o refrigerante selecionado. A falha de energia está fora da máquina do estado e "não existe". Uma máquina de estado só pode manipular entradas para estados que ela possui, então você tem duas opções.

  1. Garanta que todas as ações sejam atômicas. A máquina pode ter perda total de energia e ainda deixar tudo em um estado estável e correto.
  2. Estenda seus estados para incluir problemas desconhecidos e faça com que erros o levem a esse estado, onde os problemas são tratados.

Para referência, o artigo da wiki sobre máquinas de estado é completo. Sugiro também o Code Complete , para os capítulos sobre a criação de software estável e confiável.


"Poderíamos obter a entrada c" - é por isso que as linguagens seguras são tão importantes. Se o seu tipo de entrada é um bool, você pode obter truee false, mas nada mais. Mesmo assim, é importante entender seus tipos - por exemplo, tipos anuláveis, de ponto flutuante NaN, etc.
MSalters

5

Expressões regulares são implementadas como máquinas de estados finitos. A tabela de transição gerada pelo código da biblioteca terá um estado de falha incorporado, para lidar com o que acontece se a entrada não corresponder ao padrão. Há pelo menos uma transição implícita para o estado de falha de quase todos os outros estados.

As gramáticas da linguagem de programação não são FSMs, mas os geradores de analisadores (como Yacc ou bison) geralmente têm uma maneira de inserir um ou mais estados de erro, para que entradas inesperadas possam fazer com que o código gerado termine em um estado de erro.

Parece que o FSM precisa de um estado de erro ou de falha ou o equivalente moral, além de transições explícitas (para casos que você antecipa) e implícitas (para casos que você não antecipa) para um dos estados de falha ou erro.


Perdoe-me, se minha pergunta parecer tola, pois não tenho formação formal em ciências da computação e estou apenas aprendendo programação por alguns meses. Isso significa que, quando permiti dizer um método manipulador, para um evento pressionado por um botão, e nesse método eu tenho uma estrutura moderadamente complicada de se-outro-interruptor (20 a 30 linhas de código), que Eu sempre deveria lidar com entradas não desejáveis ​​explicitamente? OU você quer dizer isso em um nível "global"? Devo ter uma classe separada assistindo esse FSM e, quando o problema ocorrer, ele redefinirá os valores e estados?
Earl Grey

Explicar os analisadores gerados por Yacc ou Bison está além de mim, mas geralmente você lida com casos conhecidos e, em seguida, possui um pequeno bloco de código para "tudo o resto passa ao estado de erro ou falha". O código para o estado de erro / falha faria toda a redefinição. Talvez você precise ter um valor extra que indique por que você chegou ao estado de falha.
precisa

Seu FSM deve ter pelo menos um estado para erros ou vários estados para diferentes tipos de erros.
Whatsisname

3

A melhor maneira de evitar isso é o teste automatizado .

A única maneira de ter confiança real sobre o que seu código faz com base em determinadas entradas é testá-las. Você pode clicar em seu aplicativo e tentar fazer as coisas incorretamente, mas isso não é dimensionado bem para garantir que você não tenha nenhuma regressão. Em vez disso, você pode criar um teste que transmita entrada incorreta em um componente do seu código e garanta que ele lide com isso de maneira sã.

Isso não poderá provar que a máquina de estado nunca poderá ser quebrada, mas lhe dirá que muitos dos casos comuns são tratados corretamente e não quebram outras coisas.


2

o que você está procurando é o tratamento de exceções. Uma filosofia de design para evitar permanecer em um estado inconsistente é documentada como the way of the samurai: retorne vitorioso ou não retorne. Em outras palavras: um componente deve verificar todas as suas entradas e garantir que seja capaz de processá-las normalmente. Não é esse o caso, deve gerar uma exceção contendo informações úteis.

Depois que uma exceção é gerada, ela borbulha a pilha. Você deve definir uma camada de tratamento de erros que saberá o que fazer. Se um arquivo do usuário estiver corrompido, explique ao seu cliente que os dados foram perdidos e recrie um arquivo vazio e limpo.

A parte importante aqui é voltar ao estado de funcionamento e evitar a propagação de erros. Quando terminar, você pode trabalhar em componentes individuais para torná-los mais robustos.

Eu não sou um especialista em objetivo-c, mas esta página deve ser um bom ponto de partida:

http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocExceptionHandling.html


1

Esqueça sua máquina de estado finito. O que você tem aqui é uma grave situação multithread . Qualquer botão pode ser pressionado a qualquer momento e seus gatilhos externos podem disparar a qualquer momento. Os botões provavelmente estão todos em um segmento, mas um gatilho pode disparar literalmente no mesmo instante que um dos botões ou um, muitos ou todos os outros gatilhos.

O que você deve fazer é determinar seu estado no momento em que você decide agir. Obtenha todos os botões e estados de disparo. Salve-os em variáveis locais . Os valores originais podem mudar toda vez que você os olha. Em seguida, aja conforme a situação. É um instantâneo de como o sistema olhou em um ponto. Um milésimo de segundo depois, pode parecer muito diferente, mas com o multi-threading, não existe um "agora" atual atual no qual você possa se apoiar, apenas uma imagem que você salvou em variáveis ​​locais.

Então você deve responder ao seu estado histórico - salvo. Tudo está consertado e você deve ter uma ação para todos os estados possíveis. Ele não levará em consideração as alterações feitas entre o momento em que você tirou o instantâneo e o tempo em que você exibiu seus resultados, mas essa é a vida no mundo multithreading. E pode ser necessário usar a sincronização para evitar que o seu instantâneo fique desfocado demais. (Você não pode estar atualizado, mas pode chegar perto de obter todo o seu estado a partir de um instante específico no tempo.)

Leia sobre multiencadeamento. Você tem muito a aprender. E por causa desses gatilhos, acho que você não pode usar muitos truques frequentemente fornecidos para facilitar o processamento paralelo ("Threads de Trabalho" e outros). Você não está fazendo "processamento paralelo"; você não está tentando usar 75% dos 8 núcleos. Você está usando 1% de toda a CPU, mas possui threads altamente independentes e altamente interagentes, e será necessário muito pensamento para sincronizá-los e impedir que a sincronização bloqueie o sistema.

Teste em máquinas de um e vários núcleos; Descobri que eles se comportam de maneira bastante diferente com o multi-threading. As máquinas de núcleo único geram menos bugs com vários threads, mas esses erros são muito mais estranhos. (Embora as máquinas com vários núcleos incomodem sua mente até você se acostumar com elas.)

Um último pensamento desagradável: isso não é coisa fácil de testar. Você precisará gerar gatilhos aleatórios e pressionar o botão e deixar o sistema funcionar por um tempo para ver o que acontece. Código multiencadeado não é determinístico. Algo pode falhar uma vez em um bilhão de execuções, apenas porque o tempo estava fora de um nanossegundo. Coloque instruções de depuração (com instruções if cuidadosas para evitar 999.999.999 mensagens desnecessárias) e você precisará fazer um bilhão de execuções apenas para obter uma mensagem útil. Felizmente, as máquinas são bem rápidas atualmente.

Desculpe despejar tudo isso em você tão cedo em sua carreira. Espero que alguém encontre outra resposta com uma maneira de contornar tudo isso (acho que existem coisas por aí que podem domar os gatilhos, mas você ainda tem o conflito do gatilho / botão). Nesse caso, pelo menos essa resposta permitirá que você saiba o que está perdendo. Boa sorte.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.