Como construímos uma API RESTful temporal e assíncrona baseada em Vert.x, Keycloak e Kotlin / Coroutines para Sirix.io (Open Source)

Johannes Lichtenberger Blocked Unblock Seguir Seguindo 29 de dezembro de 2018 Visualização interativa – Usa pacotes de borda hierárquica para visualizar nós movidos.

Por que armazenar dados históricos torna-se viável hoje em dia

A vida é subjugada a constante evolução. Assim são nossos dados, seja em pesquisa, negócios ou gerenciamento de informações pessoais. Como tal, é surpreendente que os bancos de dados geralmente mantenham o estado atual. Com o advento, no entanto, de drives flash como por exemplo SSDs, que são muito mais rápidos em acessar dados aleatoriamente em contraste com discos giratórios e não muito bons em apagar ou ignorar dados, agora somos capazes de desenvolver algoritmos inteligentes de versionamento e sistemas de armazenamento para manter estados passados sem impedir a eficiência / desempenho. Portanto, as operações de pesquisa / inserção / exclusão devem estar no tempo logarítmico (O (log (n)), para competir com as estruturas de índice comumente usadas.

O sistema de armazenamento temporal SirixDB

O Sirix é um sistema de armazenamento temporário com versão, que é estruturado por log no seu próprio núcleo.

Aceitamos N transações somente leitura , que estão vinculadas a uma única revisão (cada transação pode ser iniciada em qualquer revisão anterior) simultaneamente a uma transação de gravação em um único recurso. Nosso sistema, portanto, é baseado no isolamento de instantâneos . A transação de gravação pode reverter a revisão mais recente para qualquer revisão anterior. As alterações feitas nessa revisão anterior podem ser confirmadas para criar um novo instantâneo e, portanto, uma nova revisão.

As gravações são em lote e sincronizadas em disco em uma passagem de pós-ordem da estrutura em árvore do índice interno, durante uma confirmação de transação. Assim, podemos armazenar hashes das páginas em ponteiros-pai, da mesma forma que o ZFS, para futuras verificações de integridade .

Instantâneos, ou seja, novas revisões são criadas durante cada confirmação. Além de um ID de revisão numérico, o registro de data e hora é serializado. Uma revisão pode depois ser aberta especificando o ID ou o timestamp. Usar um registro de data e hora invoca uma pesquisa binária em uma matriz de registros de data e hora, armazenada persistentemente em um segundo arquivo e carregada na memória na inicialização. A pesquisa termina se o registro de data e hora exato for encontrado ou a revisão mais próxima do momento determinado. Os dados nunca são gravados no mesmo local e, portanto, não são modificados no local. Em vez disso, o Sirix usa a semântica copy-on-write (COW) no nível de registro (cria fragmentos de página e geralmente não copia páginas inteiras). Toda vez que uma página precisa ser modificada, os registros que foram alterados, bem como alguns dos registros inalterados, são gravados em um novo local. Quais registros exatamente são copiados depende do algoritmo de controle de versão usado . É, portanto, especialmente adequado para drives baseados em flash, como por exemplo, SSDs. Alterações em um recurso dentro de um banco de dados ocorrem dentro da transação de gravação única vinculada ao recurso mencionada anteriormente. Portanto, primeiro, um ResourceManager deve ser aberto no recurso específico para iniciar transações individuais com todos os recursos. Note que já começamos a trabalhar em transações de banco de dados 🙂

Recentemente, escrevemos outro artigo com muito mais informações sobre os princípios por trás do Sirix.

API simples baseada em cursor de transação

A seguir, um código Java simples para criar um banco de dados, um recurso dentro do banco de dados e a importação de um documento XML. Ele será fragmentado em nossa representação interna (que pode ser considerada uma implementação DOM persistente, portanto, tanto um layout na memória quanto um formato de serialização binária estão envolvidos).

O armazenamento nativo do JSON será o próximo. Em geral, todos os tipos de dados podem ser armazenados no Sirix, desde que possam ser buscados por um identificador de registro sequencial gerado, que é atribuído pelo Sirix durante a inserção e um serializador / desserializador customizado é conectado. No entanto, estamos trabalhando em várias camadas pequenas para armazenar nativamente dados JSON.

Vert.x, Kotlin / Coroutines e Keycloak

O Vert.x, por outro lado, é modelado de perto após o Node.js e para a JVM. Tudo no Vert.x deve ser não bloqueador. Assim, um único encadeamento chamado de event-loop pode manipular muitos pedidos. O bloqueio de chamadas deve ser tratado em um pool de threads especial. O padrão são dois loops de evento por CPU (Padrão de Vários Reatores).

Estamos usando o Kotlin , porque é simples e conciso. Uma das características, que é realmente interessante, são coroutines. Conceitualmente eles são como fios muito leves. Embora a criação de threads seja muito cara, criar uma co-rotina não é. Coroutines permitem escrever de código assíncrono quase como sequencial. Sempre que ele é suspenso devido ao bloqueio de chamadas ou a longas tarefas em execução, o encadeamento subjacente não é bloqueado e pode ser reutilizado. Sob o capô, cada função de suspensão recebe outro parâmetro através do compilador Kotlin, uma continuação, que armazena onde retomar a função (retomada normal, retomada com uma exceção).

O Keycloak é usado como o servidor de autorização por meio do OAuth2 (Password Credentials Flow), pois decidimos não implementar a autorização por nós mesmos.

Coisas a considerar ao construir o servidor

Primeiro, temos que decidir qual fluxo OAuth2 melhor atende às nossas necessidades. Como construímos uma API REST geralmente não consumida pelos agentes / navegadores do usuário, decidimos usar o Fluxo de Credenciais da Pasword. É tão simples como isto: primeiro obter um token de acesso, segundo enviar com cada solicitação no cabeçalho de autorização.

Para obter o token de acesso, primeiro uma solicitação deve ser feita em relação a uma rota POST / login com as credenciais de nome de usuário / senha enviadas no corpo como um objeto JSON.

A implementação é assim:

O manipulador de co-rotina é uma função de extensão simples:

Coroutines são lançados no loop de eventos Vert.x (o dispatcher).

Para executar um manipulador de execução mais longa, usamos

O Vert.x usa um pool de threads diferente para esse tipo de tarefa. A tarefa é executada em outro thread. Esteja ciente de que o loop de eventos não será bloqueado, a co-rotina será suspensa.

Design de API pelo exemplo

Agora estamos mudando o foco para nossa API novamente e mostramos como ele foi projetado com exemplos . Primeiro precisamos configurar nosso servidor e Keycloak (leia em http://sirix.io como fazer isso).

Quando os dois servidores estiverem ativos e funcionando, poderemos escrever um simples cliente HTTP. Primeiro, precisamos obter um token do ponto de extremidade /login com um determinado JSON-Object "username / password". Usando um HTTP Client assíncrono (do Vert.x) no Kotlin, é assim:

Este token de acesso deve então ser enviado no cabeçalho HTTP de autorização para cada solicitação subseqüente. Armazenar um primeiro recurso se parece com isso (simples solicitação HTTP PUT):

Primeiro, um banco de dados vazio com o database nomes com alguns metadados é criado, segundo o fragmento XML é armazenado com o nome resource1 . A solicitação HTTP PUT é idempotente. Outra solicitação PUT com o mesmo ponto de extremidade de URL apenas excluiria o banco de dados e o recurso antigos e recriaria o banco de dados / recurso.

O código de resposta HTTP deve ser 200 (tudo correu bem), caso em que o corpo HTTP produz:

Estamos serializando os IDs gerados de nosso sistema de armazenamento para nós de elementos.

Por meio de uma GET HTTP-Request para https://localhost:9443/database/resource1 , também podemos recuperar o recurso armazenado novamente.

No entanto, isso não é realmente interessante até agora. Podemos atualizar o recurso por meio de uma POST-Request . Supondo que recuperamos o token de acesso como antes, podemos simplesmente fazer uma solicitação de POST e usar as informações que coletamos antes sobre os IDs de nós:

A parte interessante é a URL que estamos usando como o endpoint. Nós simplesmente dizemos, selecione o nó com o ID 3, então insira o fragmento XML dado como o primeiro filho. Isso produz o seguinte documento XML serializado:

Cada PUT – assim como a solicitação POST commits implicitamente a transação subjacente. Assim, agora podemos enviar a primeira solicitação GET para recuperar o conteúdo de todo o recurso novamente, por exemplo, especificando uma consulta XPath simples, para selecionar o nó raiz em todas as revisões GET https://localhost:9443/database/resource1?query=/xml/all-time::* e obtenha o seguinte resultado XPath:

Note que usamos um eixo de deslocamento do tempo no parâmetro de consulta. Em geral, suportamos vários eixos XPath temporais adicionais: futuro, futuro-ou-eu, passado, passado-ou-eu, anterior, anterior-ou-eu, próximo, próximo-ou-eu, primeiro, último, todos os tempos

Os eixos de tempo são compatíveis com testes de nó:
<eixo do tempo> :: <teste do nó>
é definido como
<eixo do tempo> :: * / self :: <teste do nó>.

É claro que a maneira usual seria usar um dos eixos XPath padrão primeiro para navegar até os nós de seu interesse, por exemplo, o eixo descendente e / ou filho, adicionar predicado (s) e, em seguida, navegar no tempo, para observar como um nó e uma subárvore mudaram. Este é um recurso poderoso incrível e pode ser o assunto de um artigo futuro.

O mesmo pode ser alcançado através da especificação de um intervalo de revisões para serializar (parâmetros de revisão inicial e final) na solicitação GET:

GET https://localhost:9443/database/resource1?start-revision=1&end-revision=2

ou via timestamps:

GET https://localhost:9443/database/resource1?start-revision-timestamp=2018-12-20T18:00:00&end-revision-timestamp=2018-12-20T19:00:00

No entanto, se primeiro abrirmos um recurso, por meio de uma consulta, selecionar nós individuais será mais rápido usar o eixo de deslocamento no tempo, caso contrário, a mesma consulta deverá ser executada para cada revisão aberta (analisada, compilada, executada…).

Com certeza, também podemos excluir o recurso ou qualquer subárvore dele por uma expressão XQuery de atualização (que não é muito RESTful) ou com uma simples solicitação HTTP DELETE :

Isso exclui o nó com ID 3 e, no nosso caso, como um nó de elemento, a subárvore inteira. Com certeza ele é confirmado como revisão 3 e, como tal, todas as revisões antigas ainda podem ser consultadas para toda a subárvore como era durante a transação-commit (na primeira revisão é apenas o elemento com o nome “barra” sem nenhuma subárvore).

Se quisermos obter um diff, atualmente na forma de um XQuery Update Statement, simplesmente chame a função XQuery sdb:diff que é definida como:

sdb:diff($coll as xs:string, $res as xs:string, $rev1 as xs:int, $rev2 as xs:int) as xs:string

Poderíamos especificar outros formatos de serialização com certeza.

Por exemplo, podemos enviar uma solicitação GET assim no banco de dados / resource1 que criamos acima:

GET https://localhost:9443/?query= sdb%3Adiff%28%27database%27%2C%27resource1%27%2C1%2C2%29

Note que o query-String tem que ser codificado por URL, assim decodificado

sdb:diff('database','resource1',1,2)

e estamos comparando a revisão 1 e 2 (mas o desempenho da distribuição é, com certeza, a mesma complexidade de tempo para cada tupla de revisão que comparamos). A saída para o diff em nosso exemplo é essa instrução XQuery-Update envolvida em um elemento sequence de fechamento:

Isso significa que resource1 from database é aberto na primeira revisão. Em seguida, a subárvore <xml>foo<bar/></xml> é anexada ao nó com a identificação de nó estável 3 como um primeiro filho.