Fórum Implementar LoadBalance no exemplo do Andreano Lanuse. #385623

03/09/2010

0

Olá Pessoal …


Ontem me baseando no exemplo do Andreano sobre Failover, iniciei a
tentativa de adicionar o LoadBalance na aplicação, acredito que vai
conseguir me atender, mas não acredito que o método que utilizei
seja a melhor maneira para fazer isso, por isso estou querendo criticas
e sugestões de quem já implementou ou não, qualquer ajuda é
bem vinda vou postar o código e explicar o que foi alterado para a
tentativa do Load Balance …


Antes vou explicar umas classes adicionais ao exemplo do Andreado,
criei quatro classes TSystem, TServer, TUserFlag e
TDSHTTPServiceHelper. Essas classes foram criadas para o controle dos
Sistemas que terão acesso a esse Servidor Failover e os Servidores
DataSnap com as regras de negocio.

TSystem: Responsável pela entrada no sistema, é nele que vai ficar
armezanado os sistemas que vão poder acessar, por exemplo, em um
sistema de RP, que tem módulos, pode ser que eu queira que cada
módulo tem um próprio servidor DataSnap, ai tipo essa classe vai
ser preenchida com os módulos, exemplo Financeiro, GED e por ai vai


TServer: Complemento da TSystem, posso ter varios Sistemas para o
Servidor de Failover e cada Sistema pode ter varios Servidores DataSnap,
dependendo da quantidade de acessos, eu posso disponibilar dois para um,
três para outro. Nessa classe vai ter os dados dos servidores que
estarão disponibilizados para cada sistema.

TUserFlag: Essa classe foi criada somente para ligar a "Session" com as
minhas classes TSystem e TServer. A TSystem porque se precisar
Redirecionar uma conexão perdida, vai estar os meus servidores e a
TServer, para ter um controle de quantas sessões estão abertas. Eu
utilizei a UserFlag, por que entendi, que seria tipo uma Tag. Pode ser
que eu esteja enganado.

TDSHTTPServiceHelper: Essa classe não sei se é a melhor forma de
se fazer isto, mas foi a solução que encontrei, certo. O meu sistema
Cliente manda para o Servidor Failover com que Sistema quer trabalhar,
como ele manda ? Simples, pela propiedade User do
TDSHTTPServiceAuthenticationManager, porque ai? Por que o evento de
verificação de usuário vem antes da autentificação do
DSHTTPService, ai eu posso alterar o DSHostName e a DSPort, antes de ser
conectado, pegando do Servidor que esta com menos carga, até ai tudo
bem, mas tipo, eu não consegui associar a minha Session com os dados
que estavam vindo do meu Client, depois que passa pelo evento de
Autentificação ele vai para o OpenBackupSession, do meu Failover,
lá já tem a Session, e nos parâmetros tem também Sender, que
é o meu TDSHTTPService, por isso criei essa Class Helper para
adicionar as propriedades System e Server para serem associadas a
Session atravez da TUserFlag. Eu não sei se foi uma boa pratica e se
vou ter problemas futuros. Não cheguei a ter em meus testes.


Pronto pessoal basicamente a diferença é essa.

Tenho algumas duvidas, sobre a impletação do Failover, pelo que
pecebi esta obrigando a ter só um Reopen, quero saber, se terei
problemas, alterando para mais de um em cada Session. Outra duvida é
sobre o uso do Session.UserPoint, no caso do exemplo do Andreano ele
só usa no primeira sessão antes de dar um Reopen, como eu planejo
usar mais, eu recrio UserPoint, quero saber se isso foi uma boa pratica.
Outra duvida é sobre a validação dos meus Servidores DataSnap
gostaria de saber se tem, alguma forma mas pratica, do que estou fazendo
na function ActiveConnection.

Esta ai a Unit completa.


unit Main;

interface

uses
    Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr,
Dialogs,
    IdTCPServer, IdHTTPServer, IdCustomHTTPServer, IdContext, StdCtrls,
    IdBaseComponent, IdComponent, IdCustomTCPServer, DSHTTP, WideStrings,
    DbxDatasnap, DB, SqlExpr, DSService, DSTCPServerTransport,
    DbxSocketChannelNative, DSCommonServer, DSServer, DSHTTPCommon,
    Generics.Collections, DBClient, Forms;

