Skip to content

Snafu version of virtual error stack implemented based on GreptimeDB code

License

Notifications You must be signed in to change notification settings

Oatelauser/snafu-stack-error

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

错误堆栈最佳实践

目录

范式规则

错误堆栈的最佳实践范式规则如下

  1. 每一个错误都必须定义#[snafu(display)]的错误信息
  2. 每一个内部错误必须实现ErrorExt,用于判断哪些错误对外展示什么信息
  3. 一个模块内部使用一个枚举类定义错误,达到内部统一的错误类型
  4. 模块内部对接外部错误(非当前模块的错误),必须定义错误传递者代理接受外部错误
  5. 模块之间的对接,也遵循规则3,需要定义错误传递者接受其他模块的错误,这个错误的命名一般是以模块名命名

范式定义

GreptimeDB项目代码为例,它的错误范式定义

#[derive(Snafu)]
#[snafu(visibility(pub))]
#[stack_trace_debug]
pub enum Error {

    #[snafu(display("Invalid utf-8 value"))]
    InvalidUtf8Value {
        #[snafu(source)]
        error: FromUtf8Error,     // 关联外部错误 
        location: Location,
    },

    #[snafu(display("Error accessing catalog"))]
    CatalogError {
        source: catalog::error::Error,   // 关联内部错误 
        #[snafu(implicit)]
        location: Location,
    },
    
}

impl ErrorExt for Error {
    fn status_code(&self) -> StatusCode {
        use Error::*;
        match self {
            InvalidUtf8Value { .. } | InvalidFlushArgument { .. } => StatusCode::InvalidArguments,
            CatalogError { .. } => StatusCode::Internal,
            UnsupportedDataType { .. } => StatusCode::Unsupported,
            CollectRecordbatch { .. } => StatusCode::EngineExecuteQuery,
        }
    }
    
    fn as_any(&self) -> &dyn Any {
        self
    }
}

pub type Result<T> = std::result::Result<T, Error>;   // 定义当前错误的 Result 类,方便代码的使用
  1. #[derive(Snafu)]:使用snafu过程宏定义错误对象
  2. stack_trace_debug:自动实现StackError和Display特征
  3. #[snafu(display)]:每个枚举值必须添加异常信息,且不能为空字符串
  4. 内部错误:定义的错误对象使用source字段关联内部错误
  5. 外部错误:定义的错误对象使用error字段关联外部错误,且必须标识#[snafu(source)],因为snafu不识别error字段
  6. Location:对于代码位置Location的定义,不推荐使用#[snafu(implicit)],而是显示调用location!
  7. 内部错误需要实现ErrorExt特征
  8. 每个error文件都会定义自己的Result类型

错误的使用

GreptimeDB对于错误的处理为例,该项目存在多个模块,例如auth模块和servers模块的交互为例

  1. auth模块提供check_permission方法暴露它自身的Error错误
    // error.rs
    #[derive(Snafu)]
    #[snafu(visibility(pub))]
    #[stack_trace_debug]
    pub enum Error {     // auth模块定义的错误 
        #[snafu(display("User is not authorized to perform this action"))]
        PermissionDenied {
            #[snafu(implicit)]
            location: Location,
        },
        ... ...
    }
    
    
    // permission.rs
    impl PermissionChecker for Option<&PermissionCheckerRef> {
        fn check_permission(
            &self,
            user_info: Option<UserInfoRef>,
            req: PermissionReq,
        ) -> Result<PermissionResp> {
            match self {
                Some(checker) => match checker.check_permission(user_info, req) {
                    Ok(PermissionResp::Reject) => PermissionDeniedSnafu.fail(),   // 如果校验被拒绝,则直接抛出 PermissionDenied 内部错误 
                    Ok(PermissionResp::Allow) => Ok(PermissionResp::Allow),
                    Err(e) => Err(e),  // 如果是校验方法出现错误,则直接抛出
                },
                None => Ok(PermissionResp::Allow),
            }
        }
    }
  2. servers模块提供metrics方法,内部调用auth::check_permission并做了错误转换
    #[derive(Snafu)]
    #[snafu(visibility(pub))]
    #[stack_trace_debug]
    pub enum Error {   // servers模块定义的错误 
    
        #[snafu(display("Failed to get user info"))]
        Auth {
            #[snafu(implicit)]
            location: Location,
            source: auth::error::Error,
        },
        
        #[snafu(display("Execute gRPC query error"))]
        ExecuteGrpcQuery {
            #[snafu(implicit)]
            location: Location,
            source: BoxedError,
        },
        ... ...
    }
    
    #[async_trait]
    impl OpenTelemetryProtocolHandler for Instance {
    
        #[tracing::instrument(skip_all)]
        async fn metrics(
            &self,
            request: ExportMetricsServiceRequest,
            ctx: QueryContextRef,
        ) -> ServerResult<Output> {
            self.plugins
                .get::<PermissionCheckerRef>()
                .as_ref()
                .check_permission(ctx.current_user(), PermissionReq::Otlp)   // 调用 auth::check_permission 方法 
                .context(AuthSnafu)?;   // 将auth模块的错误转换为当前模块的错误,也就是错误传递者 
    
            let interceptor_ref = self
                .plugins
                .get::<OpenTelemetryProtocolInterceptorRef<servers::error::Error>>();
            interceptor_ref.pre_execute(ctx.clone())?;   // 可能产生当前模块的错误
    
            let (requests, rows) = otlp::metrics::to_grpc_insert_requests(request)?;     // 可能产生当前模块的错误
            OTLP_METRICS_ROWS.inc_by(rows as u64);
    
            self.handle_row_inserts(requests, ctx)
                .await
                .map_err(BoxedError::new)
                .context(error::ExecuteGrpcQuerySnafu)     // frontend模块的错误转换为当前模块的错误 
        }
    
    }
  3. 最外层的代码处理这些范式错误,根据业务需求,使用实现的ErrorExt、StackError特征处理
    fn test() -> Result<()> {
        let path = "config.toml";
        // let r = fs::read(path).context(IOSnafu { location: location!() })
        //     .context(InvalidSnafu);
        let error: TestError = InvalidDatabaseOptionSnafu { key: String::from("Alice"), location: location!() }.build();
        // let a = error.last();
        let a = <TestError as StackError>::last(&error);
        println!("{}", a);
        Ok(())
        // ensure!(false, InvalidDatabaseOptionSnafu { key: String::from("Alice"), location: location!()});
    }

About

Snafu version of virtual error stack implemented based on GreptimeDB code

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages