Como usar genéricos em ferrugem, resumo de objetos de características e servidor kv (3)

Pode-se dizer que no desenvolvimento Rust, a programação genérica é uma habilidade que devemos dominar. Ao construir cada estrutura de dados ou função, é melhor se perguntar: **Preciso corrigir o tipo neste momento? **É possível adiar esta decisão para o mais tarde possível, de modo a deixar espaço para o futuro? Se pudermos adiar decisões através de genéricos, a arquitetura do sistema poderá ser flexível o suficiente para enfrentar melhor as mudanças futuras.

Aprendemos anteriormente que as características podem atingir polimorfismo paramétrico, o que significa que funções ou estruturas de dados são representadas por T, não por tipos específicos; elas
também podem implementar polimorfismo ad hoc, ou seja, sobrecarga de função. Diferentes parâmetros de uma interface de função têm diferentes implementações;
ainda mais incrível O legal é que quando traits são usados ​​como parâmetros, restrições de feature e múltiplas restrições podem ser implementadas. Isso reflete a ideia de que combinação é maior que herança. Em C++, é herança múltipla. Um exemplo de servidor KV é usar a característica que implementa store como um parâmetro genérico para implementar ligação atrasada. A mesma estrutura de dados tem implementações diferentes da mesma característica. Por exemplo, se um novo tipo de armazenamento for adicionado a kv no futuro, o novo tipo de armazenamento será adicionado. Ao implementar características, a implementação é diferente. Em C++, a classe base é usada como parâmetro, o que equivale a atribuir a subclasse à classe base. No entanto, isso é polimorfismo de tempo de execução e tem o sobrecarga de uma tabela de funções virtuais em tempo de execução, enquanto genéricos É distribuído estaticamente, rodando rápido, mas compilando lentamente. Os

parâmetros são características genéricas.
Três cenários de uso de parâmetros genéricos
. Use parâmetros genéricos para atrasar a ligação de estruturas de dados;
use parâmetros genéricos e PhantomData para declarar que não há estruturas de dados na estrutura de dados. Tipos que são usados ​​diretamente, mas precisam ser usados ​​durante a implementação;
o uso de parâmetros genéricos permite que a mesma estrutura de dados tenha diferentes implementações da mesma característica.

Usando parâmetros genéricos para ligação atrasada
Vejamos primeiro algo com o qual já estamos familiarizados, o uso de parâmetros genéricos para ligação atrasada. Armazenar no servidor kv é um parâmetro genérico, que pode ser gradualmente restringido em implementações subsequentes. Desde que a característica de armazenamento seja implementada, ela pode ser usada como parâmetro.

pub fn dispatch(cmd: CommandRequest, store: &impl Storage) -> CommandResponse { … }
// 等价于
pub fn dispatch<Store: Storage>(cmd: CommandRequest, store: &Store) -> CommandResponse { … }

Fornecendo tipos adicionais usando parâmetros genéricos e dados fantasmas
Agora vamos projetar uma estrutura de dados de Usuário e Produto, ambos com um ID do tipo u64. Porém, espero que o id de cada estrutura de dados só possa ser comparado com ids do mesmo tipo, ou seja, se user.id e product.id forem comparados, o compilador poderá reportar diretamente um erro e rejeitar esse comportamento.
Primeiro, use um identificador de estrutura de dados personalizado para representar o ID:
pub struct Identifier { inner: u64,}
Em seguida, em Usuário e Produto, cada um usa o Identificador para vincular o identificador ao seu próprio tipo, para que diferentes tipos de IDs não possam ser comparados.

#[derive(Debug, Default, PartialEq, Eq)]
pub struct User {
    
     id: Identifier<Self>}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Product {
    
     id:Identifier<Self>}

No entanto, ele não consegue compilar. por que? Como o Identificador não usa o parâmetro genérico T quando é definido, o compilador considera T redundante, portanto só pode ser compilado excluindo T. Porém, se T for excluído, os IDs do usuário e do produto poderão ser comparados e não conseguiremos alcançar a função desejada. O que devemos fazer?
PhantomData é geralmente traduzido como dados fantasmas em chinês. Esse nome tem um encanto maligno que deixa as pessoas com medo de se aproximar dele, mas é amplamente utilizado no processamento. Parâmetros genéricos que não são necessários no processo de definição da estrutura de dados, mas são necessários no o processo de implementação.