type

    TServer = class
    private
      FcDriverName: String;
      FnPort: Integer;
      FcHostName: String;
      FoSessionId: TStringList;
      procedure SetDriverName(const Value: String);
      procedure SetHostName(const Value: String);
      procedure SetSetPort(const Value: Integer);
      procedure SetSessionId(const Value: TStringList);
    public
      function ConnectionCount: Integer;
      property DriverName     : String      read FcDriverName      write SetDriverName;
      property HostName       : String      read FcHostName        write SetHostName;
      property Port           : Integer     read FnPort            write SetSetPort;
      property SessionId      : TStringList read FoSessionId       write SetSessionId;

      constructor Create(lcDriverName, lcHostName: String; lnPort: Integer);
    end;

    TSystem = class
    private
      FcName: String;
      FbActive: Boolean;
      FoServerList: TList;
      procedure SetActive(const Value: Boolean);
      procedure SetName(const Value: String);
      function GetServer(Index: Integer): TServer;
      procedure DoFreeServerList;
    protected
      procedure DoLoadServer; virtual;
    public
      function ServerCount: Integer;
      function GetActiveServer: TServer;
      property Name  : String  read FcName   write SetName;
      property Active: Boolean read FbActive write SetActive;
      property Server[Index: Integer]: TServer read GetServer;

      constructor Create(lcName: String; lbActive: boolean);
      destructor Destroy; override;
    end;

    TUserFlag = class
    private
      FbActive: Boolean;
      FoSystem: TSystem;
      FoServer: TServer;
      procedure SetActive(const Value: Boolean);
      procedure SetSystem(const Value: TSystem);
      procedure SetServer(const Value: TServer);
    protected
    public
      property System: TSystem read FoSystem write SetSystem;
      property Server: TServer read FoServer write SetServer;
      property Active: Boolean read FbActive write SetActive;

      constructor Create(loSystem: TSystem; loServer: TServer);
    end;

    TDSHTTPServiceHelper = class Helper for TDSHTTPService
    private
      { private declarations }
    protected
      { protected declarations }
    public
      { public declarations }
      class var FoSystem: TSystem;
      class var FoServer: TServer;
    published
      { published declarations }
    end;

    TfrmMain = class(TForm)
      DSHTTPService: TDSHTTPService;
      DSHTTPSAManager: TDSHTTPServiceAuthenticationManager;
      Memo1: TMemo;
      procedure DSHTTPSAManagerHTTPAuthenticate(Sender: TObject; const Protocol,
        Context, User, Password: string; var valid: Boolean);
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
    private
      FoSystemList: TList;
      procedure DoLoadSystem;
      function SystemByName(lcValue: String): TSystem;
      function CommandTypeToString(loCmdType: TDSHTTPCommandType): String;
      procedure Redirect(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);
      procedure OpenBackupSession(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);
      procedure CloseBackupSession(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);
      procedure WriteBackupSession(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);
      procedure ReadBackupSession(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);
      procedure ErrorWriteBackupSession(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);
      procedure ErrorReadBackupSession(Sender: TObject; Session: TDSTunnelSession; Content: TBytes;
        var Count: Integer);

      procedure LogBytes(SessionContent: TBytesStream; Buf: TBytes; Count: integer; Op: byte);
      function ReadLogBytes(SessionContent: TBytesStream; var Buf: TBytes; out Count: integer): byte;
      procedure RestoreSessionState(Session: TDSTunnelSession);

      procedure Initialize;
      procedure Finalize;


      procedure Log(lcValue: String);
    public

    end;

var
    frmMain: TfrmMain;



implementation

{$R *.DFM}

uses DBXCommon;

function SortByConnectionCount(Item1, Item2: Pointer): Integer;
begin
    Result := CompareText(IntToStr(TServer(Item1).ConnectionCount),
      IntToStr(TServer(Item2).ConnectionCount));
end;

procedure TfrmMain.CloseBackupSession(Sender: TObject; Session:
TDSTunnelSession;
    Content: TBytes; var Count: Integer);
