Um dos projetos em que a equipe de pesquisa do Incubaid está trabalhando é um novo banco de dados incorporado (onde “incorporado” se refere ao caminho de outros projetos como o GNU DBM, Berkeley DB, Tokyo Cabinet ou SQLite são usados). Esse projeto, de codenome Baardskeerder, está ainda começando e estamos brincando com abordagens diferentes para vários problemas.
O próprio banco de dados não expõe uma interface completa SQL, como a SQLite faz: a interface fornece a capacidade de armazenar valores para uma determinada chave, recupera o valor armazenado em uma chave, e algumas operações mais complexas como pesquisas de alcance.
A API para uma operação get parece bastante óbvia à primeira vista: você transmite em uma chave, e o valor correspondente será devolvido se a chave tiver sido definida antes, ou a não-existência é sinalizada de alguma maneira (nota: todos os pseudocódigos neste artigo são Haskell):
01 import qualified Data.ByteString.Char8 as BS 02 import Data.ByteString.Char8 (ByteString) 03 04 type Key = ByteString 05 type Value = ByteString 06 07 data DB = DB 08 09 get :: DB -> Key -> Maybe Value 10 get d k = undefined
Uma vez que o banco de dados é mantido (por exemplo, um arquivo em um sistema de arquivos em um disco), e provavelmente será usado pelo servidor de aplicações de estilo, retornando os valores para algum cliente, enquanto o servidor de aplicações não se importa com os valores próprios, esse estilo de API possui uma grande desvantagem: ele força a cópia dos valores de espaço do kernelspace para o userspace, após o qual o valor é escrito em algum socket, copiando do userspace para kernelspace, sem quaisquer modificações intermediárias, ou mesmo de acesso.
Em kernels mais modernos, incluindo Linux, BSD, Solaris e outros, existem várias chamadas de sistemas disponíveis que permitem uma enviar dados de arquivos em um socket, sem essa cópia inútil e do userspace. Elas incluem sendfile(2) e splice(2). Queremos ser capazes de suportar essas otimizações a partir de aplicativos incorporando da biblioteca de banco de dados, enquanto continua a fornecer a API “básica” também.
Uma abordagem seria implementar uma chamada getAndSendOnSocket personalizada na biblioteca, mas isso parece muito ad-hoc. Outra forma de combater isso é retornar um descritor de arquivo, um offset e o tamanho de uma chamada get, mas isso não parece certo, pois não queremos vazar um descritor de arquivo para o aplicativo host como está.
O que temos agora é uma abordagem orientada a callback/continuation, na qual um aplicativo host pode fornecer uma ação que terá acesso ao descritor de arquivo, obter um offset e valor de comprimento, e pode fazer o que quiser, embora seja envolto em um handler de exceção.
A assinatura de nosso get torna-se
1 get' :: DB -> Key -> (Result -> IO a) -> IO a
Aqui está uma implementação de pseudocódigo:
01 import Prelude hiding (concat, length, lookup) 02 import Data.Binary 03 import Data.ByteString.Char8 04 import qualified Data.ByteString.Lazy as BL 05 import Data.Int (Int32) 06 import Control.Exception (bracket, finally) 07 import Foreign.Marshal.Alloc 08 import Foreign.Marshal.Array 09 import Network.Socket (Socket) 10 import Network.Socket.ByteString 11 import Network.Socket.SendFile.Handle 12 import System.Posix (COff, Fd, fdToHandle) 13 import System.Posix.IO.Extra (pread) 14 15 type Length = Int32 16 17 type Key = ByteString 18 type Value = ByteString 19 20 data DB = DB 21 22 data Result = NotFound 23 | Value Value 24 | Location Fd COff Length 25 26 -- Lookup a value in the database 27 lookup :: DB -> Key -> IO Result 28 lookup d k = undefined 29 30 -- Take a reference to the database 31 -- This is dummy, the actual API is more complex 32 ref :: DB -> IO () 33 ref d = undefined 34 35 -- Return a reference to the database 36 unref :: DB -> IO () 37 unref d = undefined 38 39 get' :: DB -> Key -> (Result -> IO a) -> IO a 40 get' d k h = do 41 v <- lookup d k 42 43 let (pre, post) = case v of 44 Location _ _ _ -> (ref, unref) 45 otherwise -> (\_ -> return (), \_ -> return ()) 46 47 pre d 48 h v `finally` post d
Observe que um Result pode ser NotFound, um valor real (no nosso caso, isso pode acontecer se o valor é compactado ou criptografado no disco), ou um ponteiro para o valor em um determinado arquivo.
Implementar o get clássico se torna algo trivial:
01 get :: DB -> Key -> IO (Maybe Value) 02 get d k = get' d k h 03 where 04 h :: Result -> IO (Maybe Value) 05 h NotFound = return Nothing 06 h (Value v) = return $ Just v 07 h (Location f o l) = 08 Just `fmap` bracket 09 (mallocArray l') 10 free 11 -- TODO This assumes pread(2) always returns the requested number 12 -- of bytes 13 (\s -> pread f s l' o >> packCStringLen (s, l')) 14 where 15 l' :: Int 16 l' = fromIntegral l
A implementação de uma chamada get que envia um valor em um socket, prefixado com o tamanho do valor, se torna simples:
01 sendOnSocket :: DB -> Key -> Socket -> IO () 02 sendOnSocket d k s = get' d k h 03 where 04 h :: Result -> IO () 05 h NotFound = sendMany s zero 06 h (Value v) = do 07 sendMany s $ BL.toChunks $ encode $ (fromIntegral $ length v :: Length) 08 sendAll s v 09 h (Location f o l) = do 10 sendMany s $ BL.toChunks $ encode l 11 f' <- fdToHandle f 12 sendFile' s f' (fromIntegral o) (fromIntegral l) 13 14 zero = BL.toChunks $ encode (0 :: Int32)
Em vez de sendFile’, algumas ligações FFI para splice ou algo semelhante podem também ser bem usadas.
Na atual pseudoimplementação, um descritor de arquivo ainda pode ser vazado (um aplicativo pode chamar get’ com uma ação que armazena o Fd dado em alguns IORef), e as ações dadas podem alterar o ponteiro do arquivo usando lseek(2) e outros. Poderíamos resolver esse problema, se necessário, duplicando o descritor de arquivo usando dup(2) e passando a cópia para o aplicativo, em seguida, fechando a duplicação após a conclusão (então não há nenhum uso para o aplicativo armazenar o descritor: ele vai se tornar inválido de qualquer maneira).
***
Texto original disponível em http://blog.incubaid.com/2011/09/30/rethinking-an-embedded-database-api/