Como solucionar a falta de transações no MongoDB?


139

Eu sei que existem perguntas semelhantes aqui, mas elas estão me dizendo para voltar aos sistemas RDBMS regulares se eu precisar de transações ou usar operações atômicas ou confirmação de duas fases . A segunda solução parece a melhor escolha. O terceiro que não desejo seguir, porque parece que muitas coisas podem dar errado e não posso testá-lo em todos os aspectos. Estou tendo dificuldade em refatorar meu projeto para executar operações atômicas. Não sei se isso vem do meu ponto de vista limitado (só trabalhei com bancos de dados SQL até agora) ou se realmente não pode ser feito.

Gostaríamos de testar o MongoDB em nossa empresa. Escolhemos um projeto relativamente simples - um gateway SMS. Ele permite que nosso software envie mensagens SMS para a rede celular e o gateway faz o trabalho sujo: na verdade, comunicando-se com os provedores por meio de diferentes protocolos de comunicação. O gateway também gerencia o faturamento das mensagens. Todo cliente que solicita o serviço precisa comprar alguns créditos. O sistema diminui automaticamente o saldo do usuário quando uma mensagem é enviada e nega o acesso se o saldo for insuficiente. Também porque somos clientes de provedores de SMS de terceiros, também podemos ter nossos próprios saldos com eles. Temos que acompanhar isso também.

Comecei a pensar em como posso armazenar os dados necessários com o MongoDB se reduzir alguma complexidade (cobrança externa, envio de SMS em fila). Vindo do mundo SQL, eu criaria uma tabela separada para usuários, outra para mensagens SMS e outra para armazenar as transações relacionadas ao saldo dos usuários. Digamos que eu crie coleções separadas para todos aqueles no MongoDB.

Imagine uma tarefa de envio de SMS com as seguintes etapas neste sistema simplificado:

  1. verifique se o usuário possui saldo suficiente; negar acesso se não houver crédito suficiente

  2. envie e armazene a mensagem na coleção do SMS com os detalhes e o custo (no sistema statusativo , a mensagem teria um atributo e uma tarefa a buscaria para entrega e definiria o preço do SMS de acordo com seu estado atual)

  3. diminuir o saldo do usuário pelo custo da mensagem enviada

  4. registrar a transação na coleção de transações

Agora, qual é o problema com isso? O MongoDB pode fazer atualizações atômicas apenas em um documento. No fluxo anterior, pode acontecer que algum tipo de erro entre e a mensagem seja armazenada no banco de dados, mas o saldo do usuário não é atualizado e / ou a transação não é registrada.

Eu vim com duas idéias:

  • Crie uma única coleção para os usuários e armazene o saldo como um campo, transações e mensagens relacionadas ao usuário como sub-documentos no documento do usuário. Como podemos atualizar documentos atomicamente, isso realmente resolve o problema da transação. Desvantagens: se o usuário enviar muitas mensagens SMS, o tamanho do documento poderá aumentar e o limite de 4 MB poderá ser atingido. Talvez eu possa criar documentos históricos em tais cenários, mas não acho que seja uma boa ideia. Também não sei quão rápido o sistema seria se eu enviasse cada vez mais dados para o mesmo grande documento.

  • Crie uma coleção para usuários e outra para transações. Pode haver dois tipos de transações: compra de crédito com alteração positiva do saldo e mensagens enviadas com alteração negativa do saldo. A transação pode ter um subdocumento; por exemplo, nas mensagens enviadas, os detalhes do SMS podem ser incorporados na transação. Desvantagens: eu não armazeno o saldo atual do usuário, por isso tenho que calculá-lo toda vez que um usuário tenta enviar uma mensagem para saber se a mensagem pode passar ou não. Receio que esse cálculo possa ficar lento à medida que o número de transações armazenadas aumenta.

Estou um pouco confuso sobre qual método escolher. Existem outras soluções? Não consegui encontrar práticas recomendadas on-line sobre como solucionar esses tipos de problemas. Eu acho que muitos programadores que estão tentando se familiarizar com o mundo NoSQL estão enfrentando problemas semelhantes no começo.