var
    loSessionContent: TBytesStream;
    lnIndex: Integer;
    loUserFlag: TUserFlag;
begin
    loSessionContent := TBytesStream(Session.UserPointer);

    if TUserFlag(Session.UserFlag) <> nil
    then begin
           loUserFlag:= TUserFlag(Session.UserFlag);
           lnIndex   := loUserFlag.Server.SessionId.IndexOf(Session.SessionId);
           if lnIndex >= 0
           then loUserFlag.Server.SessionId.Delete(lnIndex);
           FreeAndNil(loUserFlag);
         end;

    if loSessionContent <> nil
    then begin
           loSessionContent.Free;
           Session.UserPointer := nil;
         end;
end;

function TfrmMain.CommandTypeToString(loCmdType: TDSHTTPCommandType):
String;
begin
    case loCmdType of
      hcUnknown: Result:= 'UNKWON';
      hcOther: Result  := 'OTHER';
      hcGET: Result    := 'GET';
      hcPOST: Result   := 'POST';
      hcDELETE: Result := 'DELETE';
      hcPUT: Result    := 'PUT';
    end;
end;

procedure TfrmMain.DoLoadSystem;
var
    loCds: TClientDataSet;
    FoSystem: TSystem;
begin
    try
      loCds := TClientDataSet.Create(nil);
      loCds.LoadFromFile(ExtractFilePath(ParamStr(0)) + 'Out\System.txt');
      loCds.Open;
    except
      begin
        if loCds <> nil
        then FreeAndNil(loCds);

        raise Exception.Create('Erro ao tentar acessar System.txt!');
      end;
    end;

    try
      while not loCds.Eof do
      begin
        FoSystem := TSystem.Create(loCds.FieldByName('Name').AsString,
          loCds.FieldByName('Active').AsBoolean);

        FoSystemList.Add(FoSystem);
        loCds.Next;
      end;
     finally
       FreeAndNil(loCds);
    end;
end;

procedure TfrmMain.DSHTTPSAManagerHTTPAuthenticate(Sender: TObject;
const Protocol,
    Context, User, Password: string; var valid: Boolean);
var
    loSystem: TSystem;
    loServer: TServer;
begin
    loSystem := SystemByName(User);

    if loSystem = nil
    then begin
           valid := false;
           raise Exception.Create('Sistema invalido: ' + User);
         end;

    loServer := loSystem.GetActiveServer;

    if loServer = nil
    then begin
           valid := false;
           raise Exception.Create('Nenhum Servidor Ativo no momento!');
         end;

    DSHTTPService.DSHostname:= loServer.HostName;
    DSHTTPService.DSPort    := loServer.Port;

    TDSHTTPService(Sender).FoSystem:= loSystem;
    TDSHTTPService(Sender).FoServer:= loServer;

    valid := true;
end;

procedure TfrmMain.ErrorReadBackupSession(Sender: TObject; Session:
TDSTunnelSession;
    Content: TBytes; var Count: Integer);
begin
    if TUserFlag(Session.UserFlag).Active
    then begin
           Redirect(Sender, Session, Content, Count);
           Session.UserPointer := Pointer(TBytesStream.Create());
           Count := Session.Read(Content, 0, Count);
         end;
end;

procedure TfrmMain.ErrorWriteBackupSession(Sender: TObject;
    Session: TDSTunnelSession; Content: TBytes; var Count: Integer);
begin
    if TUserFlag(Session.UserFlag).Active
    then begin
           Redirect(Sender, Session, Content, Count);
           Session.UserPointer := Pointer(TBytesStream.Create());
           Count := Session.Write(Content, 0, Count);
         end;
end;

procedure TfrmMain.Finalize;
var
    lnCont: Integer;
begin
    for lnCont := FoSystemList.Count - 1 downto 0 do
    begin
      FoSystemList[lnCont] := nil;
    end;
    FreeAndNil(FoSystemList);
    DSHTTPService.Active := false;
end;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
    Initialize;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
    Finalize;
end;

