napp-macro 2.0.0

NASA application entry for managed multi-source lifecycle and dependency-ordered initializers
Documentation

napp-macro

#[nasa::application(...)] 与 #[nasa::initializer(...)] 属性宏的实现 crate。业务项目不直接依赖它, 经 nasa 门面的 application feature 使用。initializer 会在 migration 和出站依赖准备完成后、 入站能力开放前参与三轮全局初始化屏障;运行时语义见 napp 与 运维指南。

宏把业务的异步 main 改写为统一进程入口:

  • 生成静态 ApplicationSpec(组件声明顺序、编译期缺省应用名)并调用 napp::run,真实 main 返回 std::process::ExitCode。
  • 声明 "web" 时在 crate 根自动生成 mvc_router!(nasa::Application) 收集端,并把业务 crate 内 nominal 的路由项投影为运行时稳定的 RouteMeta;业务不得再手写 mvc_router!(会因 crate::__mvc 重复定义编译失败)。
  • 业务 main 变成启动 Hook:零参数或接收一个 Application,返回 anyhow::Result<()>;成功返回后资源封存,运行由 Runner 接管。
  • 完整 Initialization trait impl 可被登记为静态 initializer,与 Service 启动 Hook 动态登记项合并 为同一份冻结依赖计划。

编译期校验(与运行期同口径,先在宏上失败):

  • 组件白名单:log、nacos-config、telemetry、db、redis、cache、partition、saga、 kafka、outbox、redis-job、grpc、auth、web、ws、nacos-discovery、scheduling;未知或 重复组件拒绝。
  • 业务可按任意顺序书写;宏固定规范为 log -> nacos-config -> telemetry -> db -> redis -> cache -> partition -> saga -> kafka -> outbox -> redis-job -> grpc -> auth -> web -> ws -> nacos-discovery -> scheduling。
  • saga 隐式加入 DB 与 Outbox;独立 outbox 隐式加入 DB。Inbox 是事务内原语,没有组件字符串; redis-job 隐式加入 Redis;Kafka 或其它 transport 不由 Saga 推断。
  • 隐式依赖只补齐缺项;显式同时声明 saga、db、outbox 与只声明 saga 生成同一组件图。
  • 组合约束:auth 必须和 web 同时声明;其余依赖关系由运行期根据最终配置继续校验。
  • 每个声明组件都会生成 feature 探测常量引用,能力未启用时在业务 crate 编译阶段直接失败。
  • 入口契约:必须是 crate 根的 async fn main,非泛型、至多一个 Application 参数、返回 anyhow::Result<()>;生成的类型门禁同时要求 Hook future 为 Send + 'static。入口不能再叠加 #[tokio::main] 或 #[EnableScheduling] / #[EnableAsync],因为 Application 已拥有 runtime 与调度 生命周期。
  • 生成 crate 根锚点模块:属性放错位置时错误直接指向宏调用处。
  • initializer 只能标注安全、正向、非泛型的完整 Initialization trait impl;固有 impl、单个方法、 unsafe、负向 impl 与其它 trait 都会在编译期拒绝。

宏内路径解析复用 macro-support(直接依赖优先、门面回退、Cargo 重命名兼容)。

受管单源与多源边界

宏只声明生命周期组件,不解析连接参数。MySQL、PostgreSQL、Redis 与 Kafka 的单源或多源配置由 napp 在启动期 从最终 YAML 创建并冻结;业务 Hook 只能取得受管句柄或提交 publisher、consumer、Handler 与 Saga 定义等业务计划,不能借宏属性建立第二张连接表。

资源 单源根 多源根 命名选择
MySQL database datasources.<name> Application getter、datasource_ref 与具名持久适配器
PostgreSQL database datasources.<name> pg_datasource getter、datasource_ref 与 PostgreSQL 持久适配器
Redis 扁平 redis redis.properties.<qualifier> Application getter 与 redis_ref
Kafka kafka kafkas.<client> Application getter、consumer/producer client name

同类资源的单源根与多源根互斥,引用未知名称会在 Ready 前拒绝,不会回退到默认或唯一实例。完整字段、 同源事务要求和停机边界见 napp 的单源与多源章节。

Saga 的 managed 角色与 datasource 由运行时读取最终配置,宏不推断默认库。可靠 client 的 append 和 dispatcher 使用同一 saga.client.datasource_ref;显式 outbox.datasource_ref 冲突在 Ready 前 拒绝。宏只生成组件图,不能绕过运行期同源门禁。