#[derive(Debug, Default, PartialEq, Eq)]
pub struct Identifier 
{
    
     inner: u64,
 _tag: PhantomData,}

É isso. Na verdade, PhantomData é exatamente como seu nome. Na verdade, ele tem comprimento zero e é um ZST (Zero-Sized Type). É como se não existisse. Sua única função é marcar o tipo.
Por exemplo, a estrutura do usuário possui nome, id e um parâmetro genérico T pertencente a dados fantasmas, representando usuários gratuitos ou usuários pagos. Usuários gratuitos podem se tornar usuários pagos por meio do método de assinatura e entrar nele. Usar o PhantomData para lidar com esse status pode detectar o status durante o período de compilação, evitando o fardo da detecção em tempo de execução e possíveis erros.
Os genéricos são distribuídos estaticamente e os códigos de tipo correspondentes são atribuídos durante a compilação.

Use parâmetros genéricos para fornecer múltiplas implementações
Às vezes, para a mesma característica, queremos ter implementações diferentes, o que devemos fazer? Por exemplo, uma equação pode ser uma equação linear ou uma equação quadrática.Esperamos implementar diferentes Iteradores para diferentes tipos.

#[derive(Debug, Default)]
pub struct Equation 
{
    
     current: u32, 
_method: PhantomData,
}// 线性增长
#[derive(Debug, Default)]
pub struct Linear;// 二次增长
#[derive(Debug, Default)]
pub struct Quadratic;
impl Iterator for Equation
 {
    
     type Item = u32;
  fn next(&mut self) -> Option {
    
     self.current += 1; if self.current >= u32::MAX {
    
     return None; } Some(self.current) }}
  impl Iterator for Equation
   {
    
    
   type Item = u32; 
   fn next(&mut self) -> Option {
    
     self.current += 1; if self.current >= u16::MAX as u32 {
    
     return None; } Some(self.current * self.current) }}

Há algum benefício em fazer isso? Por que não construir duas estruturas de dados LinearEquation e QuadraticEquation e implementar o Iterator respectivamente?
Na verdade, neste exemplo, usar genéricos não faz muito sentido porque a Equação em si não compartilha muito código. Mas se a Equação for diferente, exceto pela lógica de implementação do Iterador, um grande número de outros códigos são iguais e, no futuro, além de equações lineares e equações quadráticas, também suportará cúbico, quadrático..., então use uma estrutura de dados genérica para É muito necessário unificar a mesma lógica e usar tipos específicos de parâmetros genéricos para lidar com mudanças na lógica.
Semelhante ao significado de herança em C++, Equation é a classe base e possui o iterador de método.LinearEquation e QuadraticEquation são subclasses que implementam especificamente o iterador.

Situação extra:
O que devo fazer se o valor de retorno contiver parâmetros genéricos?
Por exemplo, para o método get_iter(), não nos importamos que tipo de Iterador é o valor de retorno, desde que nos permita chamar continuamente o método next() e obter uma estrutura Kvpair.
Rust atualmente não suporta o uso da característica impl como um valor de retorno em uma característica. O que fazer? É muito simples, podemos retornar um objeto trait, que elimina diferenças de tipo e unifica todos os diferentes tipos que implementam Iterator sob o mesmo objeto trait

pub trait Storage {
    
    
    ...
    /// 遍历 HashTable,返回 kv pair 的 Iterator
    fn get_iter(&self, table: &str) -> 
        Result<Box<dyn Iterator<Item = Kvpair>>, KvError>;
}

É claro que a programação genérica também é uma faca de dois gumes. Sempre que introduzirmos a abstração, mesmo que consigamos obter uma abstração de custo zero, lembre-se de que a abstração em si é um custo . Quando abstraímos código em funções e estruturas de dados em estruturas genéricas, mesmo que quase não haja custos adicionais em tempo de execução, isso ainda trará custos de tempo de design . Se a abstração não for bem feita, trará custos ainda maiores. Custos de manutenção

