Paul Lam.
Impacto social de engenharia.
Impacto social de engenharia.
Máquina de estado finito orientada a eventos para um sistema de comércio distribuído.
Um problema que eu tive ao construir meu sistema de comércio distribuído é gerenciar estados de forma assíncrona de múltiplos desencadeantes. Por exemplo, quando o motor alfa diz comprar, ele precisa de confirmação do motor de posição para ver se é seguro entrar em uma nova posição. Eu poderia encadear um cheque após outro de forma imperativa ou através de retorno de chamada. No entanto, a restrição subjacente é que estes desencadeiam:
são intensivos em recursos para gerar, talvez precisem compor muitos deles, não seqüenciais ou têm uma dependência individual, e, o mais importante, estão em programas separados ou em máquinas diferentes.
Assim, optei por abstrair esse problema em seu próprio módulo do sistema como uma máquina de estados finitos (FSM) dirigida a eventos para acompanhar transições de estado. Termo de intimidação, mas minha primeira implementação foi apenas declarações if-else para se qualificar como tal. O benefício é que cada um dos componentes do meu sistema só precisa empurrar sinais e extrair estados de uma interface central sem ter que se preocupar com o que deveria chamar a seguir ou pesquisar qualquer outra coisa para ver se as estrelas estão alinhadas. Isso simplificou drasticamente o desenvolvimento e a manutenção.
As responsabilidades do meu módulo FSM são:
ouça todos os sinais, descubra todas as transições e publique os estados mais recentes para o resto do sistema.
Manipulação de eventos assíncronos.
Eu uso RabbitMQ como a camada de transporte de mensagens entre os módulos do meu sistema. Tudo o que preciso fazer aqui é associar um manipulador de mensagens apropriado a cada entrada de disparo para o FSM. Aqui está um exemplo dos manipuladores de eventos usando a biblioteca Clojure RabbitMQ, Langohr. O resto desta parte é apenas o padrão RabbitMQ publicar / inscrever-se.
Isso é chamado quando um evento de posição é recebido com informações como usuário, instrumento e quantidade. Este manipulador iria encaminhar essas informações buscando estados atuais para esse usuário, avaliando o próximo estado com entrada e, em seguida, armazene os novos estados para o usuário.
Transições do estado.
Abaixo está um dos diagramas de transição de estado do meu sistema.
Existem 4 estados representados por 4 cores com 4 estados de sinalização de transição de estado. Espera-se que o programa atenda até centenas de estados independentes simultaneamente com os disparadores de eventos que chegam em algumas ocasiões por segundo.
Como eu dizia, minha primeira implementação é apenas um conjunto de métodos if-else. Por exemplo, um gatilho de engate chamaria o método envolvente para determinar o próximo estado, dado o envolvimento implícito e o estado atual.
Havia um punhado desse código de referência. Então, depois de implantar o sistema, voltei a refatorá-los. Eu tenho significado dar uma experiência no core. logic por um tempo, então isso parece ser um bom lugar para começar a usá-lo.
Antes de podermos perguntar a questão do solucionador de lógica, precisamos definir as relações. Aqui eu defino uma relação de transição para especificar toda a definição de transição de estado convenientemente em um só lugar.
E os métodos do manipulador de eventos são apenas invólucros para uma expressão de lógica de um verso, perguntando a pergunta - dado o estágio atual, cur-state e a entrada de entrada, entrada, que estado pode tomar para satisfazer essa restrição?
Não é o exemplo core. logic mais ilustrativo, mas faz o trabalho.
Começar com o core. logic é surpreendentemente fácil. Eu passei pelo Primer e tutorial e consegui isso trabalhando em uma tentativa.
Armazenamento em cache e compartilhamento de estado.
Agora que a transição do estado foi atendida, os estados são armazenados em cache e são atendidos no Redis para outras partes do sistema. Eu uso o Redis para isso porque é rápido e fácil. Os valores são armazenados em formato edn em vez de algo mais popular como o JSON para manter a estrutura de dados através do fio.
Esta é a minha primeira vez usando o edn em produção. Todas as mensagens interprocessamento neste sistema comercial são formatadas em formato edn. Funciona perfeitamente com Clojure simplesmente usando str para escrever e clojure. edn / read-string para ler. Além dos meus outros componentes do Clojure no sistema, minha interface do corretor comercial está escrita em Java. O meu programa Java usa o edn-java para analisar e desproporcionar estruturas complexas de dados Clojure (por exemplo, mapas aninhados com palavras-chave).
Eu acho que o acoplamento edn com a Redis é uma escolha fantástica, pois é quase como trabalhar com as estruturas de dados de concorrência originais de Clojure, como átomo, mas também permitir que programas externos acessem os dados.
Simples e rápidos.
Todo o programa FSM dirigido por eventos é inferior a 200 linhas do código Clojure e não levou mais do que algumas horas para fazer. No entanto, fiz alguns momentos de reflexão por alguns dias. Não fiz qualquer referência para estimar o resultado do desempenho. Então, tudo o que posso dizer é que esta configuração pode lidar com meu caso de uso simplista com quase nenhuma carga no servidor, então eu estou feliz com isso.
Alguns anos atrás, eu teria definido um monte de bandeiras para mudar estados. Na verdade, é isso que eu fiz. A maior satisfação aqui para mim não é a implementação ou tecnologias, é ver através do problema subjacente em questão e resolvê-lo com um padrão comum que tornou meu trabalho mais simples.
algotrading.
59 & # 32; пользователей находятся здесь.
МОДЕРАТОРЫ.
Bem-vindo ao Reddit,
a primeira página da internet.
e inscreva-se em uma das milhares de comunidades.
Это архивированный пост. Você está interessado em иолосовать или комментировать.
Quer adicionar à discussão?
помощь правила сайта центр поддержки вики реддикет mod guidelines связаться с нами.
приложенияи инструменты Reddit para iPhone Reddit para o site móvel Android кнопки.
Использование данного сайта означает, что вы принимаете & # 32; пользовательского соглашения & # 32; и & # 32; Политика конфиденциальности. &cópia de; 2018 reddit инкорпорейтед. Все права защищены.
REDDIT e o logotipo ALIEN são marcas registradas da reddit inc.
& pi; Renderizado pelo PID 112576 em & # 32; app-481 & # 32; em 2018-01-13 05: 51: 02.079666 + 00: 00 executando b995ef9 código do país: UA.
Aprenda-lhe alguns Erlang.
Rage Against The Finite-State Machines.
O que eles são?
Uma máquina de estados finitos (FSM) não é realmente uma máquina, mas tem um número finito de estados. Eu sempre encontrei máquinas de estados finitos mais fáceis de entender com gráficos e diagramas. Por exemplo, o seguinte seria um diagrama simplista para um cão (muito burro) como uma máquina de estado:
Aqui, o cão tem 3 estados: sentado, latindo ou abanando a cauda. Diferentes eventos ou entradas podem forçá-lo a mudar seu estado. Se um cão está calmamente sentado e vê um esquilo, ele começará a latir e não vai parar até você acariciá-lo de novo. No entanto, se o cão estiver sentado e você pet, não temos idéia do que pode acontecer. No mundo de Erlang, o cão pode entrar em colisão (e, eventualmente, ser reiniciado por seu supervisor). No mundo real, seria um evento estranho, mas seu cão voltaria depois de ser atropelado por um carro, então não é tudo ruim.
Aqui está o diagrama de estado de um gato para uma comparação:
Este gato tem um único estado, e nenhum evento pode alterá-lo.
Implementar a máquina de estado de gato em Erlang é uma tarefa divertida e simples:
Podemos tentar o módulo para ver que o gato realmente nunca dá uma porcaria:
O mesmo pode ser feito para o cão FSM, exceto que mais estados estão disponíveis:
Deve ser relativamente simples combinar cada um dos estados e as transições para o que estava no diagrama acima. Aqui está o FSM em uso:
Você pode acompanhar o esquema se quiser (eu costumo fazer, ajuda a ter certeza de que nada está errado).
Esse é realmente o núcleo dos FSMs implementados como processos Erlang. Há coisas que poderiam ter sido feitas de forma diferente: poderíamos ter estado passado nos argumentos das funções do estado de forma semelhante ao que fazemos com o loop principal dos servidores. Também poderíamos ter adicionado funções de inicialização e término, manipulações de código, etc.
Outra diferença entre o cão eo gato FSMs é que os eventos do gato são síncronos e os eventos do cão são assíncronos. Em um FSM real, ambos poderiam ser usados de forma mista, mas eu fui para a representação mais simples da pura preguiça inexplorada. Existem outras formas de evento que os exemplos não mostram: eventos globais que podem acontecer em qualquer estado.
Um exemplo de tal evento pode ser quando o cão recebe um cheiro de comida. Uma vez que o evento de comida de cheiro é desencadeado, não importa o estado em que o cão está, ele iria procurar a fonte de alimento.
Agora, não vamos gastar muito tempo implementando tudo isso em nosso FSM 'escrito em um guardanapo'. Em vez disso, nos moveremos diretamente para o comportamento gen_fsm.
Máquinas genéricas de finito-estado.
O comportamento gen_fsm é algo semelhante ao gen_server na medida em que é uma versão especializada dele. A maior diferença é que, ao invés de lidar com chamadas e lançamentos, estamos lidando com eventos síncronos e assíncronos. Como nos exemplos de cães e gatos, cada estado é representado por uma função. Novamente, passaremos pelas devoluções de chamada que nossos módulos precisam implementar para funcionar.
Este é o mesmo init / 1 usado para servidores genéricos, exceto os valores de retorno aceitos são, e. A ups da parada funciona da mesma maneira que para o gen_server s, e hibernar e Timeout mantêm a mesma semântica.
O que há de novo é a variável StateName. StateName é um átomo e representa a próxima função de retorno de chamada a ser chamada.
As funções StateName / 2 e StateName / 3 são nomes de espaço reservado e você deve decidir o que serão. Vamos supor que a função init / 1 retorna a tupla. Isso significa que a máquina de estados finitos ficará sentada. Este não é o mesmo tipo de estado que vimos com o gen_server; é bastante equivalente aos estados de sentar-se, cascarão e wag_tail do anterior cão FSM. Esses estados determinam um contexto em que você lida com um determinado evento.
Um exemplo disso seria alguém que o chamasse no seu telefone. Se você estiver no estado "dormindo em uma manhã de sábado", sua reação pode ser gritar no telefone. Se o seu estado está "esperando uma entrevista de emprego", é provável que você escolha o telefone e responda educadamente. Por outro lado, se você estiver no estado 'morto', então estou surpreso por você mesmo ler esse texto.
De volta ao nosso FSM. A função init / 1 disse que devemos estar no estado sentado. Sempre que o processo gen_fsm recebe um evento, será chamada a função sentada / 2 ou a sessão / 3. A função assento / 2 é chamada para eventos assíncronos e sessão / 3 para sincronizados.
Os argumentos para sentar / 2 (ou geralmente StateName / 2) são Evento, a mensagem real enviada como um evento e StateData, os dados que foram transferidos para as chamadas. sentado / 2 pode então retornar as tuplas, e.
Os argumentos para sentar / 3 são semelhantes, exceto que existe uma variável De entre o evento e StateData. A variável From é usada exatamente da mesma maneira que foi para gen_server s, incluindo gen_fsm: reply / 2. As funções StateName / 3 podem retornar as seguintes tuplas:
Observe que não há limite em quantas dessas funções você pode ter, desde que sejam exportadas. Os átomos retornados como NextStateName nas tuplas determinarão se a função será chamada ou não.
handle_event.
Na última seção, mencionei eventos globais que desencadeariam uma reação específica, independentemente do estado em que estivéssemos (o alimento de cheiro de cachorro vai soltar o que quer que esteja fazendo e, em vez disso, buscará comida). Para esses eventos que devem ser tratados da mesma maneira em todos os estados, o retorno de chamada handle_event / 3 é o que deseja. A função leva argumentos semelhantes a StateName / 2 com a exceção de que ele aceita uma variável StateName entre eles, informando qual o estado quando o evento foi recebido. Ele retorna os mesmos valores que StateName / 2.
handle_sync_event.
O retorno de chamada handle_sync_event / 4 é para StateName / 3 que handle_event / 2 é para StateName / 2. Ele lida com eventos globais síncronos, leva os mesmos parâmetros e retorna o mesmo tipo de tuplas que StateName / 3.
Agora, pode ser um bom momento para explicar como sabemos se um evento é global ou se pretende ser enviado para um estado específico. Para determinar isso, podemos observar a função usada para enviar um evento para o FSM. Os eventos assíncronos destinados a qualquer função StateName / 2 são enviados com send_event / 2, os eventos síncronos a serem capturados pelo StateName / 3 devem ser enviados com sync_send_event / 2-3.
As duas funções equivalentes para eventos globais são send_all_state_event / 2 e sync_send_all_state_event / 2-3 (um nome bastante longo).
code_change.
Isso funciona exatamente o mesmo que para o gen_server s, exceto que é necessário um parâmetro de estado extra quando chamado como code_change (OldVersion, StateName, Data, Extra) e retorna uma tupla do formulário.
Isso deve, novamente, agir um pouco como o que temos para servidores genéricos. terminar / 3 deve fazer o oposto do init / 1.
Uma especificação do sistema de negociação.
É hora de colocar tudo isso em prática. Muitos tutoriais Erlang sobre máquinas finitas utilizam exemplos que contêm interruptores telefônicos e coisas similares. É meu palpite que a maioria dos programadores raramente terá que lidar com telefones para máquinas de estado. Por isso, vamos olhar para um exemplo mais apropriado para muitos desenvolvedores: vamos projetar e implementar um sistema de negociação de itens para alguns jogos de vídeo fictícios e não-existentes.
O design que escolhi é um pouco desafiador. Ao invés de usar um corretor através do qual os jogadores rodam itens e confirmações (o que, francamente, seria mais fácil), vamos implementar um servidor onde ambos os jogadores se falam diretamente (o que teria a vantagem de ser distribuível).
Porque a implementação é complicada, vou passar um bom tempo ao descrevê-lo, o tipo de problemas a serem enfrentados e as formas de corrigi-los.
Em primeiro lugar, devemos definir as ações que podem ser feitas pelos nossos jogadores ao negociar. O primeiro está pedindo a criação de um comércio. O outro usuário também deve ser capaz de aceitar esse comércio. Não vamos dar-lhes o direito de negar um comércio, no entanto, porque queremos manter as coisas simples. Serão fáceis de adicionar esse recurso uma vez que tudo for feito.
Uma vez que o comércio é configurado, nossos usuários devem ser capazes de negociar uns com os outros. Isso significa que eles devem ser capazes de fazer ofertas e depois retraí-las se quiserem. Quando ambos os jogadores estão satisfeitos com a oferta, eles podem se declarar todos prontos para finalizar o comércio. Os dados devem então ser guardados em algum lugar dos dois lados. Em qualquer momento, também deve ter sentido para qualquer um dos jogadores cancelar todo o comércio. Alguns pleb poderiam oferecer apenas itens considerados indignos para a outra parte (quem pode estar muito ocupado) e, portanto, deve ser possível revistá-los com um merecido cancelamento.
Em suma, as seguintes ações devem ser possíveis:
pedir um comércio aceitar uma oferta comercial itens retrair uma oferta declarar-se como pronto cancelar brutalmente o comércio.
Agora, quando cada uma dessas ações é tomada, o FSM do outro jogador deve ser informado. Isso faz sentido, porque quando Jim conta ao FSM para enviar um item para o Carl, o FSM da Carl deve ser informado disso. Isso significa que ambos os jogadores podem conversar com seus próprios FSM, que falarão com o FSM do outro. Isso nos dá algo assim:
A primeira coisa a notar quando temos dois processos idênticos que se comunicam entre si é que devemos evitar chamadas síncrona tanto quanto possível. A razão para isso é que se o FSM de Jim envia uma mensagem para o FSM de Carl e então espera sua resposta enquanto, ao mesmo tempo, o FSM de Carl envia uma mensagem para o FSM de Jim e aguarda sua própria resposta específica, ambos acabam esperando o outro sem responder nunca. Isso efetivamente congela ambos os EFMs. Temos um impasse.
Uma solução para isso é aguardar um tempo limite e, em seguida, seguir em frente, mas haverá sobras de mensagens nas caixas de correio de ambos os processos e o protocolo será desarrumado. Isso certamente é uma lata de vermes, e então queremos evitá-lo.
A maneira mais simples de fazê-lo é evitar todas as mensagens síncronas e ficar totalmente assíncrono. Note que Jim ainda pode fazer uma chamada síncrona para o seu próprio FSM; não há risco aqui porque o FSM não precisará chamar Jim e, portanto, não pode ocorrer nenhum impasse entre eles.
Quando dois desses FSMs se comunicam entre si, toda a troca pode parecer um pouco assim:
Ambos os FSM estão em estado ocioso. Quando você pede que o Jim troque, Jim tem que aceitar antes que as coisas mudem. Então, ambos podem oferecer itens ou retirá-los. Quando você está se declarando preparado, o comércio pode ocorrer. Esta é uma versão simplificada de tudo o que pode acontecer e veremos todos os casos possíveis com mais detalhes nos próximos parágrafos.
Aqui vem a parte difícil: definir o diagrama de estado e como as transições de estados acontecem. Normalmente, um bom pensamento reflete isso, porque você tem que pensar em todas as pequenas coisas que podem dar errado. Algumas coisas podem dar errado, mesmo depois de terem sido revisadas muitas vezes. Por isso, vou simplesmente colocar o que eu decidi implementar aqui e depois explicá-lo.
Em primeiro lugar, ambas as máquinas de estado finito começam no estado ocioso. Neste ponto, uma coisa que podemos fazer é pedir a algum outro jogador para negociar conosco:
Nós entramos no modo idle_wait para aguardar uma eventual resposta após o FSM encaminhar a demanda. Uma vez que o outro FSM envia a resposta, o nosso pode mudar para negociar:
O outro jogador também deve estar em negociação depois disso. Obviamente, se pudermos convidar o outro, o outro pode nos convidar. Se tudo correr bem, isso deve acabar ficando assim:
Então, isso é praticamente o oposto, como os dois diagramas de estados anteriores empacotados em um. Note que esperamos que o jogador aceite a oferta neste caso. O que acontece se, por pura sorte, pedimos ao outro jogador trocar conosco, ao mesmo tempo em que nos pede que troquemos?
O que acontece aqui é que ambos os clientes perguntam ao seu próprio FSM para negociar com o outro. Assim que as mensagens de negociação for enviada, ambos os FSMs mudam para o estado idle_wait. Então eles serão capazes de processar a questão da negociação. Se analisarmos os diagramas de estados anteriores, vemos que essa combinação de eventos é a única vez que receberemos pergunte negociar mensagens enquanto estiver no estado idle_wait. Consequentemente, sabemos que obter essas mensagens em idle_wait significa que atingimos a condição de corrida e podemos assumir que ambos os usuários querem conversar um com o outro. Podemos mover os dois para negociar o estado. Hooray.
Então, agora estamos negociando. De acordo com a lista de ações que listei anteriormente, devemos apoiar os usuários que oferecem itens e depois retrair a oferta:
Tudo isso faz é encaminhar a mensagem do nosso cliente para o outro FSM. As duas máquinas de estados finitos precisarão manter uma lista de itens oferecidos por qualquer jogador, para que eles possam atualizar essa lista ao receber essas mensagens. Nós ficamos no estado de negociação depois disso; talvez o outro jogador também ofereça itens:
Aqui, nosso FSM basicamente age de forma semelhante. Isto é normal. Uma vez que nos cansamos de oferecer coisas e achamos que somos generosos o suficiente, temos que dizer que estamos prontos para oficializar o comércio. Como temos que sincronizar ambos os jogadores, teremos que usar um estado intermediário, como fizemos para ocioso e idle_wait:
O que fazemos aqui é que, assim que nosso jogador estiver pronto, nosso FSM pede o FSM de Jim se ele estiver pronto. Enquanto aguarda a resposta, nosso próprio FSM cai no seu estado de espera. A resposta que receberemos dependerá do estado do FSM de Jim: se estiver em espera, ele nos informará que está pronto. Caso contrário, ele vai nos dizer que ainda não está pronto. É exatamente isso que nosso FSM responde automaticamente a Jim, se ele nos perguntar se estamos prontos quando estamos no estado de negociação:
Nossa máquina de estado finito permanecerá no modo de negociação até que nosso jogador diga que está pronto. Vamos assumir que ele fez e estamos agora no estado de espera. No entanto, Jim ainda não está. Isso significa que, quando nos declararmos como prontos, teremos perguntado a Jim se ele também estava pronto e seu FSM terá respondido "ainda não":
Ele não está pronto, mas nós somos. Não podemos fazer muito, mas continuar aguardando. Enquanto espera por Jim, que ainda está negociando, é possível que ele tente nos enviar mais itens ou talvez cancele suas ofertas anteriores:
Claro, queremos evitar Jim removendo todos os seus itens e depois clicando em "Estou pronto!", Nos aparecendo no processo. Assim que ele muda os itens oferecidos, voltamos para negociar o estado para que possamos modificar nossa própria oferta, ou examinar a atual e decidir que estamos prontos. Enxague e repita.
Em algum momento, Jim estará pronto para finalizar o comércio também. Quando isso acontece, sua máquina de estados finitos perguntará a nós se estivermos prontos:
O que o FSM faz é responder que estamos realmente prontos. Nós ficamos no estado de espera e nos recusamos a mudar para o estado pronto. Por que é isso? Porque existe uma condição de corrida potencial! Imagine que a seguinte sequência de eventos ocorre, sem fazer este passo necessário:
Isso é um pouco complexo, então vou explicar. Por causa da maneira como as mensagens são recebidas, possamos apenas processar a oferta do item depois de nos declararmos prontos e também depois que Jim se declarou pronto. Isso significa que assim que lemos a mensagem da oferta, voltamos para negociar o estado. Durante esse tempo, Jim nos contará que está pronto. Se ele mudasse os estados ali mesmo e se movesse para pronto (como ilustrado acima), ele seria pego esperando indefinidamente enquanto não saberíamos o que diabos fazer. Isso também pode acontecer ao contrário Ugh.
Uma maneira de resolver isso é adicionando uma camada de indireção (Obrigado a David Wheeler). É por isso que ficamos no modo de espera e enviamos 'pronto!' (como mostrado em nosso diagrama de estado anterior). Veja como lidamos com isso "pronto". mensagem, supondo que já estávamos no estado pronto porque dissemos ao nosso FSM que estávamos preparados de antemão:
Quando recebemos 'pronto!' dos outros FSM, enviamos 'pronto!' de volta. Isto é para se certificar de que não teremos a "condição de corrida dupla" mencionada acima. Isso criará um "pronto" supérfluo! mensagem em um dos dois FSMs, mas teremos que ignorá-lo neste caso. Em seguida, enviamos uma mensagem "ack" (e o FSM do Jim fará o mesmo) antes de mudar para o estado pronto. A razão pela qual essa mensagem 'ack' existe é devido a alguns detalhes de implementação sobre sincronização de clientes. Coloquei o diagrama por razões de correção, mas não vou explicar isso até mais tarde. Esqueça disso por enquanto. Finalmente conseguimos sincronizar ambos os jogadores. Whew.
Então agora está o estado pronto. Este é um pouco especial. Ambos os jogadores estão prontos e, basicamente, têm dado às máquinas finitas todo o controle de que precisam. Isso nos permite implementar uma versão bastardizada de um compromisso de duas fases para garantir que as coisas funcionem bem ao fazer o funcionário oficial:
Nossa versão (como descrito acima) será bastante simplista. Escrever um commit de duas fases verdadeiramente correto exigiria muito mais código do que o que é necessário para que possamos entender as máquinas de estados finitos.
Finalmente, só temos que permitir que o comércio seja cancelado a qualquer momento. Isso significa que, de alguma forma, independentemente do estado em que estejamos, vamos ouvir a mensagem "cancelar" dos dois lados e sair da transação. Também deve ser uma cortesia comum para que o outro lado saiba que nos foi antes de partir.
Bem! É uma grande quantidade de informações para absorver de uma só vez. Não se preocupe se demorar um pouco para compreendê-lo completamente. Levou um monte de pessoas para examinar meu protocolo para ver se estava certo e, mesmo assim, todos nós perdemos algumas condições de corrida que eu peguei alguns dias depois ao revisar o código ao escrever este texto. É normal ter que lê-lo mais de uma vez, especialmente se você não está acostumado a protocolos assíncronos. Se for esse o caso, eu encorajo você a tentar projetar seu próprio protocolo. Então, pergunte-se "o que acontece se duas pessoas fizerem as mesmas ações de forma muito rápida? E se encadearem outros dois eventos rapidamente? O que eu faço com as mensagens que eu não lido quando mudo estados?" Você verá que a complexidade cresce muito rápido. Você pode encontrar uma solução semelhante à minha, possivelmente melhor (deixe-me saber se este é o caso!) Não importa o resultado, é uma coisa muito interessante para trabalhar e nossos FSMs ainda são relativamente simples.
Depois de ter digerido tudo isso (ou antes, se você é um leitor rebelde), você pode ir para a próxima seção, onde implementamos o sistema de jogos. Por enquanto, você pode fazer uma boa pausa para o café se você quiser fazê-lo.
Negociação de jogos entre dois jogadores.
A primeira coisa que precisa ser feita para implementar nosso protocolo com o gen_fsm da OTP é criar a interface. Haverá 3 chamadores para o nosso módulo: o jogador, o comportamento gen_fsm e o FSM do outro jogador. Só precisamos exportar a função do jogador e as funções gen_fsm. Isso ocorre porque o outro FSM também será executado dentro do módulo trade_fsm e pode acessá-los de dentro:
Então essa é a nossa API. Você pode ver que estou planejando que algumas funções sejam síncronas e assíncronas. Isso é principalmente porque queremos que nosso cliente nos ligue de forma síncrona em alguns casos, mas o outro FSM pode fazê-lo de forma assíncrona. Ter o cliente síncrono simplifica muito nossa lógica, limitando o número de mensagens contraditórias que podem ser enviadas uma após a outra. Vamos lá. Vamos primeiro implementar a API pública real de acordo com o protocolo definido acima:
Isso é bastante padrão; Todas estas funções 'gen_fsm' foram cobertas antes (exceto start / 3-4 e start_link / 3-4 que eu acredito que você pode descobrir) neste capítulo.
Em seguida, implementaremos as funções FSM para FSM. Os primeiros têm a ver com as configurações de comércio, quando queremos primeiro pedir ao outro usuário que se junte a nós em um comércio:
A primeira função pede ao outro pid se eles querem trocar, e o segundo é usado para responder (de forma assíncrona, é claro).
Podemos então escrever as funções para oferecer e cancelar ofertas. De acordo com o nosso protocolo acima, é como eles deveriam ser:
Então, agora que temos essas chamadas feitas, precisamos nos concentrar no resto. As chamadas restantes dizem respeito a estarem prontas ou não e ao tratamento da confirmação final. Novamente, dado nosso protocolo acima, temos três chamadas: are_you_ready, que pode ter as respostas não_yet ou pronto! :
As únicas funções restantes são aquelas que devem ser usadas por ambos os FSMs ao fazer o commit no estado pronto. Seu uso preciso será descrito mais detalhadamente mais adiante, mas, por enquanto, os nomes e o diagrama de seqüência / estado anteriores devem ser suficientes. No entanto, você ainda pode transcrevê-los para sua própria versão do trade_fsm:
Ah, e também há a função de cortesia que nos permite avisar o outro FSM que cancelamos o comércio:
Agora podemos mudar para a parte realmente interessante: as devoluções de retorno gen_fsm. O primeiro retorno de chamada é init / 1. No nosso caso, queremos que cada FSM segure um nome para o usuário que representa (dessa forma, nossa saída será mais agradável) nos dados que ele continua transmitindo para si próprio. O que mais queremos manter na memória? No nosso caso, queremos o pid do outro, os itens que oferecemos e os itens que o outro oferece. Nós também vamos adicionar a referência de um monitor (então sabemos que abortar se o outro morre) e um de campo, costumavam fazer respostas atrasadas:
No caso do init / 1, só nos importaremos com o nosso nome por agora. Observe que vamos começar no estado ocioso:
As próximas devoluções a serem consideradas seriam os próprios estados. Até agora descrevi as transições de estado e as chamadas que podem ser feitas, mas precisamos de uma maneira de garantir que tudo esteja bem. Em primeiro lugar, escreveremos algumas funções de utilidade:
E podemos começar com o estado ocioso. Por uma questão de convenção, abordarei a versão assíncrona primeiro. Este não precisa se preocupar com nada, exceto o outro jogador que pede uma troca com o nosso próprio jogador, se você olhar para as funções da API, usará uma chamada síncrona:
Um monitor é configurado para nos permitir lidar com o outro morrendo, e sua referência é armazenada nos dados do FSM junto com o pid do outro, antes de passar para o estado idle_wait. Tenha em atenção que informaremos todos os eventos inesperados e ignorá-los ao permanecer no estado em que já estávamos. Podemos ter algumas mensagens fora da banda aqui e ali que poderiam ser o resultado de condições de corrida. Geralmente é seguro ignorá-los, mas não podemos facilmente livrar-se deles. É melhor não bater todo o FSM nestas mensagens desconhecidas, mas um pouco esperadas.
Quando nosso próprio cliente solicita ao FSM que entre em contato com outro jogador para um comércio, ele enviará um evento síncrono. O retorno de marcha lenta / inactiva será necessário:
Procedemos de forma semelhante à versão assíncrona, exceto que precisamos realmente perguntar ao outro lado se eles querem negociar conosco ou não. Você notará que ainda não respondemos ao cliente. Isso porque não temos nada de interessante para dizer, e queremos que o cliente fique trancado e aguarde que o comércio seja aceito antes de fazer qualquer coisa. A resposta só será enviada se o outro lado aceitar uma vez que estamos em idle_wait.
Quando estamos lá, temos que lidar com a outra aceitar negociar e a outra pedindo para negociar (o resultado de uma condição de corrida, conforme descrito no protocolo):
Isso nos dá duas transições para o estado de negociação, mas lembre-se de que devemos usar o gen_fsm: responda / 2 responda ao nosso cliente para dizer que está certo começar a oferecer itens. Há também o caso do cliente do nosso FSM aceitar o comércio sugerido pela outra parte:
Mais uma vez, este avança para o estado de negociação. Aqui, devemos lidar com consultas assíncronas para adicionar e remover itens provenientes tanto do cliente quanto do outro FSM. No entanto, ainda não decidimos como armazenar itens. Porque eu sou um pouco preguiçoso e suponho que os usuários não troquem esses itens, listas simples irão fazê-lo por enquanto. No entanto, podemos mudar de idéia em um ponto posterior, por isso seria uma boa idéia para embrulhar operações de itens em suas próprias funções. Adicione as seguintes funções na parte inferior do arquivo com aviso / 3 e inesperado / 2:
Simples, mas eles têm o papel de isolar as ações (adicionar e remover itens) de sua implementação (usando listas). Poderíamos facilmente mudar para proplistas, arrays ou qualquer outra estrutura de dados sem interromper o resto do código.
Usando ambas as funções, podemos implementar a oferta e remoção de itens:
Este é um aspecto feio de usar mensagens assíncronas em ambos os lados. Um conjunto de mensagens tem a forma 'make' e 'retract', enquanto o outro 'faz' e 'desfazha'. Isso é inteiramente arbitrário e apenas usado para diferenciar entre comunicações de jogador-para-FSM e comunicações de FSM para FSM. Note-se que, naqueles que vêm de nosso próprio jogador, temos que dizer ao outro lado sobre as mudanças que estamos fazendo.
Outra responsabilidade é lidar com a mensagem are_you_ready que mencionamos no protocolo. Este é o último evento assíncrono a manipular no estado de negociação:
Conforme descrito no protocolo, sempre que não estamos no estado de espera e recebemos esta mensagem, devemos responder com not_yet. Foram também a saída de detalhes do comércio para o usuário para que uma decisão possa ser tomada.
Quando essa decisão for tomada e o usuário estiver pronto, o evento pronto será enviado. Este deve ser síncrono porque não queremos que o usuário continue modificando sua oferta, adicionando itens ao alegar que ele está pronto:
Neste ponto, uma transição para o estado de espera deve ser feita. Note que apenas esperar o outro não é interessante. Nós salvamos a variável From para que possamos usá-la com gen_fsm: reply / 2 quando tivermos algo a dizer ao cliente.
O estado de espera é uma besta engraçada. Novos itens podem ser oferecidos e retraídos porque o outro usuário pode não estar pronto. Faz sentido, então, reverter automaticamente para o estado de negociação. Seria bom para oferecer excelentes itens para nós, apenas para o outro para removê-los e declarar-se pronto, roubando nossa pilhagem. Voltando à negociação é uma boa decisão:
Agora, isso é algo significativo e nós respondemos ao jogador com as coordenadas que armazenamos em S # state. from. O próximo conjunto de mensagens com as quais precisamos nos preocupar são aqueles relacionados com a sincronização de ambos os EFM para que eles possam mudar para o estado pronto e confirmar o comércio. Para este, devemos realmente nos concentrar no protocolo definido anteriormente.
As três mensagens que podemos ter são are_you_ready (porque o outro usuário se declarou pronto), not_yet (porque pedimos ao outro se ele estava pronto e ele não estava) e pronto! (porque pedimos ao outro se ele estava pronto e ele estava).
Começaremos com are_you_ready. Lembre-se que, no protocolo, dissemos que poderia haver uma condição de corrida escondida. The only thing we can do is send the ready! message with am_ready/1 and deal with the rest later:
We'll be stuck waiting again, so it's not worth replying to our client yet. Similarly, we won't reply to the client when the other side sends a not_yet to our invitation:
On the other hand, if the other is ready, we send an extra ready! message to the other FSM, reply to our own user and then move to the ready state:
You might have noticed that I've used ack_trans/1 . In fact, both FSMs should use it. Por que é isso? To understand this we have to start looking at what goes on in the ready! state.
When in the ready state, both players' actions become useless (except cancelling). We won't care about new item offers. This gives us some liberty. Basically, both FSMs can freely talk to each other without worrying about the rest of the world. This lets us implement our bastardization of a two-phase commit. To begin this commit without either player acting, we'll need an event to trigger an action from the FSMs. The ack event from ack_trans/1 is used for that. As soon as we're in the ready state, the message is treated and acted upon; the transaction can begin.
Two-phase commits require synchronous communications, though. This means we can't have both FSMs starting the transaction at once, because they'll end up deadlocked. The secret is to find a way to decide that one finite state machine should initiate the commit, while the other will sit and wait for orders from the first one.
It turns out that the engineers and computer scientists who designed Erlang were pretty smart (well, we knew that already). The pids of any process can be compared to each other and sorted. This can be done no matter when the process was spawned, whether it's still alive or not, or if it comes from another VM (we'll see more about this when we get into distributed Erlang).
Knowing that two pids can be compared and one will be greater than the other, we can write a function priority/2 that will take two pids and tell a process whether it's been elected or not:
And by calling that function, we can have one process starting the commit and the other following the orders.
Here's what this gives us when included in the ready state, after receiving the ack message:
This big try . catch expression is the leading FSM deciding how the commit works. Both ask_commit/1 and do_commit/1 are synchronous. This lets the leading FSM call them freely. You can see that the other FSM just goes and wait. It will then receive the orders from the leading process. The first message should be ask_commit . This is just to make sure both FSMs are still there; nothing wrong happened, they're both dedicated to completing the task:
Once this is received, the leading process will ask to confirm the transaction with do_commit . That's when we must commit our data:
And once it's done, we leave. The leading FSM will receive ok as a reply and will know to commit on its own end afterwards. This explains why we need the big try . catch : if the replying FSM dies or its player cancels the transaction, the synchronous calls will crash after a timeout. The commit should be aborted in this case.
Just so you know, I defined the commit function as follows:
Pretty underwhelming, eh? It's generally not possible to do a true safe commit with only two participants—a third party is usually required to judge if both players did everything right. If you were to write a true commit function, it should contact that third party on behalf of both players, and then do the safe write to a database for them or rollback the whole exchange. We won't go into such details and the current commit/1 function will be enough for the needs of this book.
We're not done yet. We have not yet covered two types of events: a player cancelling the trade and the other player's finite state machine crashing. The former can be dealt with by using the callbacks handle_event/3 and handle_sync_event/4 . Whenever the other user cancels, we'll receive an asynchronous notification:
When we do it we must not forget to tell the other before quitting ourselves:
And voilà! The last event to take care of is when the other FSM goes down. Fortunately, we had set a monitor back in the idle state. We can match on this and react accordingly:
Note that even if the cancel or DOWN events happen while we're in the commit, everything should be safe and nobody should get its items stolen.
Note: we used io:format/2 for most of our messages to let the FSMs communicate with their own clients. In a real world application, we might want something more flexible than that. One way to do it is to let the client send in a Pid, which will receive the notices sent to it. That process could be linked to a GUI or any other system to make the player aware of the events. The io:format/2 solution was chosen for its simplicity: we want to focus on the FSM and the asynchronous protocols, not the rest.
Only two callbacks left to cover! They're code_change/4 and terminate/3 . For now, we don't have anything to do with code_change/4 and only export it so the next version of the FSM can call it when it'll be reloaded. Our terminate function is also really short because we didn't handle real resources in this example:
We can now try it. Well, trying it is a bit annoying because we need two processes to communicate to each other. To solve this, I've written the tests in the file trade_calls. erl, which can run 3 different scenarios. The first one is main_ab/0 . It will run a standard trade and output everything. The second one is main_cd/0 and will cancel the transaction halfway through. The last one is main_ef/0 and is very similar to main_ab/0 , except it contains a different race condition. The first and third tests should succeed, while the second one should fail (with a crapload of error messages, but that's how it goes). You can try it if you feel like it.
That Was Quite Something.
If you've found this chapter a bit harder than the others, I must remind you that it's entirely normal. I've just gone crazy and decided to make something hard out of the generic finite-state machine behaviour. If you feel confused, ask yourself these questions: Can you understand how different events are handled depending on the state your process is in? Do you understand how you can transition from one state to the other? Do you know when to use send_event/2 and sync_send_event/2-3 as opposed to send_all_state_event/2 and sync_send_all_state_event/3 ? If you answered yes to these questions, you understand what gen_fsm is about.
The rest of it with the asynchronous protocols, delaying replies and carrying the From variable, giving a priority to processes for synchronous calls, bastardized two-phase commits and whatnot are not essential to understand . They're mostly there to show what can be done and to highlight the difficulty of writing truly concurrent software, even in a language like Erlang. Erlang doesn't excuse you from planning or thinking, and Erlang won't solve your problems for you. It'll only give you tools.
That being said, if you understood everything about these points, you can be proud of yourself (especially if you had never written concurrent software before). You are now starting to really think concurrently.
Fit for the Real World?
In a real game, there is a lot more stuff going on that could make trading even more complex. Items could be worn by the characters and damaged by enemies while they're being traded. Maybe items could be moved in and out of the inventory while being exchanged. Are the players on the same server? If not, how do you synchronise commits to different databases?
Our trade system is sane when detached from the reality of any game. Before trying to fit it in a game (if you dare), make sure everything goes right. Test it, test it, and test it again. You'll likely find that testing concurrent and parallel code is a complete pain. You'll lose hair, friends and a piece of your sanity. Even after this, you'll have to know your system is always as strong as its weakest link and thus potentially very fragile nonetheless.
Don't Drink Too Much Kool-Aid:
While the model for this trade system seems sound, subtle concurrency bugs and race conditions can often rear their ugly heads a long time after they were written, and even if they've been running for years. While my code is generally bullet proof (yeah, right), you sometimes have to face swords and knives. Beware the dormant bugs.
Fortunately, we can put all of this madness behind us. We'll next see how OTP allows you to handle various events, such as alarms and logs, with the help of the gen_event behaviour.
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution Non-Commercial No Derivative License.
Learn you some Erlang.
Rage Against The Finite-State Machines.
What Are They?
A finite-state machine (FSM) is not really a machine, but it does have a finite number of states. I've always found finite-state machines easier to understand with graphs and diagrams. For example, the following would be a simplistic diagram for a (very dumb) dog as a state machine:
Here the dog has 3 states: sitting, barking or wagging its tail. Different events or inputs may force it to change its state. If a dog is calmly sitting and sees a squirrel, it will start barking and won't stop until you pet it again. However, if the dog is sitting and you pet it, we have no idea what might happen. In the Erlang world, the dog could crash (and eventually be restarted by its supervisor). In the real world that would be a freaky event, but your dog would come back after being ran over by a car, so it's not all bad.
Here's a cat's state diagram for a comparison:
This cat has a single state, and no event can ever change it.
Implementing the cat state machine in Erlang is a fun and simple task:
We can try the module to see that the cat really never gives a crap:
The same can be done for the dog FSM, except more states are available:
It should be relatively simple to match each of the states and transitions to what was on the diagram above. Here's the FSM in use:
You can follow along with the schema if you want (I usually do, it helps being sure that nothing's wrong).
That's really the core of FSMs implemented as Erlang processes. There are things that could have been done differently: we could have passed state in the arguments of the state functions in a way similar to what we do with servers' main loop. We could also have added an init and terminate functions, handled code updates, etc.
Another difference between the dog and cat FSMs is that the cat's events are synchronous and the dog's events are asynchronous . In a real FSM, both could be used in a mixed manner, but I went for the simplest representation out of pure untapped laziness. There are other forms of event the examples do not show: global events that can happen in any state.
One example of such an event could be when the dog gets a sniff of food. Once the smell food event is triggered, no matter what state the dog is in, he'd go looking for the source of food.
Now we won't spend too much time implementing all of this in our 'written-on-a-napkin' FSM. Instead we'll move directly to the gen_fsm behaviour.
Generic Finite-State Machines.
The gen_fsm behaviour is somewhat similar to gen_server in that it is a specialised version of it. The biggest difference is that rather than handling calls and casts , we're handling synchronous and asynchronous events . Much like our dog and cat examples, each state is represented by a function. Again, we'll go through the callbacks our modules need to implement in order to work.
This is the same init/1 as used for generic servers, except the return values accepted are , , and . The stop tuple works in the same manner as for gen_server s, and hibernate and Timeout keep the same semantics.
What's new here is that StateName variable. StateName is an atom and represents the next callback function to be called.
The functions StateName/2 and StateName/3 are placeholder names and you are to decide what they will be. Let's suppose the init/1 function returns the tuple . This means the finite state machine will be in a sitting state. This is not the same kind of state as we had seen with gen_server ; it is rather equivalent to the sit , bark and wag_tail states of the previous dog FSM. These states dictate a context in which you handle a given event.
An example of this would be someone calling you on your phone. If you're in the state 'sleeping on a Saturday morning', your reaction might be to yell in the phone. If your state is 'waiting for a job interview', chances are you'll pick the phone and answer politely. On the other hand, if you're in the state 'dead', then I am surprised you can even read this text at all.
Back to our FSM. The init/1 function said we should be in the sitting state. Whenever the gen_fsm process receives an event, either the function sitting/2 or sitting/3 will be called. The sitting/2 function is called for asynchronous events and sitting/3 for synchronous ones.
The arguments for sitting/2 (or generally StateName/2 ) are Event , the actual message sent as an event, and StateData , the data that was carried over the calls. sitting/2 can then return the tuples , , and .
The arguments for sitting/3 are similar, except there is a From variable in between Event and StateData . The From variable is used in exactly the same way as it was for gen_server s, including gen_fsm:reply/2. The StateName/3 functions can return the following tuples:
Note that there's no limit on how many of these functions you can have, as long as they are exported. The atoms returned as NextStateName in the tuples will determine whether the function will be called or not.
handle_event.
In the last section, I mentioned global events that would trigger a specific reaction no matter what state we're in (the dog smelling food will drop whatever it is doing and will instead look for food). For these events that should be treated the same way in every state, the handle_event/3 callback is what you want. The function takes arguments similar to StateName/2 with the exception that it accepts a StateName variable in between them, telling you what the state was when the event was received. It returns the same values as StateName/2 .
handle_sync_event.
The handle_sync_event/4 callback is to StateName/3 what handle_event/2 is to StateName/2 . It handles synchronous global events, takes the same parameters and returns the same kind of tuples as StateName/3 .
Now might be a good time to explain how we know whether an event is global or if it's meant to be sent to a specific state. To determine this we can look at the function used to send an event to the FSM. Asynchronous events aimed at any StateName/2 function are sent with send_event/2, synchronous events to be picked up by StateName/3 are to be sent with sync_send_event/2-3.
The two equivalent functions for global events are send_all_state_event/2 and sync_send_all_state_event/2-3 (quite a long name).
code_change.
This works exactly the same as it did for gen_server s except that it takes an extra state parameter when called like code_change(OldVersion, StateName, Data, Extra) , and returns a tuple of the form .
This should, again, act a bit like what we have for generic servers. terminate/3 should do the opposite of init/1 .
A Trading System Specification.
It's time to put all of this in practice. Many Erlang tutorials about finite-state machines use examples containing telephone switches and similar things. It's my guess that most programmers will rarely have to deal with telephone switches for state machines. Because of that, we're going to look at an example which is more fitting for many developers: we'll design and implement an item trading system for some fictional and non-existing video game.
The design I have picked is somewhat challenging. Rather than using a broker through which players route items and confirmations (which, frankly, would be easier), we're going to implement a server where both players speak to each other directly (which would have the advantage of being distributable).
Because the implementation is tricky, I'll spend a good while describing it, the kind of problems to be faced and the ways to fix them.
First of all, we should define the actions that can be done by our players when trading. The first is asking for a trade to be set up. The other user should also be able to accept that trade. We won't give them the right to deny a trade, though, because we want to keep things simple. It will be easy to add this feature once the whole thing is done.
Once the trade is set up, our users should be able to negotiate with each other. This means they should be able to make offers and then retract them if they want. When both players are satisfied with the offer, they can each declare themselves as ready to finalise the trade. The data should then be saved somewhere on both sides. At any point in time, it should also make sense for any of the players to cancel the whole trade. Some pleb could be offering only items deemed unworthy to the other party (who might be very busy) and so it should be possible to backhand them with a well-deserved cancellation.
In short, the following actions should be possible:
ask for a trade accept a trade offer items retract an offer declare self as ready brutally cancel the trade.
Now, when each of these actions is taken, the other player's FSM should be made aware of it. This makes sense, because when Jim tells his FSM to send an item to Carl, Carl's FSM has to be made aware of it. This means both players can talk to their own FSM, which will talk to the other's FSM. This gives us something a bit like this:
The first thing to notice when we have two identical processes communicating with each other is that we have to avoid synchronous calls as much as possible. The reason for this is that if Jim's FSM sends a message to Carl's FSM and then waits for its reply while at the same time Carl's FSM sends a message over to Jim's FSM and waits for its own specific reply, both end up waiting for the other without ever replying. This effectively freezes both FSMs. We have a deadlock.
One solution to this is to wait for a timeout and then move on, but then there will be leftover messages in both processes' mailboxes and the protocol will be messed up. This certainly is a can of worms, and so we want to avoid it.
The simplest way to do it is to avoid all synchronous messages and go fully asynchronous. Note that Jim might still make a synchronous call to his own FSM; there's no risk here because the FSM won't need to call Jim and so no deadlock can occur between them.
When two of these FSMs communicate together, the whole exchange might look a bit like this:
Both FSMs are in an idle state. When you ask Jim to trade, Jim has to accept before things move on. Then both of you can offer items or withdraw them. When you are both declaring yourself ready, the trade can take place. This is a simplified version of all that can happen and we'll see all possible cases with more detail in the next paragraphs.
Here comes the tough part: defining the state diagram and how state transitions happen. Usually a good bit of thinking goes into this, because you have to think of all the small things that could go wrong. Some things might go wrong even after having reviewed it many times. Because of this, I'll simply put the one I decided to implement here and then explain it.
At first, both finite-state machines start in the idle state. At this point, one thing we can do is ask some other player to negotiate with us:
We go into idle_wait mode in order to wait for an eventual reply after our FSM forwarded the demand. Once the other FSM sends the reply, ours can switch to negotiate :
The other player should also be in negotiate state after this. Obviously, if we can invite the other, the other can invite us. If all goes well, this should end up looking like this:
So this is pretty much the opposite as the two previous state diagrams bundled into one. Note that we expect the player to accept the offer in this case. What happens if by pure luck, we ask the other player to trade with us at the same time he asks us to trade?
What happens here is that both clients ask their own FSM to negotiate with the other one. As soon as the ask negotiate messages are sent, both FSMs switch to idle_wait state. Then they will be able to process the negotiation question. If we review the previous state diagrams, we see that this combination of events is the only time we'll receive ask negotiate messages while in the idle_wait state. Consequently, we know that getting these messages in idle_wait means that we hit the race condition and can assume both users want to talk to each other. We can move both of them to negotiate state. Hooray.
So now we're negotiating. According to the list of actions I listed earlier, we must support users offering items and then retracting the offer:
All this does is forward our client's message to the other FSM. Both finite-state machines will need to hold a list of items offered by either player, so they can update that list when receiving such messages. We stay in the negotiate state after this; maybe the other player wants to offer items too:
Here, our FSM basically acts in a similar manner. Isto é normal. Once we get tired of offering things and think we're generous enough, we have to say we're ready to officialise the trade. Because we have to synchronise both players, we'll have to use an intermediary state, as we did for idle and idle_wait :
What we do here is that as soon as our player is ready, our FSM asks Jim's FSM if he's ready. Pending its reply, our own FSM falls into its wait state. The reply we'll get will depend on Jim's FSM state: if it's in wait state, it'll tell us that it's ready. Otherwise, it'll tell us that it's not ready yet. That's precisely what our FSM automatically replies to Jim if he asks us if we are ready when in negotiate state:
Our finite state machine will remain in negotiate mode until our player says he's ready. Let's assume he did and we're now in the wait state. However, Jim's not there yet. This means that when we declared ourselves as ready, we'll have asked Jim if he was also ready and his FSM will have replied 'not yet':
He's not ready, but we are. We can't do much but keep waiting. While waiting after Jim, who's still negotiating by the way, it is possible that he will try to send us more items or maybe cancel his previous offers:
Of course, we want to avoid Jim removing all of his items and then clicking "I'm ready!", screwing us over in the process. As soon as he changes the items offered, we go back into the negotiate state so we can either modify our own offer, or examine the current one and decide we're ready. Enxague e repita.
At some point, Jim will be ready to finalise the trade too. When this happens, his finite-state machine will ask ours if we are ready:
What our FSM does is reply that we indeed are ready. We stay in the waiting state and refuse to move to the ready state though. Por que é isso? Because there's a potential race condition! Imagine that the following sequence of events takes place, without doing this necessary step:
This is a bit complex, so I'll explain. Because of the way messages are received, we could possibly only process the item offer after we declared ourselves ready and also after Jim declared himself as ready. This means that as soon as we read the offer message, we switch back to negotiate state. During that time, Jim will have told us he is ready. If he were to change states right there and move on to ready (as illustrated above), he'd be caught waiting indefinitely while we wouldn't know what the hell to do. This could also happen the other way around! Ugh.
One way to solve this is by adding one layer of indirection (Thanks to David Wheeler). This is why we stay in wait mode and send 'ready!' (as shown in our previous state diagram). Here's how we deal with that 'ready!' message, assuming we were already in the ready state because we told our FSM we were ready beforehand:
When we receive 'ready!' from the other FSM, we send 'ready!' back again. This is to make sure that we won't have the 'double race condition' mentioned above. This will create a superfluous 'ready!' message in one of the two FSMs, but we'll just have to ignore it in this case. We then send an 'ack' message (and the Jim's FSM will do the same) before moving to ready state. The reason why this 'ack' message exists is due to some implementation details about synchronising clients. I've put it in the diagram for the sake of being correct, but I won't explain it until later. Forget about it for now. We finally managed to synchronise both players. Whew.
So now there's the ready state. This one is a bit special. Both players are ready and have basically given the finite-state machines all the control they need. This lets us implement a bastardized version of a two-phase commit to make sure things go right when making the trade official:
Our version (as described above) will be rather simplistic. Writing a truly correct two-phase commit would require a lot more code than what is necessary for us to understand finite-state machines.
Finally, we only have to allow the trade to be cancelled at any time. This means that somehow, no matter what state we're in, we're going to listen to the 'cancel' message from both sides and quit the transaction. It should also be common courtesy to let the other side know we're gone before leaving.
Alright! It's a whole lot of information to absorb at once. Don't worry if it takes a while to fully grasp it. It took a bunch of people to look over my protocol to see if it was right, and even then we all missed a few race conditions that I then caught a few days later when reviewing the code while writing this text. It's normal to need to read it more than once, especially if you are not used to asynchronous protocols. If this is the case, I fully encourage you to try and design your own protocol. Then ask yourself "what happens if two people do the same actions very fast? What if they chain two other events quickly? What do I do with messages I don't handle when changing states?" You'll see that the complexity grows real fast. You might find a solution similar to mine, possibly a better one (let me know if this is the case!) No matter the outcome, it's a very interesting thing to work on and our FSMs are still relatively simple.
Once you've digested all of this (or before, if you're a rebel reader), you can go to the next section, where we implement the gaming system. For now you can take a nice coffee break if you feel like doing so.
Game trading between two players.
The first thing that needs to be done to implement our protocol with OTP's gen_fsm is to create the interface. There will be 3 callers for our module: the player, the gen_fsm behaviour and the other player's FSM. We will only need to export the player function and gen_fsm functions, though. This is because the other FSM will also run within the trade_fsm module and can access them from the inside:
So that's our API. You can see I'm planning on having some functions being both synchronous and asynchronous. This is mostly because we want our client to call us synchronously in some cases, but the other FSM can do it asynchronously. Having the client synchronous simplifies our logic a whole lot by limiting the number of contradicting messages that can be sent one after the other. We'll get there. Let's first implement the actual public API according to the protocol defined above:
This is rather standard; all these 'gen_fsm' functions have been covered before (except start/3-4 and start_link/3-4 which I believe you can figure out) in this chapter.
Next we'll implement the FSM to FSM functions. The first ones have to do with trade setups, when we first want to ask the other user to join us in a trade:
The first function asks the other pid if they want to trade, and the second one is used to reply to it (asynchronously, of course).
We can then write the functions to offer and cancel offers. According to our protocol above, this is what they should be like:
So, now that we've got these calls done, we need to focus on the rest. The remaining calls relate to being ready or not and handling the final commit. Again, given our protocol above, we have three calls: are_you_ready , which can have the replies not_yet or ready! :
The only functions left are those which are to be used by both FSMs when doing the commit in the ready state. Their precise usage will be described more in detail later, but for now, the names and the sequence/state diagram from earlier should be enough. Nonetheless, you can still transcribe them to your own version of trade_fsm:
Oh and there's also the courtesy function allowing us to warn the other FSM we cancelled the trade:
We can now move to the really interesting part: the gen_fsm callbacks. The first callback is init/1 . In our case, we'll want each FSM to hold a name for the user it represents (that way, our output will be nicer) in the data it keeps passing on to itself. What else do we want to hold in memory? In our case, we want the other's pid, the items we offer and the items the other offers. We're also going to add the reference of a monitor (so we know to abort if the other dies) and a from field, used to do delayed replies:
In the case of init/1 , we'll only care about our name for now. Note that we'll begin in the idle state:
The next callbacks to consider would be the states themselves. So far I've described the state transitions and calls that can be made, but We'll need a way to make sure everything goes alright. We'll write a few utility functions first:
And we can start with the idle state. For the sake of convention, I'll cover the asynchronous version first. This one shouldn't need to care for anything but the other player asking for a trade given our own player, if you look at the API functions, will use a synchronous call:
A monitor is set up to allow us to handle the other dying, and its ref is stored in the FSM's data along with the other's pid, before moving to the idle_wait state. Note that we will report all unexpected events and ignore them by staying in the state we were already in. We can have a few out of band messages here and there that could be the result of race conditions. It's usually safe to ignore them, but we can't easily get rid of them. It's just better not to crash the whole FSM on these unknown, but somewhat expected messages.
When our own client asks the FSM to contact another player for a trade, it will send a synchronous event. The idle/3 callback will be needed:
We proceed in a way similar to the asynchronous version, except we need to actually ask the other side whether they want to negotiate with us or not. You'll notice that we do not reply to the client yet. This is because we have nothing interesting to say, and we want the client locked and waiting for the trade to be accepted before doing anything. The reply will only be sent if the other side accepts once we're in idle_wait .
When we're there, we have to deal with the other accepting to negotiate and the other asking to negotiate (the result of a race condition, as described in the protocol):
This gives us two transitions to the negotiate state, but remember that we must use gen_fsm:reply/2 reply to our client to tell it it's okay to start offering items. There's also the case of our FSM's client accepting the trade suggested by the other party:
Again, this one moves on to the negotiate state. Here, we must handle asynchronous queries to add and remove items coming both from the client and the other FSM. However, we have not yet decided how to store items. Because I'm somewhat lazy and I assume users won't trade that many items, simple lists will do it for now. However, we might change our mind at a later point, so it would be a good idea to wrap item operations in their own functions. Add the following functions at the bottom of the file with notice/3 and unexpected/2 :
Simple, but they have the role of isolating the actions (adding and removing items) from their implementation (using lists). We could easily move to proplists, arrays or whatever data structure without disrupting the rest of the code.
Using both of these functions, we can implement offering and removing items:
This is an ugly aspect of using asynchronous messages on both sides. One set of message has the form 'make' and 'retract', while the other has 'do' and 'undo'. This is entirely arbitrary and only used to differentiate between player-to-FSM communications and FSM-to-FSM communications. Note that on those coming from our own player, we have to tell the other side about the changes we're making.
Another responsibility is to handle the are_you_ready message we mentioned in the protocol. This one is the last asynchronous event to handle in the negotiate state:
As described in the protocol, whenever we're not in the wait state and receive this message, we must reply with not_yet . Were also outputting trade details to the user so a decision can be made.
When such a decision is made and the user is ready, the ready event will be sent. This one should be synchronous because we don't want the user to keep modifying his offer by adding items while claiming he's ready:
At this point a transition to the wait state should be made. Note that just waiting for the other is not interesting. We save the From variable so we can use it with gen_fsm:reply/2 when we have something to tell to the client.
The wait state is a funny beast. New items might be offered and retracted because the other user might not be ready. It makes sense, then, to automatically rollback to the negotiating state. It would suck to have great items offered to us, only for the other to remove them and declare himself ready, stealing our loot. Going back to negotiation is a good decision:
Now that's something meaningful and we reply to the player with the coordinates we stored in S#state. from . The next set of messages we need to worry about are those related to with synchronising both FSMs so they can move to the ready state and confirm the trade. For this one we should really focus on the protocol defined earlier.
The three messages we could have are are_you_ready (because the other user just declared himself ready), not_yet (because we asked the other if he was ready and he was not) and ready! (because we asked the other if he was ready and he was).
We'll start with are_you_ready . Remember that in the protocol we said that there could be a race condition hidden there. The only thing we can do is send the ready! message with am_ready/1 and deal with the rest later:
We'll be stuck waiting again, so it's not worth replying to our client yet. Similarly, we won't reply to the client when the other side sends a not_yet to our invitation:
On the other hand, if the other is ready, we send an extra ready! message to the other FSM, reply to our own user and then move to the ready state:
You might have noticed that I've used ack_trans/1 . In fact, both FSMs should use it. Por que é isso? To understand this we have to start looking at what goes on in the ready! state.
When in the ready state, both players' actions become useless (except cancelling). We won't care about new item offers. This gives us some liberty. Basically, both FSMs can freely talk to each other without worrying about the rest of the world. This lets us implement our bastardization of a two-phase commit. To begin this commit without either player acting, we'll need an event to trigger an action from the FSMs. The ack event from ack_trans/1 is used for that. As soon as we're in the ready state, the message is treated and acted upon; the transaction can begin.
Two-phase commits require synchronous communications, though. This means we can't have both FSMs starting the transaction at once, because they'll end up deadlocked. The secret is to find a way to decide that one finite state machine should initiate the commit, while the other will sit and wait for orders from the first one.
It turns out that the engineers and computer scientists who designed Erlang were pretty smart (well, we knew that already). The pids of any process can be compared to each other and sorted. This can be done no matter when the process was spawned, whether it's still alive or not, or if it comes from another VM (we'll see more about this when we get into distributed Erlang).
Knowing that two pids can be compared and one will be greater than the other, we can write a function priority/2 that will take two pids and tell a process whether it's been elected or not:
And by calling that function, we can have one process starting the commit and the other following the orders.
Here's what this gives us when included in the ready state, after receiving the ack message:
This big try . catch expression is the leading FSM deciding how the commit works. Both ask_commit/1 and do_commit/1 are synchronous. This lets the leading FSM call them freely. You can see that the other FSM just goes and wait. It will then receive the orders from the leading process. The first message should be ask_commit . This is just to make sure both FSMs are still there; nothing wrong happened, they're both dedicated to completing the task:
Once this is received, the leading process will ask to confirm the transaction with do_commit . That's when we must commit our data:
And once it's done, we leave. The leading FSM will receive ok as a reply and will know to commit on its own end afterwards. This explains why we need the big try . catch : if the replying FSM dies or its player cancels the transaction, the synchronous calls will crash after a timeout. The commit should be aborted in this case.
Just so you know, I defined the commit function as follows:
Pretty underwhelming, eh? It's generally not possible to do a true safe commit with only two participants—a third party is usually required to judge if both players did everything right. If you were to write a true commit function, it should contact that third party on behalf of both players, and then do the safe write to a database for them or rollback the whole exchange. We won't go into such details and the current commit/1 function will be enough for the needs of this book.
We're not done yet. We have not yet covered two types of events: a player cancelling the trade and the other player's finite state machine crashing. The former can be dealt with by using the callbacks handle_event/3 and handle_sync_event/4 . Whenever the other user cancels, we'll receive an asynchronous notification:
When we do it we must not forget to tell the other before quitting ourselves:
And voilà! The last event to take care of is when the other FSM goes down. Fortunately, we had set a monitor back in the idle state. We can match on this and react accordingly:
Note that even if the cancel or DOWN events happen while we're in the commit, everything should be safe and nobody should get its items stolen.
Note: we used io:format/2 for most of our messages to let the FSMs communicate with their own clients. In a real world application, we might want something more flexible than that. One way to do it is to let the client send in a Pid, which will receive the notices sent to it. That process could be linked to a GUI or any other system to make the player aware of the events. The io:format/2 solution was chosen for its simplicity: we want to focus on the FSM and the asynchronous protocols, not the rest.
Only two callbacks left to cover! They're code_change/4 and terminate/3 . For now, we don't have anything to do with code_change/4 and only export it so the next version of the FSM can call it when it'll be reloaded. Our terminate function is also really short because we didn't handle real resources in this example:
We can now try it. Well, trying it is a bit annoying because we need two processes to communicate to each other. To solve this, I've written the tests in the file trade_calls. erl, which can run 3 different scenarios. The first one is main_ab/0 . It will run a standard trade and output everything. The second one is main_cd/0 and will cancel the transaction halfway through. The last one is main_ef/0 and is very similar to main_ab/0 , except it contains a different race condition. The first and third tests should succeed, while the second one should fail (with a crapload of error messages, but that's how it goes). You can try it if you feel like it.
That Was Quite Something.
If you've found this chapter a bit harder than the others, I must remind you that it's entirely normal. I've just gone crazy and decided to make something hard out of the generic finite-state machine behaviour. If you feel confused, ask yourself these questions: Can you understand how different events are handled depending on the state your process is in? Do you understand how you can transition from one state to the other? Do you know when to use send_event/2 and sync_send_event/2-3 as opposed to send_all_state_event/2 and sync_send_all_state_event/3 ? If you answered yes to these questions, you understand what gen_fsm is about.
The rest of it with the asynchronous protocols, delaying replies and carrying the From variable, giving a priority to processes for synchronous calls, bastardized two-phase commits and whatnot are not essential to understand . They're mostly there to show what can be done and to highlight the difficulty of writing truly concurrent software, even in a language like Erlang. Erlang doesn't excuse you from planning or thinking, and Erlang won't solve your problems for you. It'll only give you tools.
That being said, if you understood everything about these points, you can be proud of yourself (especially if you had never written concurrent software before). You are now starting to really think concurrently.
Fit for the Real World?
In a real game, there is a lot more stuff going on that could make trading even more complex. Items could be worn by the characters and damaged by enemies while they're being traded. Maybe items could be moved in and out of the inventory while being exchanged. Are the players on the same server? If not, how do you synchronise commits to different databases?
Our trade system is sane when detached from the reality of any game. Before trying to fit it in a game (if you dare), make sure everything goes right. Test it, test it, and test it again. You'll likely find that testing concurrent and parallel code is a complete pain. You'll lose hair, friends and a piece of your sanity. Even after this, you'll have to know your system is always as strong as its weakest link and thus potentially very fragile nonetheless.
Don't Drink Too Much Kool-Aid:
While the model for this trade system seems sound, subtle concurrency bugs and race conditions can often rear their ugly heads a long time after they were written, and even if they've been running for years. While my code is generally bullet proof (yeah, right), you sometimes have to face swords and knives. Beware the dormant bugs.
Fortunately, we can put all of this madness behind us. We'll next see how OTP allows you to handle various events, such as alarms and logs, with the help of the gen_event behaviour.
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution Non-Commercial No Derivative License.
Trading system finite state machine.
Respostas finais - Ciência - NUMERICANA.
A selection of mathematical and scientific questions, with definitive answers presented by Dr. Gérard P. Michon (mathematics, physics, etc.).
Rage Against The Finite - State Machines | Learn You …
Presenting finite - state machines and their OTP implementation with an asynchronous item trading system for a fictive game.
BibMe: Bibliografia gratuita e amp; Citation Maker - MLA, APA .
BibMe Free Bibliography & amp; Citation Maker - MLA, APA, Chicago, Harvard.
Computador - Wikipedia.
Um computador é um dispositivo que pode ser instruído a executar seqüências arbitrárias de operações aritméticas ou lógicas automaticamente. A capacidade dos computadores a seguir.
AlgorithmicTrading | Sobre Nós | Company History | ...
About AlgorithmicTrading An Established Third Party Trading System Developer With a Rich History Of Providing High Quality Automated Trading Systems.
Speed of light - Wikipedia.
The speed of light in vacuum, commonly denoted c, is a universal physical constant important in many areas of physics. Its exact value is 299,792,458 metres per .
Bitcoin FAQ - Frequently Asked Questions About …
O que é Bitcoin? Bitcoin is a form of digital currency which is based on an open source code that was created and is held electronically. Bitcoin is a decentralized .
Wake Up Nova Zelândia | O que faz o globalista ...
Os governos eleitos são falsas frentes coordenadas por um governo sombrio global.
Research - Machine Intelligence Research Institute.
We focus our research on AI approaches that can be made transparent, so that humans can understand why the AIs behave as they do.
英汉日科技词汇(An English-Chinese …
本词汇表版权为有限会社MSC所有, 欢迎使用。 船舶配件贸易分类==> Main Ship Equipments | Equipment Types | Main Marine Manufacturers.
No comments:
Post a Comment