procedure TfrmMain.Initialize;
begin
    DSHTTPService.HttpServer.TunnelService.OnErrorOpenSession := Redirect;
    DSHTTPService.HttpServer.TunnelService.OnErrorWriteSession:= ErrorWriteBackupSession;
    DSHTTPService.HttpServer.TunnelService.OnErrorReadSession := ErrorReadBackupSession;
    DSHTTPService.HttpServer.TunnelService.OnOpenSession      := OpenBackupSession;
    DSHTTPService.HttpServer.TunnelService.OnWriteSession     := WriteBackupSession;
    DSHTTPService.HttpServer.TunnelService.OnReadSession      := ReadBackupSession;
    DSHTTPService.HttpServer.TunnelService.OnCloseSession     := CloseBackupSession;

    DSHTTPService.Active := true;

    FoSystemList := TList.Create;
    DoLoadSystem;
end;

procedure TfrmMain.Log(lcValue: String);
begin
    Memo1.Lines.Add(lcValue);
end;

procedure TfrmMain.LogBytes(SessionContent: TBytesStream; Buf: TBytes;
    Count: integer; Op: byte);
var
    loHeader: TBytes;
begin
    SetLength(loHeader, 5);
    loHeader[0] := Op;
    loHeader[1] := (Count shr 24) and 255;
    loHeader[2] := (Count shr 16) and 255;
    loHeader[3] := (Count shr 8) and 255;
    loHeader[4] := Count and 255;

    SessionContent.Write(loHeader[0], 5);
    SessionContent.Write(Buf[0], Count);
end;

procedure TfrmMain.OpenBackupSession(Sender: TObject; Session:
TDSTunnelSession;
    Content: TBytes; var Count: Integer);
var
    loUserFlag: TUserFlag;
begin
    TDSHttpService(Sender).FoServer.SessionId.Add(Session.SessionId);

    if Session.UserFlag = 0
    then begin
           loUserFlag := TUserFlag.Create(TDSHttpService(Sender).FoSystem,
             TDSHttpService(Sender).FoServer);
           Session.UserFlag := Integer(loUserFlag);
         end
    else begin
           TUserFlag(Session.UserFlag).System := TDSHttpService(Sender).FoSystem;
           TUserFlag(Session.UserFlag).Server := TDSHttpService(Sender).FoServer;
         end;

    if TUserFlag(Session.UserFlag).Active
    then Session.UserPointer := Pointer(TBytesStream.Create());
end;

procedure TfrmMain.ReadBackupSession(Sender: TObject; Session:
TDSTunnelSession;
    Content: TBytes; var Count: Integer);
var
    loSessionContent: TBytesStream;
begin
    if TUserFlag(Session.UserFlag).Active
    then begin
           loSessionContent := TBytesStream(Session.UserPointer);
           LogBytes(loSessionContent, Content, Count, 2);
         end;
end;

function TfrmMain.ReadLogBytes(SessionContent: TBytesStream; var Buf:
TBytes;
    out Count: integer): byte;
var
    loHeader: TBytes;
begin
    if SessionContent.Position < SessionContent.Size
    then begin
           SetLength(loHeader, 5);

           SessionContent.Read(loHeader[0], 5);
           Result := loHeader[0];

           Count := loHeader[1];
           Count := (Count shl 8) or loHeader[2];
           Count := (Count shl 8) or loHeader[3];
           Count := (Count shl 8) or loHeader[4];
           SetLength(Buf, Count);
           SessionContent.Read(Buf[0], Count);
         end
    else Result := 0;
end;

procedure TfrmMain.Redirect(Sender: TObject; Session: TDSTunnelSession;
    Content: TBytes; var Count: Integer);
var
    DBXProperties: TDBXDatasnapProperties;
    lcMsg: String;
    loServer: TServer;
    loSystem: TSystem;