总结一下trait:
1、特征约束。比如store trait作为参数,下次添加新的类型根本不需要修改代码,只需要在为新的存储类型添加新的方法,开闭原则;并且如果C++必须要用多态运行时,有开销,而且组合优于继承(破坏封装,方法污染,耦合强,多重继承的问题)

2、还可以有幽灵数据,数据结构定义过程中不需要,但是在实现过程中需要的泛型参数。用泛型数据结构来统一相同的逻辑,用泛型参数的具体类型来处理变化的逻辑,就非常有必要了。也是用来替代C++的继承的;

3、特殊情况就是返回值不能是trait,比如返回一个迭代器,不需要知道它的具体类型,可以用特征对象,根C++的多态很像了。最好不用吧。

Como o objeto trait é usado no combate real
Insira a descrição da imagem aqui
Este exemplo mostra que o objeto trait usa o trait &dyn para representar o tipo real como um tipo de nível superior que implementa o trait.Semelhante ao C++, existe uma tabela, que contém a implementação do trait característica pelo tipo específico. É equivalente ao polimorfismo, que degenera a subclasse em uma classe base, mas a classe base possui uma tabela de funções virtuais que armazena a implementação da característica do tipo real.
A questão aqui é que, como mencionado anteriormente, o polimorfismo ad hoc pode ser alcançado usando características como parâmetros, o que é semelhante à passagem de parâmetros genéricos, mas a implementação é diferente de acordo com os diferentes tipos reais. Por que precisamos de objetos característicos?
1. Principalmente pela conveniência de programação, boa lógica e legibilidade de código. Por exemplo, se quisermos implementar um componente UI com diferentes elementos (botões, caixas de texto, etc., esses componentes possuem métodos draw). Todos eles existem em uma tabela vec e precisam ser renderizados na tela um por um usando o mesmo método! Se uma restrição de recurso genérica for usada, a lista deverá ser toda do mesmo tipo. Para resolver todos os problemas acima, Rust introduz um conceito - objeto característico. Implementação de caixa. Qualquer tipo que implemente o recurso Draw pode ser armazenado em vec.
Os objetos trait podem tornar o código mais simples, pois não há necessidade de usar parâmetros genéricos ao implementá-lo.
2. A segunda é quando são necessários valores de retorno genéricos. Rust atualmente não suporta o uso da característica impl como um valor de retorno em uma característica. O que fazer? É simples, podemos retornar o objeto trait. Por exemplo, ao retornar um iterador, todos os diferentes tipos que implementam o Iterator são unificados no mesmo objeto de característica.

Resumindo:
quando o sistema precisa usar polimorfismo para resolver requisitos complexos e mutáveis, para que a mesma interface possa exibir comportamentos diferentes , temos que decidir se a distribuição estática em tempo de compilação (parâmetros genéricos) ou em tempo de execução é melhor. (objetos de característica).
A distribuição dinâmica terá sobrecarga de tempo de execução, mas tornará o código mais conciso, especialmente quando você precisar abstrair diferentes tipos específicos, como formar diferentes componentes em uma coleção de componentes de UI, você precisa de um objeto característico, porque as matrizes só podem conter elementos de do mesmo tipo. , tuplas podem ser consideradas? E quando o valor de retorno contém genéricos, um objeto trait deve ser usado, porque Rust atualmente não suporta o trait impl como valor de retorno.
A distribuição estática é uma abstração de custo zero, mas o custo do projeto existe. Se for simples, você pode usá-la. Se for muito complicado, envolver múltiplas características e a conexão for complicada, você não precisa considerá-la .

Dicas:
Sabemos que & dyn draw, Box e Arc podem ser usados ​​como objetos de recursos. Mas quanto mais sobrecarga a distribuição dinâmica tem em comparação com a distribuição estática?
Se for & dyn draw (ptr e vptr alocados na pilha), na verdade é apenas mais um acesso à memória da vtable, e o impacto não é grande. Box, Arc e Arc alocam mais uma memória heap, o que tem um grande impacto e faz com que a velocidade seja dezenas de vezes mais lenta.
Portanto, na maioria dos casos, não precisamos prestar muita atenção aos problemas de desempenho dos objetos trait ao escrever código. Se você realmente se preocupa com o desempenho dos objetos de características no caminho crítico, tente primeiro ver se consegue fazer isso sem fazer alocação extra de memória heap.
(Para valores de retorno (o objeto da pilha será destruído no final da função) e para transferência entre threads (a característica de envio deve ser implementada), Box, , Arc deve ser usado)

Como projetar e arquitetar um sistema em torno de características?
Na verdade, não apenas características em Rust, mas também conceitos relacionados ao processamento de interface em qualquer linguagem são os conceitos mais importantes no uso dessa linguagem. Todo o comportamento do desenvolvimento de software pode ser basicamente considerado o processo de criação e iteração contínua de interfaces e, em seguida, implementação nessas interfaces.

Construindo um servidor KV simples - habilidades avançadas de trait
Concluímos as funções básicas do armazenamento KV, mas deixamos duas pequenas caudas:
o método get_iter() do trait Storage não foi implementado;
ainda existem alguns TODOs no execute() método do Serviço. Notificação de eventos que precisam ser tratados.
Armazenamento persistente completo


impl Storage for MemTable {
    
    
    ...
    fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError> {
    
    
        // 使用 clone() 来获取 table 的 snapshot
        let table = self.get_or_create_table(table).clone();
        let iter = table
            .iter()
            .map(|v| Kvpair::new(v.key(), v.value().clone()));
        Ok(Box::new(iter)) // <-- 编译出错
    }
}

table.iter() usa uma referência à tabela e retornamos iter, mas iter se refere à tabela como uma variável local, portanto não pode ser compilado. Neste momento, precisamos de um iterador que possa ocupar totalmente a tabela. Podemos usar table.into_iter() para transferir a propriedade da tabela para o iter: let iter = table.into_iter().map(|data| data.into ()) ;
(String, Value) precisa ser convertido em Kvpair, ainda usamos into() para fazer isso.
Ainda precisamos pensar sobre isso, se quisermos implementar a característica Storage para mais armazenamentos de dados no futuro, como lidaremos com o método get_iter()?

Nós iremos:
obter um Iterador próprio em uma determinada tabela,
mapear o Iterador e
converter cada item mapeado em um Kvpair

A etapa 2 aqui é a mesma para todas as implementações do método get_iter() da característica de armazenamento. É possível encapsulá-lo, ou seja, implementar uma característica storeiterator que contenha a operação do mapa, de forma que sempre que outros tipos de armazenamento precisarem implementar get_iter, a segunda etapa possa ser omitida. Na verdade, no caso do servidor KV, tal abstração traz poucos benefícios. Porém, se as etapas agora não são 3 etapas, mas 5 etapas/10 etapas, e um grande número de etapas são iguais, ou seja, toda vez que implementamos uma nova loja, temos que escrever o mesmo código lógica, então, essa Abstração é muito necessária.

Notificação de evento de suporte


pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
    
    
    debug!("Got request: {:?}", cmd);
    // TODO: 发送 on_received 事件
    let res = dispatch(cmd, &self.inner.store);
    debug!("Executed response: {:?}", res);
    // TODO: 发送 on_executed 事件

    res
}

Para resolver esses TODOs, precisamos fornecer um mecanismo de notificação de eventos: ao criar um Serviço, registre a função de processamento de eventos correspondente; quando o método execute() for executado, faça a notificação de eventos correspondente para que a função de processamento de eventos registrada possa ser executado.
Vejamos primeiro como registrar a função do manipulador de eventos.
Se você deseja registrar-se, em outras palavras, a estrutura de dados Service/ServiceInner precisa ter um local para hospedar a função de registro de eventos. Você pode tentar adicioná-lo à estrutura ServiceInner:


/// Service 内部数据结构
pub struct ServiceInner<Store> {
    
    
    store: Store,
    on_received: Vec<fn(&CommandRequest)>,
    on_executed: Vec<fn(&CommandResponse)>,
    on_before_send: Vec<fn(&mut CommandResponse)>,
    on_after_send: Vec<fn()>,
}

Antes de escrever o código para registro de evento, é melhor escrever primeiro um teste e considerar como registrar-se da perspectiva do usuário.


#[test]
fn event_registration_should_work() {
    
    
    fn b(cmd: &CommandRequest) {
    
    
        info!("Got {:?}", cmd);
    }
    fn c(res: &CommandResponse) {
    
    
        info!("{:?}", res);
    }
    fn d(res: &mut CommandResponse) {
    
    
        res.status = StatusCode::CREATED.as_u16() as _;
    }
    fn e() {
    
    
        info!("Data is sent");
    }

    let service: Service = ServiceInner::new(MemTable::default())
        .fn_received(|_: &CommandRequest| {
    
    })
        .fn_received(b)
        .fn_executed(c)
        .fn_before_send(d)
        .fn_after_send(e)
        .into();

    let res = service.execute(CommandRequest::new_hset("t1", "k1", "v1".into()));
    assert_eq!(res.status, StatusCode::CREATED.as_u16() as _);
    assert_eq!(res.message, "");
    assert_eq!(res.values, vec![Value::default()]);
}

Como pode ser visto no código de teste, esperamos chamar continuamente o método fn_xxx através da estrutura ServiceInner para registrar a função de processamento de eventos correspondente para ServiceInner; após a adição ser concluída, convertemos ServiceInner em Service através do método into(). Este é um Builder Pattern clássico, que pode ser visto em muitos códigos Rust. (Para construir um objeto complexo, você só precisa passar os parâmetros para a classe de construção e não precisa se preocupar em como construí-lo. Usando private para modificar o método de construção, o mundo externo não pode criar o objeto diretamente , e só pode usar o método build da classe Builder interna; o processo de construção do objeto fica oculto. ; Métodos expostos a chamadas externas e usados ​​para construir componentes.)


impl<Store: Storage> ServiceInner<Store> {
    
    
    pub fn new(store: Store) -> Self {
    
    
        Self {
    
    
            store,
            on_received: Vec::new(),
            on_executed: Vec::new(),
            on_before_send: Vec::new(),
            on_after_send: Vec::new(),
        }
    }

    pub fn fn_received(mut self, f: fn(&CommandRequest)) -> Self {
    
    
        self.on_received.push(f);
        self
    }

    pub fn fn_executed(mut self, f: fn(&CommandResponse)) -> Self {
    
    
        self.on_executed.push(f);
        self
    }

    pub fn fn_before_send(mut self, f: fn(&mut CommandResponse)) -> Self {
    
    
        self.on_before_send.push(f);
        self
    }

    pub fn fn_after_send(mut self, f: fn()) -> Self {
    
    
        self.on_after_send.push(f);
        self
    }
}

Embora tenhamos concluído o registro da função manipuladora de eventos, ainda não enviamos a notificação do evento. Além disso, como nossos eventos incluem eventos imutáveis ​​(como on_received) e eventos variáveis ​​(como on_before_send), a notificação de eventos precisa separar os dois. Para definir duas características: Notify e NotifyMut:

/// 事件通知(不可变事件)
pub trait Notify<Arg> {
    
    
    fn notify(&self, arg: &Arg);
}

/// 事件通知(可变事件)
pub trait NotifyMut<Arg> {
    
    
    fn notify(&self, arg: &mut Arg);
}



impl<Arg> Notify<Arg> for Vec<fn(&Arg)> {
    
    
    #[inline]
    fn notify(&self, arg: &Arg) {
    
    
        for f in self {
    
    
            f(arg)
        }
    }
}

impl<Arg> NotifyMut<Arg> for Vec<fn(&mut Arg)> {
    
    
  #[inline]
    fn notify(&self, arg: &mut Arg) {
    
    
        for f in self {
    
    
            f(arg)
        }
    }
}