61
Perdoe-me se eu estiver errado, mas parece que este projeto usará um repositório de dados NoSQL, independentemente de ele ser beneficiado ou não. Os NoSQL não são uma alternativa ao SQL como uma opção "fashion", mas para quando a tecnologia dos RDBMS relacionais não se encaixa no espaço do problema e um armazenamento de dados não relacional. Muitas das suas perguntas têm "Se fosse SQL, então ..." e isso soa um aviso para mim. Todos os NoSQL vieram da necessidade de resolver um problema que o SQL não conseguiu e, em seguida, foram generalizados para facilitar o uso e, é claro, o movimento começa a rolar.
PurplePilot

4
Estou ciente de que este projeto não é exatamente o melhor para experimentar o NoSQL. No entanto, estou com medo se começarmos a usá-lo com outros projetos (digamos, um software de gerenciamento de coleções de bibliotecas porque estamos no gerenciamento de coleções) e, de repente, algum tipo de solicitação surgir, que precisa de transações (e na verdade existe, imagine que os livros é transferido de uma coleção para outra), precisamos saber como podemos superar o problema. Talvez seja apenas eu que tenha uma mente estreita e pense que sempre há uma necessidade de transações. Mas pode ser que exista uma maneira de superá-las de alguma forma.
precisa saber é o seguinte

3
Concordo com o PurplePilot, você deve escolher uma tecnologia que se encaixe em uma solução, e não tentar enxertar uma solução que não seja apropriada para um problema. A modelagem de dados para os bancos de dados de gráficos é um paradigma completamente diferente do design do RDBMS e você precisa esquecer tudo o que sabe e reaprender a nova maneira de pensar.

9
Entendo que devo usar a ferramenta apropriada para a tarefa. No entanto, para mim - quando leio respostas como essa - parece que o NoSQL não é bom para nada em que os dados sejam críticos. É bom para o Facebook ou o Twitter, onde se alguns comentários se perdem, o mundo continua, mas qualquer coisa acima disso está fora do negócio. Se isso é verdade, eu não entendo porque os outros se preocupam com a construção, por exemplo. uma loja virtual com MongoDB: kylebanker.com/blog/2010/04/30/mongodb-and-ecommerce Ele até menciona que a maioria das transações pode ser superada com operações atômicas. O que estou procurando é o como.
precisa saber é o seguinte

2
Você diz "parece que o NoSQL não é bom para nada em que os dados são críticos" não é verdadeiro onde não é bom (talvez) seja o processamento transacional do tipo ACID transacional. Além disso, os NoSQL são projetados para armazenamentos de dados distribuídos, que podem ser muito difíceis de serem alcançados pelos armazenamentos do tipo SQL quando você entra nos cenários de replicação principal do slave. O NoSQL possui estratégias para consistência eventual e garante que apenas o conjunto de dados mais recente seja usado, mas não o ACID.
PurplePilot

Respostas:


23

A partir da versão 4.0, o MongoDB terá transações ACID com vários documentos. O plano é habilitar aqueles em implantações de conjuntos de réplicas primeiro, seguidos pelos clusters fragmentados. As transações no MongoDB parecerão exatamente como os desenvolvedores de transações estão familiarizadas com os bancos de dados relacionais - elas serão de múltiplas instruções, com semântica e sintaxe semelhantes (como start_transactione commit_transaction). É importante ressaltar que as alterações no MongoDB que permitem transações não afetam o desempenho de cargas de trabalho que não as exigem.

Para mais detalhes, consulte aqui .

Ter transações distribuídas, não significa que você deve modelar seus dados como em bancos de dados relacionais tabulares. Aproveite o poder do modelo de documento e siga as boas e recomendadas práticas de modelagem de dados.


1
As transações chegaram! 4,0 GA'ed. mongodb.com/blog/post/...
Grigori Melnik

As transações do MongoDB ainda têm limitação no tamanho da transação, 16 MB; recentemente, tive um caso de uso em que preciso colocar 50k registros de um arquivo no mongoDB; portanto, para manter a propriedade atômica, pensei em usar transações, mas desde 50k registros json exceder esse limite, gera o erro "O tamanho total de todas as operações de transação deve ser menor que 16793600. O tamanho real é 16793817". Para mais informações você pode ir através do aberto bilhete jira oficial no mongoDB jira.mongodb.org/browse/SERVER-36330
Gautam Malik

