Lab5

Importante

Uma atualização no código anexado foi enviada hoje (26/11). Não altera a realização do lab. Os arquivos anteriores devem ser substituídos pelos desta versão.

Mudanças do código original:

  • Pequena reestruturação nos testes (testes que não passam até execução do lab foram movidos para outra classe). Novos testes adicionados, para garantir que o lab foi feito corretamente.
  • O método load de ListPersistanceDecorator foi renomeado para load_list para sanar uma colisão de nomes que estava ocorrendo (não afeta a realização do lab)
  • Agora o método load_list não é mais chamado automaticamente do construtor de ListPersistanceDecorator. Você deve chamar este método nos Lists decorados com persistência, se quiser carregá-los do disco, após a decoração da lista ter sido feita. load_list deve ser chamado no builder, quando for solicitado o retorno do objeto construído no builder (e se a persistência tiver sido adicionada).
  • initialize.rb agora carrega todos os arquivos da pasta lib automaticamente, dispensando alteração no arquivo sempre que se adicionar ou alterar o nome de um arquivo.

Objetivos

Nos labs 5 e 6 vamos desenvolver uma aplicação cliente servidor simples, mas completa, que irá utilizar as duas linguagens de programação utilizadas no curso. Neste lab 5, iremos desenvolver grande parte do "backend" da nossa aplicação em Ruby. Iremos estudar um pouco de Threads, IO e treinar a implementação do padrão Decorator, visto em sala. Iremos partir de uma versão inicial do backend, já desenvolvido, e implementar algumas melhorias.

Aplicação

A aplicação que iremos desenvolver será um "bolão virtual", onde apostadores poderão dar palpites sobre resultados de partidas esportivas na qual exista um placar final (ex.: jogos de futebol). Cada usuário irá dispor de uma senha de acesso que garantirá a autenticidade das suas apostas. A porção servidor da aplicação irá agregar todos os jogos, apostas e usuários em um local central. A porção cliente irá possibilitar ao usuário fazer consultas às partidas e apostas existentes, e efetuar novas apostas. Cada partida terá um "tempo de finalização", a partir do qual não será mais possível fazer apostas.

Backend

O código do backend (parte do servidor), conforme mostrado em aula, se encontra anexado. Descomprima o conteúdo do projeto em uma pasta e, dentro dela, execute os testes da aplicação com o comando

rake test

Você pode também rodar os testes de dentro do Netbeans, se estiver utilizando-o (Botão direito no nome do projeto > Test).

ATUALIZAÇÃO: Se tiver problemas ao rodar o comando acima, instale a gem "rake", com o comando abaixo. É necessário ter o rubygems instalado]. Se utilizar o Netbeans, o rubygems e o rake já estarão instalados quando se utiliza o interpretador JRuby interno.

LINUX:
sudo gem install rake
WINDOWS:
gem install rake

A saída deverá mostrar que dois erros foram encontrados na classe de testes AfterYourWorkTest. Estes testes devem passar após suas alterações no laboratório.

(in /home/bernardo/Documents/docs/Mestrado/estagio_docencia/labs/lab5_alunos)