O parâmetro Arg corresponde ao argumento da função de registro de eventos, como: fn(&CommandRequest);

Depois que o trait Notify/NotifyMut for implementado, podemos modificar o método execute():


impl<Store: Storage> Service<Store> {
    
    
    pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
    
    
        debug!("Got request: {:?}", cmd);
        self.inner.on_received.notify(&cmd);
        let mut res = dispatch(cmd, &self.inner.store);
        debug!("Executed response: {:?}", res);
        self.inner.on_executed.notify(&res);
        self.inner.on_before_send.notify(&mut res);
        if !self.inner.on_before_send.is_empty() {
    
    
            debug!("Modified response: {:?}", res);
        }

        res
    }
}

Agora, o evento correspondente pode ser notificado à função manipuladora correspondente. Este mecanismo de notificação é atualmente uma chamada de função síncrona.Se necessário no futuro, podemos alterá-lo para passagem de mensagens para processamento assíncrono.
Somente que todo o processo consiste em adicionar um array para responder ao evento na estrutura do serviço. O elemento do array é a função a ser executada. Por ser um array, você pode usar o modo construtor ao construir o serviço e chamar continuamente o Método fn_xxx. Este método adiciona a função a ser executada. A função adiciona o evento de resposta correspondente ao array. Em seguida, execute notify em algum lugar, execute a função no array e notifique.
Observe que a razão pela qual um array é usado para um determinado ponto de gatilho aqui também é para atender ao princípio de abertura e fechamento, pois, por exemplo, após receber uma solicitação, as coisas a serem feitas ou notificações mudam com a demanda, podendo aumentar ou diminuir. Se utiliza apenas a forma de uma função e executa diretamente a implementação correspondente em fn_xxx. Ao enfrentar alterações, o código deve ser modificado, o que não atende ao princípio de abertura e fechamento.

Agora pense nos locais onde todo o nosso projeto satisfaz o princípio de abertura e fechamento:
1. As características genéricas são usadas para obter efeitos polimórficos semelhantes aos do C++. Não há necessidade de modificar o código para novos tipos de armazenamento no futuro, desde que conforme as características são implementadas para os novos tipos. É isso. E genéricos são distribuição estática, abstração de custo zero;
2. Para o método get_iter, o parâmetro de retorno usa um objeto característico e não se preocupa com o tipo específico, desde que satisfaça a implementação do traço do iterador. Ele também alcança um efeito semelhante ao polimorfismo, que é a distribuição dinâmica. ; Além disso, quando analisamos diferentes tipos de iteradores, o segundo passo é mapear o Iterador, então encapsulamos esta etapa e implementamos o iterador de armazenamento, para que quando houver um novo tipo de
armazenamento , não há necessidade de repetir a segunda etapa, especialmente É mais significativo quando esta operação comum tem muitas etapas.
3. O mecanismo de notificação de eventos aqui não é uma função única que manipula eventos. Como o processo de processamento pode mudar, uma matriz de processamento é configurada. Durante a construção, você pode usar o modo construtor para chamar fn_xxx continuamente para adicionar funções à matriz de processamento Quando a notificação for necessária, notifique as funções Executar em uma matriz sequencialmente.
4. O código de teste para testar a parte da loja também está em conformidade com o princípio de abertura e fechamento. A interface é estável. Características genéricas são usadas como parâmetros de interface. Quando há novos testes de tipo de armazenamento, não há necessidade de modificar o código.

其他的可以不说,主要说一下事件通知机制(**构造者模式**):不是单独一个处理事件的函数,因为处理过程可能变化,所以设置了处理数组,构造时可以利用构造者模式不断调用fn_xxx往处理数组中添加函数,需要通知的时候notify顺序执行数组中的函数。

Resumo: Na verdade, há dois pontos a serem entendidos aqui: um é o processo geral de três partes e a série de serviços e, em seguida, o uso de características e a implementação do padrão de construtor do mecanismo de notificação de eventos; (entre em contato com o padrão de design)