begin
    if Sender is Exception
    then lcMsg := Exception(Sender).Message;

    loServer := TUserFlag(Session.UserFlag).System.GetActiveServer;

    if loServer <> nil
    then begin
           DBXProperties                                     := TDBXDatasnapProperties.Create(nil);
           DBXProperties.Values[TDBXPropertyNames.DriverName]:= loServer.DriverName;
           DBXProperties.Values[TDBXPropertyNames.HostName]  := loServer.HostName;
           DBXProperties.Values[TDBXPropertyNames.Port]      := IntToStr(loServer.Port);
           TDSHTTPService(Sender).FoServer := loServer;
         end
    else begin
           TUserFlag(Session.UserFlag).Active := false;
           raise Exception.Create('Nenhum Servidor ativo no momento!');
         end;

    try
      loSystem:= TUserFlag(Session.UserFlag).System;
      try
        Session.Reopen(DBXProperties);
        RestoreSessionState(Session);
        CloseBackupSession(Sender, Session, Content, Count);
        loServer.SessionId.Add(Session.SessionId);
        Session.UserFlag := Integer(TUserFlag.Create(loSystem, loServer));
      except
        raise Exception.Create('Erro ao tentar reabrir sessão');
      end;
    finally
      FreeAndNil(DBXProperties);
    end;
end;

procedure TfrmMain.RestoreSessionState(Session: TDSTunnelSession);
var
    loSessionContent: TBytesStream;
    loBuf: TBytes;
    lnCount: Integer;
    loOp: Byte;
begin
    loSessionContent         := TBytesStream(Session.UserPointer);
    loSessionContent.Position:= 0;
    repeat
      loOp:= ReadLogBytes(loSessionContent, loBuf, lnCount);
      case loOp of
        2: //read
        begin
          Session.Read(loBuf, 0, lnCount);
        end;
        1: // write
        begin
          Session.Write(loBuf, 0, lnCount);
        end;
      end;
    until loOp = 0;
end;

function TfrmMain.SystemByName(lcValue: String): TSystem;
var
    lnCount: Integer;
begin
    Result := nil;
    for lnCount := 0 to FoSystemList.Count - 1 do
      if UpperCase(lcValue) = UpperCase(FoSystemList[lnCount].Name)
      then begin
             Result := FoSystemList[lnCount];
             Break;
           end;
end;

procedure TfrmMain.WriteBackupSession(Sender: TObject; Session:
TDSTunnelSession;
    Content: TBytes; var Count: Integer);
var
    SessionContent: TBytesStream;
begin
    if TUserFlag(Session.UserFlag).Active
    then begin
           SessionContent := TBytesStream(Session.UserPointer);
           LogBytes(SessionContent, Content, Count, 1);
         end;
end;

{ TServer }

function TServer.ConnectionCount: Integer;
begin
    Result:= FoSessionId.Count;
end;

constructor TServer.Create(lcDriverName, lcHostName: String; lnPort:
Integer);
begin
    inherited Create;

    DriverName:= lcDriverName;
    HostName  := lcHostName;
    Port      := lnPort;
    SessionId := TStringList.Create;
end;

procedure TServer.SetDriverName(const Value: String);
begin
    if Value = ''
    then raise Exception.Create('A propriedade DriverName não pode ser nulo!');

    FcDriverName := Value;
end;

procedure TServer.SetHostName(const Value: String);
begin
    if Value = ''
    then raise Exception.Create('A propriedade HostName não pode ser nulo!');

    FcHostName := Value;
end;

procedure TServer.SetSessionId(const Value: TStringList);
begin
    FoSessionId := Value;
end;

procedure TServer.SetSetPort(const Value: Integer);
begin
    if Value = 0
    then raise Exception.Create('A propriedade Port não pode ser igual á 0!');

    FnPort := Value;
end;

{ TSystem }

constructor TSystem.Create(lcName: String; lbActive: boolean);
begin
    inherited Create;

    if lcName = ''
    then raise Exception.Create('A propriedade Name não pode ser nula!');

    Name        := lcName;
    Active      := lbActive;
    FoServerList:= TList.Create;

    DoLoadServer;
end;

destructor TSystem.Destroy;
begin
    DoFreeServerList;
    inherited;
end;

procedure TSystem.DoFreeServerList;
var
    lnCont: Integer;
begin
    for lnCont := ServerCount - 1 downto 0 do
    begin
      FoServerList[lnCont] := nil;
    end;
    FreeAndNil(FoServerList);
end;

procedure TSystem.DoLoadServer;
var
    loCds: TClientDataSet;
    FoServer: TServer;