1) Error:
test_all_match_list_decorators(AfterYourWorkTest):
NameError: uninitialized constant AfterYourWorkTest::MatchListThreadSafeDecorator
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:2465:in `const_missing'
./test/after_your_work_test.rb:53:in `test_all_match_list_decorators'
/usr/lib/ruby/1.8/test/unit/testcase.rb:78:in `__send__'
/usr/lib/ruby/1.8/test/unit/testcase.rb:78:in `run'
/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'
/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'
/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'
/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `run_suite'
/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'
/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'
/usr/lib/ruby/1.8/test/unit.rb:278
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb:5

2) Error:
test_persistance_advanced(AfterYourWorkTest):
NameError: uninitialized constant AfterYourWorkTest::MatchListPersistanceDecorator
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake.rb:2465:in `const_missing'
./test/after_your_work_test.rb:18:in `test_persistance_advanced'
/usr/lib/ruby/1.8/test/unit/testcase.rb:78:in `__send__'
/usr/lib/ruby/1.8/test/unit/testcase.rb:78:in `run'
/usr/lib/ruby/1.8/test/unit/testsuite.rb:34:in `run'
/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `each'
/usr/lib/ruby/1.8/test/unit/testsuite.rb:33:in `run'
/usr/lib/ruby/1.8/test/unit/ui/testrunnermediator.rb:46:in `run_suite'
/usr/lib/ruby/1.8/test/unit/ui/testrunnerutilities.rb:29:in `run'
/usr/lib/ruby/1.8/test/unit/autorunner.rb:12:in `run'
/usr/lib/ruby/1.8/test/unit.rb:278
/usr/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb:5

Deprecated option (--fox).

Finished in 0.41 seconds.
28 tests, 0 failures, 2 errors

A atividade do lab 5 consiste em, após revisar o código explicado em sala de aula, implementar e finalizá-lo, implementando algumas melhorias:

Melhoria nos Decorators de Thread Safety

Conforme explicado em aula, o ListThreadSafeDecorator, decora o método add de forma a sincronizá-lo, utilizando o módulo MonitorMixin. No entanto, mesmo se o utilizarmos para decorar o MatchList e o UserList, ainda teremos operações que podem ser feitas na classe Match (métodos make_bet e finalize) que alteram o conteúdo das suas instâncias, que se acessadas simultaneamente por várias Threads podem causar condições de corrida (ex.: Dois usuários tentando fazer a uma aposta em uma mesma partida ao mesmo tempo). Para isto, precisamos decorar estes métodos da classe Match. Crie um decorator para isto e o chame de MatchThreadSafeDecorator.

Veja o que foi feito no MatchListLoggerDecorator para decorar o MatchList de forma a permitir que o método add construa Matchs decorados (explicado em detalhes em sala), e crie uma classe MatchListThreadSafeDecorator implementando esta solução.

Depois, altere o método de teste test_thread_decorated_integration em integration_test.rb para contemplar o uso de MatchListThreadSafeDecorator para se decorar o MatchList. Rode os testes para verificar o bom funcionamento das suas alterações.

Melhoria nos Decorators de Persistência

Conforme explicado em aula, o ListPersistanceDecorator, adicionam os métodos save e load_list, que salvam e carregam um arquivo yaml com a representação do vetor de elementos da List (um UserList ou MatchList). O método load_list deve ser chamado no List decorado, para fazer com que seu conteúdo seja populado a partir das informações gravadas no disco, no formato yml. Além disto decora o método add do objeto decorado, adicionando uma chamada ao método save em seu final.

No entanto, de forma semelhante ao caso anterior, temos chamadas em Match (métodos make_bet e finalize) que alteram o conteúdo dos seus objetos. Portanto, devemos decorar estes métodos para efetuar chamadas ao método save do MatchList decorado, para assegurar que os dados serão gravados em disco.

Inspeccionem o método de teste test_persistance_advanced do arquivo de teste decorator_test e verifique que a falta da chamada $match_list.save, presente em test_persistence_simple, faz com que parte das operações realizadas (as apostas) não sejam persistidas no disco.

Portanto, de forma, semalhante ao que foi feito no exemplo anterior, crie um decorador para a classe Match, MatchPersistanceDecorator para adicionar estas chamadas. Este decorador será bem semelhante a MatchThreadSafeDecorator que vc criou no exemplo anterior. Dica: Você terá que armazenar uma referência ao objeto MatchList decorado neste decorator, para conseguir chamar o método save dele.

Depois, de forma quase idêntica ao item anterior, crie a classe MatchListPersistanceDecorator para fazer com que o método add crie matches decorados com MatchPersistanceDecorator.

Depois, altere o método de teste test_persistance_advanced em decorator_test.rb para contemplar o uso de MatchListPersistanceDecorator para se decorar o MatchList e confirme que o teste test_persistance_advanced agora passa.

Criação de Builders para UserList e MatchList com as decorações

Para treinar o uso do padrão builder, crie duas classes Builder (aula 17), MatchListBuilder e UserListBuilder que facilitar a construção dos objetos destas classes decorados. Cada builder deverá ter suporte a adicionar os 3 tipos de decoração existentes na aplicação (Logger, ThreadSafe e Persistance). Além disto, o builder deverá ter um método "add_all" que adiciona todas as decorações ao objeto. O builder deverá ter um método para retornar objeto construído (ex.: match_list ou user_list). Para exemplificar, o uso do Builder, veja o trecho de código abaixo:

ml_builder = MatchListBuilder.new
ml_builder.add_persistance
ml_builder.add_logging
ml_builder.add_thread_safety
$match_list = ml_builder.match_list
 
#ou então, para simplificar, que deve ser equivalente a adicionar os 3 decorators na ordem acima
ul_builder = UserListBuilder.new
ul_builder.add_all
$user_list = ml_builder.user_list

IMPORTANTE: O método que retorna o objeto do builder, deverá automaticamente chamar o método load_list, caso o objeto tiver sido decorado com persistencia, para carregar a lista a partir do disco:

class MatchListBuilder
  #...
  def match_list
    @match_list.load_list if @added_persistance
    @match_list
  end
end

Use o seu builder no seu método main, na próxima seção.

Criação de um método main que interprete um protocolo de entrada e saída de dados do backend

Esta atividade deve ser feita com em duplas. Defina uma dupla com um aluno que tenha o mesmo interesse que o seu (fazer ou não fazer o lab 6). Defina um protocolo para entrada e saída dos dados e para a sua aplicação. Sugestão: Utilize a biblioteca yaml para formatar dos dados de entrada e saída. Caso queira experimentar, utilize o padrão JSON ou XML. Caso queira ser mais radical, utilize um padrão customizado para os seus dados (mas você deve ser capaz interpretá-lo do outro lado).

Como qualquer aplicação, a nossa irá receber uma entrada, analisá-la, e retornar uma resposta. Esta entrada irá solicitar ao sistema que efetue alguma operação e irá determinar o tipo de resposta que será retornada. As seguintes operações devem ser suportadas pelo protocolo da entrada/saída da sua aplicação:

  • Criar Usuario
  • Autenticar Usuario
    • Deve ser feita antes de qualquer uma das operações a seguir. Se for mal sucedida, encera a aplicação. Bem sucedida, define qual o usuário para o qual as operações seguintes serão realizadas
  • Listar partidas atuais (retornando uma lista de nomes de partidas com algumas informações básicas a respeito de cada uma).
  • Listar partidas encerradas (semelhante a anterior)
  • Informações detalhadas de uma partida (apostadores, ganhadores)
  • Listar informações das minhas apostas (vencedoras, em andamento, finalizadas)
  • Fazer aposta em uma partida
  • Encerrar a conexão

Algumas destas operações também recebem parâmetros. Ex.: a autenticação precisa do nome do usuário e senha, fazer aposta precisa do placar e nome da partida, etc.

Como exemplo, vamos sugerir um protocolo usando yaml. Neste padrão, tanto os pedido quanto as respostas são codificadas em yaml. Para marcar o início e o fim de um pedido no nosso canal de io, usamos as palavras INICIO e FIM.

No io de entrada:

INICIO
[YAML_DE_PEDIDO]
FIM

No io de saída:

INICIO
[YAML_DE_RESPOSTA]
FIM

O pedido é um hash serializado em yaml no seguinte formato:

{
  'request' => {
       'operation' => 'string_com_nome_da_operacao'
       'parameters' => [ "Array", "com",  "os",  "parametros", "do", "pedido" ]
    }
}

Um exemplo já convertido para yaml:

INICIO
---
request:  
 operation: login_user  
 parameters:  
  - Joao  
  - 123456  
FIM

A resposta seria simplesmente:

{
  'response' => "Qualquer coisa que contenha a resposta (em geral um misto de arrays e hashes)"
}

Um exemplo de resposta listando array com dois hashes contento informações das partidas:

INICIO
--- 
:response: 
- :finalized: finalized
  :end_time: 28/10/2009 16:00
  :bet_count: 3 bets
  :final_score: 5x0
  :name: atletico x cruzeiro
  :winners: jose, joao
- :finalized: not finalized
  :end_time: 28/10/2009 17:00
  :bet_count: 2 bets
  :final_score: _ x _
  :name: sao paulo x palmeiras
  :winners: no winners
FIM

Como exemplo, mostramos também os mesmos dados no formato customizado "tabela", que representa uma lista de com várias linhas e colunas, onde os separadores de linhas é o caractere de nova linha ("\n") e o separador de coluna é o caractere "pipe" ( "|"). Este formato não é difícil de gerar e interpretar, e é uma alternativa interessante ao yaml.

atletico x cruzeiro | 28/10/2009 16:00 | finalized | 5x0 | 3 bets | jose, joao 
sao paulo x palmeiras | 28/10/2009 17:00 | not finalized | _ x _ | 2 bets | no winners

Para gerar o seu yaml de resposta, construa o hash com a resposta e execute o método "to_yaml", que será adicionado a todos os objetos, após a inclusão da biblioteca yaml (require 'yaml'). O processo de construção do hash e geração do yaml de resposta a partir dos dados da aplicação pode ser feito em uma classe distinta, para obter um maior encapsulamento. Para isto, foi criado o módulo "Printers", onde estas classes podem ser adicionadas. Como exemplo, criamos 3 classes "Printers" de exemplo. Duas delas usam um formato "tabela" customizado, e a outra utiliza o formato yaml no protocolo sugerido. O construtor da classe recebe como parâmetro um objeto io, que será usado para retornar a resposta (chamando-se o método puts do objeto de io).

Para interpretar o yaml de entrada, recomenda-se, de forma análoga, criar-se uma classe "RequestReader", que também receberá como parâmetro um objeto de io onde o usuário (cliente) irá entrar com a requisição. Ela deverá efetuar o parse do yaml de entrada, usando o métode classe YAML.load(string). Um exemplo de como isto pode ser feito, considerando-se as palavras "INICIO" e "FIM" estejam delimitando uma resposta.

#lê a entrada linha a linha até acharmos um "INICIO"
  loop do
    string = io.gets
    break if string && string.include?("INICIO")
  end
  yaml = io.gets("\nFIM") #depois do INICIO, lê tudo até a palavra FIM
  yaml = yaml.chomp("FIM") #tira o FIM do final do string que foi lido do io
  requisicao = YAML.load(yaml) # aqui já temos o hash com a requisicao processada
 
  #... processa requisicão e escreve saída

Sua classe RequestReader, além de fazer o processamento do input da forma acima, deverá, depois, analisá-lo (analisar o hash requisicao) e efetuar o comando solicitado na requisição, criando o printer de saída correspondente, para retornar os dados.

Crie seu main em um arquivo main.rb. Defina um método main que recebe como parâmetro os ios de entrada e saída (com valores padrão $stdin e $stdout). Utilize seu builders para construir o MatchList e UserList decorados com as 3 opções. Garanta que "Thread Safe" seja o nível mais externo de decoração, para garantir que todo o conteúdo dos métodos de escrita sejam sincronizados.

#main.rb
 
require 'initialize'
 
def main(in=$stdin, out=$stdout)
  loop do
    #utiliza RequestReader para esperar a chegada de uma requisição
  end
  # código do main
end
 
main #executa o main.

Teste manualmente a execução do método main, executando ruby main.rb e entrando com requisições para as operações criadas.

Criação do servidor multithread

O servidor multi thread irá fazer com que seja possível estabelecer mais de uma conexão simultaneamente, via rede, e fazendo com que cada nova conexão entrante seja tratada por um Thread novo. O servidor terá um Thread principal com o objetivo de escutar as conexões entrantes e um thread por cliente já conectado.

Primeiramente crie 'server.rb', que irá ser o ponto de chamada de sua aplicação. Não esqueça de remover a chamada a main no fim de 'main.rb', para evitar que o interpretador começe a rodar logo depois de ser 'requerido' em 'initialize.rb'.

No Thread principal, para receber uma conexão, utilize a classe TCPServer (documentação no apêndice A do livro Programming Ruby).

server = TCPServer.new(4344)  # 4344 é a porta de conexão de rede usada
                               # utilize de preferência um número acima de 
                               # 2000 (e menor que 64000).
while ( io_socket = server.accept ) #aguarda até que conexão seja recebida, retornando um TCPSocket com a conexão.
  # dispare uma nova thread e utilize io_socket para efetuar a entrada e saída do método main, de dentro desta thread
  #io_socket deve ser utilizado tanto para saída quanto para entrada, portanto é passado como parâmetro in e parâmetro out de main.
end

Para testar a aplicação, rode o servidor e utilize um programa de telnet para se conectar manualmente a ele. Exemplo do comando de telnet no Linux:

telnet localhost 4344

Em seguida, você estará conectado e poderá entrar com texto e utilizar seu protocolo para fazer requisições. Para mostrar que o seu servidor aceita conexões múltiplas, conecte-se simultaneamente nele.

Relatório e Instruções

Conforme conversado com o representante, o laboratório será individual e valerá por dois. O laboratório 6 (desenvolvimento do cliente) será uma alternativa para aqueles com dificuldades no projeto final (com nota valendo pelo exame) e será opcional para aqueles que entregarem o projeto final (valendo como nota adicional no exame ou bimestre). O relatório deverá ser feito de maneira habitual.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License