Em seguida, você também precisa conhecer a implementação da parte de processamento de rede e da parte de segurança da rede; bem como a implementação da programação assíncrona.

Implementando a característica Storage para um banco de dados persistente
Até agora, nosso armazenamento KV tem sido um armazenamento KV na memória. Assim que o aplicativo for encerrado, todas as chaves/valores armazenados pelo usuário desaparecerão. Queremos que o armazenamento seja durável.
Uma solução é adicionar suporte WAL e instantâneo de disco ao MemTable, para que todos os comandos de atualização enviados pelos usuários sejam armazenados no disco em ordem, e os instantâneos sejam tirados regularmente para facilitar a recuperação rápida de dados; outra solução é usar o armazenamento KV
existente , como RocksDB ou trenó. RocksDB é um armazenamento KV incorporado desenvolvido pelo Facebook com base no levelDB do Google, escrito em C++, e sled é um excelente armazenamento KV emergente na comunidade Rust, comparando com RocksDB. As funções dos dois são muito semelhantes. Do ponto de vista da demonstração, o sled é mais simples de usar e mais adequado para o conteúdo de hoje. Se usado em um ambiente de produção, o RocksDB é mais adequado porque foi temperado em vários ambientes de produção complexos. Portanto, hoje tentaremos implementar o traço Storage para sled para que ele possa se adaptar ao nosso servidor KV.
Primeiro introduza o sled em Cargo.toml: sled = "0.34" # sled db
Em seguida, crie src/storage/sleddb.rs e adicione o seguinte código:


use sled::{
    
    Db, IVec};
use std::{
    
    convert::TryInto, path::Path, str};

use crate::{
    
    KvError, Kvpair, Storage, StorageIter, Value};

#[derive(Debug)]
pub struct SledDb(Db);

impl SledDb {
    
    
    pub fn new(path: impl AsRef<Path>) -> Self {
    
    
        Self(sled::open(path).unwrap())
    }

    // 在 sleddb 里,因为它可以 scan_prefix,我们用 prefix
    // 来模拟一个 table。当然,还可以用其它方案。
    fn get_full_key(table: &str, key: &str) -> String {
    
    
        format!("{}:{}", table, key)
    }

    // 遍历 table 的 key 时,我们直接把 prefix: 当成 table
    fn get_table_prefix(table: &str) -> String {
    
    
        format!("{}:", table)
    }
}

/// 把 Option<Result<T, E>> flip 成 Result<Option<T>, E>
/// 从这个函数里,你可以看到函数式编程的优雅
fn flip<T, E>(x: Option<Result<T, E>>) -> Result<Option<T>, E> {
    
    
    x.map_or(Ok(None), |v| v.map(Some))
}