begin
    try
      loCds := TClientDataSet.Create(nil);
      loCds.Close;
      loCds.LoadFromFile(ExtractFilePath(ParamStr(0)) + 'Out\Server.txt');
      loCds.Open;
    except
      begin
        if loCds <> nil
        then FreeAndNil(loCds);

        raise Exception.Create('Erro ao tentar acessar Server.txt!');
      end;
    end;

    try
      while not loCds.Eof do
      begin
        if UpperCase(Name) = UpperCase(loCds.FieldByName('SystemName').AsString)
        then begin
               FoServer := TServer.Create(loCds.FieldByName('DriverName').AsString,
                 loCds.FieldByName('HostName').AsString, loCds.FieldByName('Port').AsInteger);

               FoServerList.Add(FoServer);
             end;
        loCds.Next;
      end;
     finally
       FreeAndNil(loCds);
    end;
end;

function TSystem.GetActiveServer: TServer;
    function ActiveConnection(lcDriverName, lcHostName, lcPort: String):
boolean;
    var
       loSQLConn: TSQLConnection;
    begin
      loSQLConn := TSQLConnection.Create(nil);
      try
        loSQLConn.Connected                := false;
        loSQLConn.DriverName               := lcDriverName;
        loSQLConn.Params.Values['HostName']:= lcHostName;
        loSQLConn.Params.Values['Port']    := lcPort;
        try
          loSQLConn.Connected:= true;
          if loSQLConn.Connected
          then Result:= true
          else Result:= false;
        except
          Result:= false;
        end;
      finally
        if loSQLConn <> nil
        then FreeAndNil(loSQLConn)
      end;
    end;
var
    lnCount: Integer;
begin
    TList(FoServerList).Sort(@SortByConnectionCount);

    Result := nil;
    for lnCount := 0 to ServerCount - 1 do
    begin
      if ActiveConnection(Trim(Server[lnCount].DriverName),
          Trim(Server[lnCount].HostName), Trim(IntToStr(Server[lnCount].Port)))
      then begin
             Result := Server[lnCount];
             Break;
           end;
    end;
end;

function TSystem.GetServer(Index: Integer): TServer;
begin
    if Index < 0
    then raise Exception.Create('Índice ' + IntToStr(Index) + ' fora da lista');

    if Index > (FoServerList.Count - 1)
    then raise Exception.Create('Índice ' + IntToStr(Index) + ' fora da lista');

    Result := FoServerList.Items[Index] as TServer;
end;

function TSystem.ServerCount: Integer;
begin
    Result := FoServerList.Count;
end;

procedure TSystem.SetActive(const Value: Boolean);
begin
    FbActive := Value;
end;

procedure TSystem.SetName(const Value: String);
begin
    FcName := Value;
end;

{ TUserFlag }

constructor TUserFlag.Create(loSystem: TSystem; loServer: TServer);
begin
    System:= loSystem;
    Server:= loServer;
    Active:= true;
end;

procedure TUserFlag.SetActive(const Value: Boolean);
begin
    FbActive := Value;
end;

procedure TUserFlag.SetServer(const Value: TServer);
begin
    if Value = nil
    then raise Exception.Create('A propriedade Server não pode ser nula!');

    FoServer := Value;
end;

procedure TUserFlag.SetSystem(const Value: TSystem);
begin
    if Value = nil
    then raise Exception.Create('A propriedade System não pode ser nula!');

    FoSystem := Value;
end;

end.

Jardson Cleyton

Jardson Cleyton

Responder

Posts

21/06/2011

Alessandro Zanela

Achei sua implementação muito intereçante. Será que poderiamos conversar mais sobre o assunto? Estou precisando implementar load balance no meu projeto e gostei bastante da sua ideia e gostaria de pegar umas dicas. Desde já agradeço e aguardo retorno

Elan
msn: elandf@hotmail.com
skype: elanfraga
Responder

Gostei + 0

16/03/2014

Camilo França

caro Jardson, tenho interesse em montar um servidor parecido com esse acima montado por você, caso tenha disponibilidade entre em contato para que possamos detalhar as necessidades e valores do serviço.

skype: camilo_netbusiness
email: camilo@nbti.com.br

Grato.
Responder

Gostei + 0

Utilizamos cookies para fornecer uma melhor experiência para nossos usuários, consulte nossa política de privacidade.

Aceitar