O MongoDB 4.2 (atualmente em beta, RC4) suporta grandes transações. Ao representar transações em várias entradas do oplog, você poderá gravar mais de 16 MB de dados em uma única transação ACID (sujeita ao tempo máximo de execução padrão padrão de 60 segundos existente). Você pode experimentá-los agora - mongodb.com/download-center/community
Grigori Melnik

O MongoDB 4.2 agora é GA, com suporte total a transações distribuídas. mongodb.com/blog/post/...
Grigori Melnik

83

Vivendo sem transações

As transações suportam propriedades ACID , mas, embora não haja transações MongoDB, temos operações atômicas. Bem, operações atômicas significam que, quando você trabalha em um único documento, esse trabalho será concluído antes que alguém mais veja o documento. Eles verão todas as alterações que fizemos ou nenhuma delas. E usando operações atômicas, muitas vezes você pode realizar o mesmo que realizaríamos usando transações em um banco de dados relacional. E a razão é que, em um banco de dados relacional, precisamos fazer alterações em várias tabelas. Geralmente tabelas que precisam ser unidas e, portanto, queremos fazer isso de uma só vez. E para fazer isso, como existem várias tabelas, teremos que iniciar uma transação, fazer todas essas atualizações e finalizar a transação. Mas comMongoDB , vamos incorporar os dados, já que os pré-juntamos aos documentos e eles são esses documentos avançados que têm hierarquia. Muitas vezes podemos realizar a mesma coisa. Por exemplo, no exemplo do blog, se quisermos ter certeza de que atualizamos uma publicação de blog atomicamente, podemos fazer isso porque podemos atualizar a publicação inteira de uma só vez. Onde, como se fosse um monte de tabelas relacionais, provavelmente teríamos que abrir uma transação para poder atualizar a coleção de postagens e a coleção de comentários.

Então, quais são as nossas abordagens que podemos adotar MongoDBpara superar a falta de transações?

  • reestruturar - reestruture o código, para que trabalhemos em um único documento e aproveitemos as operações atômicas que oferecemos nesse documento. E se fizermos isso, geralmente estamos prontos.
  • implementar em software - podemos implementar o bloqueio de software, criando uma seção crítica. Podemos construir um teste, testar e definir usando localizar e modificar. Podemos construir semáforos, se necessário. E de certa forma, é assim que o mundo maior funciona de qualquer maneira. Se pensarmos sobre isso, se um banco precisar transferir dinheiro para outro banco, eles não estarão vivendo no mesmo sistema relacional. E cada um deles tem seus próprios bancos de dados relacionais com frequência. E eles devem ser capazes de coordenar essa operação, mesmo que não possamos iniciar a transação e finalizar a transação nesses sistemas de banco de dados, apenas dentro de um sistema dentro de um banco. Portanto, certamente existem maneiras no software de contornar o problema.
  • tolerar - a abordagem final, que geralmente funciona em aplicativos da web modernos e em outros aplicativos que absorvem uma quantidade enorme de dados, é apenas tolerar um pouco de inconsistência. Um exemplo seria, se estamos falando de um feed de amigos no Facebook, não importa se todo mundo vê sua atualização de parede simultaneamente. Se ok, se uma pessoa está alguns segundos atrás por alguns segundos e eles a alcançam. Em muitos projetos de sistema, geralmente não é essencial que tudo seja mantido perfeitamente consistente e que todos tenham uma visão perfeitamente consistente e a mesma do banco de dados. Assim, poderíamos simplesmente tolerar um pouco de inconsistência temporária.

Update, findAndModify, $addToSet(Dentro de uma atualização) e $push(dentro de uma atualização) operações operar atomicamente dentro de um único documento.


2
Gosto da maneira como essa resposta é, em vez de continuar perguntando se devemos voltar ao banco de dados relacional. Obrigado @xameeramir!
precisa saber é o seguinte

3
uma seção crítica do código não vai funcionar se você tem mais de 1 servidor, tem que usar um serviço de bloqueio distribuído externa
Alexander Mills

@AlexanderMills Você pode elaborar, por favor?
Zameer

respondere parece ser transcrição de vídeo a partir daqui: youtube.com/watch?v=_Iz5xLZr8Lw
Fritz

Eu acho que isso parece bom até ficarmos restritos a operar na coleção única. Mas não podemos colocar tudo em um único documento por motivos variados (tamanho do documento ou se você estiver usando referências). Acho que podemos precisar de transações.
user2488286