impl Storage for SledDb {
    
    
    fn get(&self, table: &str, key: &str) -> Result<Option<Value>, KvError> {
    
    
        let name = SledDb::get_full_key(table, key);
        let result = self.0.get(name.as_bytes())?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn set(&self, table: &str, key: String, value: Value) -> Result<Option<Value>, KvError> {
    
    
        let name = SledDb::get_full_key(table, &key);
        let data: Vec<u8> = value.try_into()?;

        let result = self.0.insert(name, data)?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn contains(&self, table: &str, key: &str) -> Result<bool, KvError> {
    
    
        let name = SledDb::get_full_key(table, &key);

        Ok(self.0.contains_key(name)?)
    }

    fn del(&self, table: &str, key: &str) -> Result<Option<Value>, KvError> {
    
    
        let name = SledDb::get_full_key(table, &key);

        let result = self.0.remove(name)?.map(|v| v.as_ref().try_into());
        flip(result)
    }

    fn get_all(&self, table: &str) -> Result<Vec<Kvpair>, KvError> {
    
    
        let prefix = SledDb::get_table_prefix(table);
        let result = self.0.scan_prefix(prefix).map(|v| v.into()).collect();

        Ok(result)
    }

    fn get_iter(&self, table: &str) -> Result<Box<dyn Iterator<Item = Kvpair>>, KvError> {
    
    
        let prefix = SledDb::get_table_prefix(table);
        let iter = StorageIter::new(self.0.scan_prefix(prefix));
        Ok(Box::new(iter))
    }
}

impl From<Result<(IVec, IVec), sled::Error>> for Kvpair {
    
    
    fn from(v: Result<(IVec, IVec), sled::Error>) -> Self {
    
    
        match v {
    
    
            Ok((k, v)) => match v.as_ref().try_into() {
    
    
                Ok(v) => Kvpair::new(ivec_to_key(k.as_ref()), v),
                Err(_) => Kvpair::default(),
            },
            _ => Kvpair::default(),
        }
    }
}

fn ivec_to_key(ivec: &[u8]) -> &str {
    
    
    let s = str::from_utf8(ivec).unwrap();
    let mut iter = s.split(":");
    iter.next();
    iter.next().unwrap()
}

Este código implementa principalmente a característica Armazenamento. Cada método é muito simples, bastando adicionar um encapsulamento às funções fornecidas pelo sled.
O código de teste pode ser reutilizado. Isso também reflete o princípio de abertura e fechamento do código de teste. A interface de teste é estável e a implementação pode ser alterada.


mod sleddb;

pub use sleddb::SledDb;

#[cfg(test)]
mod tests {
    
    
    use tempfile::tempdir;

    use super::*;

    ...

    #[test]
    fn sleddb_basic_interface_should_work() {
    
    
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_basi_interface(store);
    }

    #[test]
    fn sleddb_get_all_should_work() {
    
    
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_get_all(store);
    }

    #[test]
    fn sleddb_iter_should_work() {
    
    
        let dir = tempdir().unwrap();
        let store = SledDb::new(dir);
        test_get_iter(store);
    }
}

No código de teste geral que foi realmente executado no final, você pode ver que a função principal permaneceu quase inalterada.O tipo de armazenamento específico foi modificado durante a construção do serviço, e o modo construtor foi usado para chamar continuamente o método fn_xxx para push a função notificada na matriz de eventos. A função de notificação será chamada durante a execução.
Além disso, se você quiser adicionar um evento de notificação, você só precisa chamar a função fn_xxx mais uma vez durante a construção, sem modificar o código; se quiser adicionar um novo tipo de armazenamento, você também pode implementar a característica store para ele, sem modificar o código, execute chamará automaticamente as funções correspondentes para manipular. (Envolvendo serialização e desserialização de arquivo protobuf, a função de execução do comando correspondente é a lógica de armazenamento e leitura)


use anyhow::Result;
use async_prost::AsyncProstStream;
use futures::prelude::*;
use kv1::{
    
    CommandRequest, CommandResponse, Service, ServiceInner, SledDb};
use tokio::net::TcpListener;
use tracing::info;

#[tokio::main]
async fn main() -> Result<()> {
    
    
    tracing_subscriber::fmt::init();
    let service: Service<SledDb> = ServiceInner::new(SledDb::new("/tmp/kvserver"))
        .fn_before_send(|res| match res.message.as_ref() {
    
    
            "" => res.message = "altered. Original message is empty.".into(),
            s => res.message = format!("altered: {}", s),
        })
        .into();
    let addr = "127.0.0.1:9527";
    let listener = TcpListener::bind(addr).await?;
    info!("Start listening on {}", addr);
    loop {
    
    
        let (stream, addr) = listener.accept().await?;
        info!("Client {:?} connected", addr);
        let svc = service.clone();
        tokio::spawn(async move {
    
    
            let mut stream =
                AsyncProstStream::<_, CommandRequest, CommandResponse, _>::from(stream).for_async();
            while let Some(Ok(cmd)) = stream.next().await {
    
    
                info!("Got a new command: {:?}", cmd);
                let res = svc.execute(cmd);
                stream.send(res).await.unwrap();
            }
            info!("Client {:?} disconnected", addr);
        });
    }
}

Acho que você gosta

Origin blog.csdn.net/weixin_53344209/article/details/130099981
Recomendado
Clasificación