使用示例

[dependencies]
nasa = { version = "2.0.0", features = ["application", "log", "redis", "cache", "web"] }
#[nasa::application("web", "cache", "redis", "log")]
async fn main(_app: nasa::Application) -> anyhow::Result<()> {
    Ok(())
}

虽然源码按 web, cache, redis, log 书写,生成的规范启动顺序仍是 log -> redis -> cache -> web。

业务优雅停机任务

生成的启动 Hook 接收同一个 Application,可直接调用 app.register_graceful_shutdown(priority, name, future);不需要增加宏属性、组件字符串或 feature。 登记只在 Service 或 Batch 的 UserHook 开放,Hook 结束后名称和任务集合封口。

宏不执行停机 future,也不另外生成信号处理器。Runner 在受监督任务收口后、UserHook 业务资源 释放前执行任务;Service 的 initializer 先清理,Batch 的静态 initializer 则在业务资源之后清理。 数值较小的 priority 先执行,同优先级按登记顺序执行。任务返回 () 或 Result<(), E>,其中 E: Into<anyhow::Error>;错误、超时和可隔离展开只记为次要失败。 初始化失败时,已经成功登记的任务仍沿统一停机路径处理;预算耗尽的未开始项不再 poll。 直接取消 Runner 不等同于请求优雅停机,析构隔离也不保证执行异步收尾。 此时实例退出 Ready 并撤销新资源借用和本实例全局入口;经过任务门时仍存活的受监督 future 保留后续 清理所有权,最后一个 future 析构后才释放依赖。保留 Application 副本不会重新开放入口。

完整名称、数量、共享期限和资源所有权约束见 业务优雅停机任务。

#[nasa::initializer]

属性入口适合无需在启动 Hook 中手工构造的静态 initializer。省略 name 时,宏从实现类型名派生 canonical kebab-case;省略 order 时使用 100000。数值越小越先执行,但 requires 依赖边始终 优先;同一可执行集合再按名称稳定裁决。

#[derive(Default)]
struct SchemaInitialization;

#[nasa::initializer(name = "schema", order = 300)]
impl nasa::application::Initialization for SchemaInitialization {}

#[derive(Default)]
struct RoutesInitialization;

#[nasa::initializer(order = 200, requires = ["schema"])]
impl nasa::application::Initialization for RoutesInitialization {
    fn initialize<'a>(
        &'a mut self,
        _context: &'a mut nasa::application::InitializationContext<'_>,
    ) -> nasa::application::ApplicationFuture<'a> {
        Box::pin(async { Ok(()) })
    }
}

可选 factory = path 接收 Application 并异步返回 ApplicationResult<Option<T>>;None 表示当前配置 不启用该项,Err 阻止 Ready。kind = "one-shot" 只执行有界初始化;"hosted" 仅允许 Service, 可暂存 Ready 后才激活的长期任务和 readiness。Runner 在 Prepare 后严格执行全部 before、全部 initialize、全部 after,三轮成功后才进入 Seal 与 Ready。 Hosted 任务与组件终端、受管 Redis 消费和派生发送、出站 Client 共用启动许可;关键本地资源、 健康证据和启动期限复验通过后才发布 Ready。initializer 可保存已装配的发送句柄,统一放行前调用 会被拒绝;UserHook 中普通 spawn_background / spawn_critical 不隐式等待该许可。

派生名称也是依赖、日志和指标 label 使用的稳定身份;实现类型重命名会改变该身份,需要跨发布保持 连续性时应显式填写 name。initializer 失败、panic、启动超时或取消都会阻止入站能力开放,并进入 统一逆序清理;已经提交到外部系统的事实不会被本地清理撤销,业务实现必须保证可安全重跑。

YML 配置与边界

本宏不读取 yml;它只生成 ApplicationSpec。zcf/application.yml 和各组件配置由 napp 运行时读取。

  • 属性只能放在 crate 根异步 main 上。
  • 声明 "web" 后宏会生成唯一的路由收集模块,业务不能再手工生成同名收集器。
  • Hook 返回成功后资源登记入口封口,运行期不能继续修改组件图。
  • feature 缺失、重复组件、未知组件和非法组合都在编译期拒绝。
  • initializer 依赖缺失、重复名称、条件禁用后仍被依赖或依赖环由运行时在调用工厂前拒绝。