24

Veja isto , por Tokutek. Eles desenvolvem um plug-in para o Mongo que promete não apenas transações, mas também um aumento no desempenho.


@Giovanni Bitliner. O Tokutek foi adquirido pela Percona e, no link que você forneceu, não vejo referência a nenhuma informação sobre o que aconteceu desde o post. Você sabe o que aconteceu com o esforço deles? Enviei por email o endereço de email nessa página para descobrir.
Tyler Collier

Do que você precisa especificamente? Se você precisa de tecnologia toku aplicada a MongoDB tentar github.com/Tokutek/mongo , se você precisar a versão mysql talvez eles acrescentado que a sua versão padrão do MySQL que eles costumam fornecer com
Giovanni Bitliner

Como posso integrar o tokutek com o nodejs.
Manoj Sanjeewa

11

Traga direto ao ponto: se a integridade transacional é uma obrigação , não use o MongoDB, mas use apenas componentes no sistema que suporta transações. É extremamente difícil criar algo sobre o componente para fornecer funcionalidade semelhante a ACID para componentes não compatíveis com ACID. Dependendo dos casos de uso individuais, pode fazer sentido separar ações em ações transacionais e não transacionais de alguma forma ...


1
Eu acho que você quer dizer que o NoSQL pode ser usado como um banco de dados auxiliar com RDBMS clássico. Não gosto da ideia de misturar NoSQL e SQL no mesmo projeto. Aumenta a complexidade e, possivelmente, também apresenta alguns problemas não triviais.
Nagyi

1
As soluções NoSQL raramente são usadas sozinhas. Os repositórios de documentos (mongo e couch) são provavelmente a única exceção a essa regra.
Karoly Horvath

7

Agora, qual é o problema com isso? O MongoDB pode fazer atualizações atômicas apenas em um documento. No fluxo anterior, pode acontecer que algum tipo de erro entre e a mensagem seja armazenada no banco de dados, mas o saldo do usuário não seja reduzido e / ou a transação não seja registrada.

Isso não é realmente um problema. O erro que você mencionou é um erro lógico (bug) ou de E / S (rede, falha no disco). Esse tipo de erro pode deixar os armazenamentos sem transação e transacionais em um estado não consistente. Por exemplo, se ele já enviou SMS, mas durante o armazenamento de um erro de mensagem, ele não pode reverter o envio de SMS, o que significa que não será registrado, o saldo do usuário não será reduzido etc.

O verdadeiro problema aqui é que o usuário pode tirar proveito das condições da corrida e enviar mais mensagens do que seu saldo permite. Isso também se aplica ao RDBMS, a menos que você envie SMS dentro de uma transação com bloqueio de campo de saldo (o que seria um grande gargalo). Como uma possível solução para o MongoDB, seria o findAndModifyprimeiro a reduzir o saldo e verificar, se for negativo, não permitir o envio e o reembolso da quantia (incremento atômico). Se positivo, continue enviando e, caso falhe, devolva o valor. A coleção do histórico de saldo também pode ser atualizada para ajudar a corrigir / verificar o campo de saldo.


Obrigado por esta ótima resposta! Sei que, se eu usar dados de armazenamento com capacidade de transação, eles podem ser corrompidos por causa do sistema SMS, do qual não tenho controle. No entanto, com o Mongo, é possível que erros de dados também ocorram internamente. Digamos que o código altera o saldo do usuário com findAndModify, o saldo fica negativo, mas antes que eu possa corrigir o erro, ocorre um erro e o aplicativo precisa reiniciar. Eu acho que você quer dizer que eu deveria implementar algo semelhante ao commit de duas fases com base na coleta de transações e fazer a verificação de correção regular no banco de dados.
Nagyi

9
Não é verdade, os armazenamentos transacionais serão revertidos se você não fizer uma confirmação final.
Karoly Horvath

9
Além disso, você não envia SMS e depois efetua login no DB, isso é totalmente errado. Primeiro armazene tudo no DB e faça uma confirmação final; depois, você pode enviar a mensagem. Nesse ponto, algo ainda pode falhar, portanto, você precisa de um trabalho cron para verificar se a mensagem foi realmente enviada, se não tentar enviar. Talvez uma fila de mensagens dedicada seja melhor para isso. Mas a coisa toda se resume a saber se você pode enviar SMSs de forma transacional ...
Karoly Horvath

@ NagyI sim, é isso que eu quis dizer. É preciso trocar os benefícios das transações para facilitar a escalabilidade. Basicamente, o aplicativo deve esperar que dois documentos em coleções diferentes possam estar em um estado inconsistente e estar prontos para lidar com isso. @yi_H ele reverterá, mas o estado não será mais real (as informações sobre a mensagem serão perdidas). Isso não é muito melhor do que apenas ter dados parciais (como saldo reduzido, mas nenhuma informação da mensagem ou vice-versa).
Pingw33n

Entendo. Na verdade, essa não é uma restrição fácil. Talvez eu deva aprender mais sobre como os sistemas RDBMS fazem transações. Você pode recomendar algum tipo de material ou livro on-line onde eu possa ler sobre isso?
Nagyi

6

O projeto é simples, mas você precisa oferecer suporte a transações para pagamento, o que dificulta tudo. Assim, por exemplo, um sistema de portal complexo com centenas de coleções (fórum, chat, anúncios, etc ...) é, de certa forma, mais simples, porque se você perder um fórum ou entrada no chat, ninguém realmente se importa. Por outro lado, se você perder uma transação de pagamento, isso é um problema sério.

Portanto, se você realmente deseja um projeto piloto para o MongoDB, escolha um que seja simples a esse respeito.


Obrigado por explicar. Triste ouvir isso. Eu gosto da simplicidade do NoSQL e do uso do JSON. Estamos à procura de uma alternativa ao ORM, mas parece que devemos ficar com ele por um tempo.
precisa saber é o seguinte

Você pode dar bons motivos para o MongoDB ser melhor que o SQL para esta tarefa? O projeto piloto parece um pouco bobo.
309 Karoly Horvath

Eu não disse que o MongoDB é melhor que o SQL. Simplesmente queremos saber se é melhor que o SQL + ORM. Mas agora está ficando mais claro que eles não são competitivos nesse tipo de projeto.
precisa saber é o seguinte

6

As transações estão ausentes no MongoDB por motivos válidos. Essa é uma daquelas coisas que tornam o MongoDB mais rápido.

No seu caso, se a transação é obrigatória, o mongo parece não ser um bom ajuste.

Pode ser RDMBS + MongoDB, mas isso adicionará complexidades e dificultará o gerenciamento e o suporte ao aplicativo.


1
Existe agora uma distribuição de MongoDB chamado TokuMX que utiliza a tecnologia fractal para entregar 50x melhoria de desempenho e dá suporte a transações completo ACID ao mesmo tempo: tokutek.com/tokumx-for-mongodb
OCDev

9
Como uma transação poderia nunca ser um "must". Assim que você precisar de 1 caso simples em que precise atualizar 2 tabelas, o mongo de repente não será mais um bom ajuste? Isso não deixa muitos casos de uso.
Mr_E

1
@Mr_E concorda, é por isso que o MongoDB é meio burro :) #
Alexander Mills

6

Este é provavelmente o melhor blog que eu encontrei sobre a implementação de transações como recurso para o mongodb.!

Sinalizador de sincronização: ideal para copiar dados de um documento mestre

Fila de tarefas: uso geral, resolve 95% dos casos. A maioria dos sistemas precisa ter pelo menos uma fila de tarefas de qualquer maneira!

Confirmação em duas fases: essa técnica garante que cada entidade sempre tenha todas as informações necessárias para chegar a um estado consistente

Reconciliação de log: a técnica mais robusta, ideal para sistemas financeiros

Controle de versão: fornece isolamento e suporta estruturas complexas

Leia isto para obter mais informações: https://dzone.com/articles/how-implement-robust-and


Inclua as partes relevantes do recurso vinculado necessárias para responder à pergunta na sua resposta. Como está, sua resposta é muito suscetível à podridão do link (ou seja, se o site vinculado cair ou mudar sua resposta é potencialmente inútil).
mech

Obrigado @mech pela sugestão
Vaibhav

4

É tarde, mas acho que isso ajudará no futuro. Eu uso o Redis para fazer uma fila para resolver esse problema.

  • Requisito: a
    imagem abaixo mostra 2 ações que precisam ser executadas simultaneamente, mas as fases 2 e 3 da ação 1 precisam ser concluídas antes do início da fase 2 da ação 2 ou oposta (uma fase pode ser uma solicitação REST api, uma solicitação de banco de dados ou executar código javascript ... ) insira a descrição da imagem aqui

  • Como uma fila o ajuda na
    fila Certifique-se de que todos os códigos de bloco entre lock()e release()em muitas funções não sejam executados ao mesmo tempo, faça-os isolar.

    function action1() {
      phase1();
      queue.lock("action_domain");
      phase2();
      phase3();
      queue.release("action_domain");
    }
    
    function action2() {
      phase1();
      queue.lock("action_domain");
      phase2();
      queue.release("action_domain");
    }
  • Como construir uma fila
    Vou focar apenas em como evitar a parte de condições de corrida ao criar uma fila no site de back-end. Se você não conhece a idéia básica da fila, venha aqui .
    O código abaixo mostra apenas o conceito, você precisa implementar da maneira correta.

    function lock() {
      if(isRunning()) {
        addIsolateCodeToQueue(); //use callback, delegate, function pointer... depend on your language
      } else {
        setStateToRunning();
        pickOneAndExecute();
      }
    }
    
    function release() {
      setStateToRelease();
      pickOneAndExecute();
    }

Mas você precisa se isRunning() setStateToRelease() setStateToRunning()isolar ou então enfrenta a condição de raça novamente. Para fazer isso, escolho o Redis para fins de ACID e escalável.
O documento Redis fala sobre sua transação:

Todos os comandos em uma transação são serializados e executados sequencialmente. Nunca pode acontecer que uma solicitação emitida por outro cliente seja atendida no meio da execução de uma transação Redis. Isso garante que os comandos sejam executados como uma única operação isolada.

P / s:
eu uso o Redis porque meu serviço já o usa, você pode usar qualquer outro meio de suporte ao isolamento para fazer isso.
O action_domaincódigo em meu código acima está indicado para quando você precisar apenas da ação 1 chamada do usuário A, bloquear a ação 2 do usuário A, não bloquear outro usuário. A ideia é colocar uma chave exclusiva para o bloqueio de cada usuário.


Você teria recebido mais votos se sua pontuação já fosse maior. É assim que a maioria aqui pensa. Sua resposta é útil no contexto da pergunta. Eu votei em você.
Mukus

3

As transações estão disponíveis agora no MongoDB 4.0. Amostra aqui

// Runs the txnFunc and retries if TransientTransactionError encountered

function runTransactionWithRetry(txnFunc, session) {
    while (true) {
        try {
            txnFunc(session);  // performs transaction
            break;
        } catch (error) {
            // If transient error, retry the whole transaction
            if ( error.hasOwnProperty("errorLabels") && error.errorLabels.includes("TransientTransactionError")  ) {
                print("TransientTransactionError, retrying transaction ...");
                continue;
            } else {
                throw error;
            }
        }
    }
}

// Retries commit if UnknownTransactionCommitResult encountered

function commitWithRetry(session) {
    while (true) {
        try {
            session.commitTransaction(); // Uses write concern set at transaction start.
            print("Transaction committed.");
            break;
        } catch (error) {
            // Can retry commit
            if (error.hasOwnProperty("errorLabels") && error.errorLabels.includes("UnknownTransactionCommitResult") ) {
                print("UnknownTransactionCommitResult, retrying commit operation ...");
                continue;
            } else {
                print("Error during commit ...");
                throw error;
            }
       }
    }
}

// Updates two collections in a transactions

function updateEmployeeInfo(session) {
    employeesCollection = session.getDatabase("hr").employees;
    eventsCollection = session.getDatabase("reporting").events;

    session.startTransaction( { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } } );

    try{
        employeesCollection.updateOne( { employee: 3 }, { $set: { status: "Inactive" } } );
        eventsCollection.insertOne( { employee: 3, status: { new: "Inactive", old: "Active" } } );
    } catch (error) {
        print("Caught exception during transaction, aborting.");
        session.abortTransaction();
        throw error;
    }

    commitWithRetry(session);
}

// Start a session.
session = db.getMongo().startSession( { mode: "primary" } );

try{
   runTransactionWithRetry(updateEmployeeInfo, session);
} catch (error) {
   // Do something with error
} finally {
   session.endSession();
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.