Dagger 函数结果缓存策略解析:TypeScript SDK 中 FunctionCachePolicy 枚举的三种取值与底层实现
【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger
本文是 Dagger v0.20 系列 TypeScript API 参考文档中FunctionCachePolicy枚举的深度解读。该枚举用于配置 Dagger 模块中函数(Function)返回结果的缓存行为,是模块作者控制"函数调用结果是否被缓存、缓存多久"的核心开关。读完本文,你将掌握Default、Never、PerSession三种策略的语义差异、与withCachePolicy及 TTL 参数的配合方式,以及它们在 Dagger 引擎缓存层面的实际作用原理。
枚举概述:什么是 FunctionCachePolicy
FunctionCachePolicy定义于 TypeScript SDK 的自动生成客户端中,描述的是函数结果缓存的配置行为("The behavior configured for function result caching")。它是 Dagger 模块 API 中Function_对象的一个配置维度,由模块运行时在注册函数时写入,并最终驱动引擎层决定调用结果的持久化方式。
该枚举的官方 TS 定义为 sdk/typescript/src/api/client.gen.ts:
/** * The behavior configured for function result caching. */ export enum FunctionCachePolicy { Default = "Default", Never = "Never", PerSession = "PerSession", }它只包含三个枚举成员,每个成员对应一个字符串值:"Default"、"Never"、"PerSession"。在 GraphQL Schema 层面对应FunctionCachePolicy枚举类型,其完整定义可见引擎基准 Schema core/schema/testdata/base_schema.graphqls 中Function.withCachePolicy字段的参数类型。
枚举成员详解
Default —— 默认缓存策略
Default:
"Default"
这是默认值,也是未显式设置缓存策略时函数所采用的策略。语义为:函数结果按照 Dagger 的默认规则进行缓存与持久化。
从源码看,当函数元数据中的CachePolicy为空时,会被统一归一到Default,见 core/typedef.go 的derivedCachePolicy:
func (fn *Function) derivedCachePolicy(mod *Module) FunctionCachePolicy { cachePolicy := fn.CachePolicy if cachePolicy == "" { cachePolicy = FunctionCachePolicyDefault } if cachePolicy == FunctionCachePolicyDefault && mod.DisableDefaultFunctionCaching { // older modules that explicitly disable the new default function caching should // fallback to the old caching behavior (per-session) cachePolicy = FunctionCachePolicyPerSession } return cachePolicy }值得注意的细节是:Default策略下,引擎为函数结果设置持久化属性IsPersistable = true,并写入 TTL——若函数显式指定了CacheTTLSeconds则使用该值,否则使用上限MaxFunctionCacheTTLSeconds(即 7 天,见 core/modfunc.go 的常量定义)作为"非常原始的剪枝手段"。对应代码在 core/typedef.go:
cachePolicy := fn.CachePolicy if cachePolicy == "" { cachePolicy = FunctionCachePolicyDefault } ... spec.IsPersistable = true switch cachePolicy { case FunctionCachePolicyNever: spec.IsPersistable = false case FunctionCachePolicyPerSession: spec.IsPersistable = false case FunctionCachePolicyDefault: if fn.CacheTTLSeconds.Valid { spec.TTL = fn.CacheTTLSeconds.Value.Int64() } else { // we still set a max TTL for now as a very primitive form of pruning spec.TTL = MaxFunctionCacheTTLSeconds } }同时,Default也是唯一允许搭配 TTL(timeToLive)的缓存策略。
Never —— 永不缓存
Never:
"Never"
该策略表示函数结果不被持久化缓存。每次调用都会重新执行函数体,适用于那些结果不应被复用(例如含随机性、实时性要求高、或产生副作用的函数)。
引擎侧的行为同样可见于上面的switch分支:FunctionCachePolicyNever使spec.IsPersistable = false,即该函数调用结果不会进入持久化缓存。在调用图层面,core/modfunc.go 的cacheImplicitInputs会为Never策略附加一个dagql.PerCallInput隐式输入:
var implicitInputs []dagql.ImplicitInput cachePolicy := fn.metadata.derivedCachePolicy(fn.mod.Self()) switch cachePolicy { case FunctionCachePolicyNever: implicitInputs = append(implicitInputs, dagql.PerCallInput) case FunctionCachePolicyPerSession: implicitInputs = append(implicitInputs, dagql.PerSessionInput) }PerCallInput意味着每次调用都被视为独立、不可共享缓存结果的节点,从而强制重算。
PerSession —— 按会话缓存
PerSession:
"PerSession"
该策略表示函数结果仅在当前 Dagger 会话(Session)内有效。同一会话中的后续调用可以命中缓存,但会话结束、新会话启动后缓存即失效,结果不会被跨会话持久化。
这与Never的区别在于:Never连同一次会话内的重复调用也不缓存,而PerSession至少在会话生命周期内可以复用结果。在引擎侧,PerSession同样使IsPersistable = false(不跨会话持久化),但在cacheImplicitInputs中附加的是dagql.PerSessionInput,即以会话为作用域进行缓存。
另外,PerSession也是 Dagger 旧版默认缓存行为。derivedCachePolicy中特意处理了"旧模块显式禁用默认函数缓存"的兼容场景:当模块设置了DisableDefaultFunctionCaching时,Default会回退为PerSession,保持老模块原有的按会话缓存语义不变(core/typedef.go)。
枚举的配套工具函数
SDK 生成代码还提供了两个配套的转换工具函数,用于在枚举值与字符串名之间互转,供模块运行时在调用 GraphQL API 时使用,见 sdk/typescript/src/api/client.gen.ts:
/** * Utility function to convert a FunctionCachePolicy value to its name so * it can be uses as argument to call a exposed function. */ export function FunctionCachePolicyValueToName( value: FunctionCachePolicy, ): string { switch (value) { case FunctionCachePolicy.Default: return "Default" case FunctionCachePolicy.Never: return "Never" case FunctionCachePolicy.PerSession: return "PerSession" default: return value } } /** * Utility function to convert a FunctionCachePolicy name to its value so * it can be properly used inside the module runtime. */ export function FunctionCachePolicyNameToValue( name: string, ): FunctionCachePolicy { switch (name) { case "Default": return FunctionCachePolicy.Default case "Never": return FunctionCachePolicy.Never case "PerSession": return FunctionCachePolicy.PerSession default: return name as FunctionCachePolicy } }FunctionCachePolicyValueToName:将枚举值转换为字符串名,作为调用暴露函数(exposed function)时的参数;FunctionCachePolicyNameToValue:将字符串名解析回枚举值,供模块运行时内部使用。
这两者实际上支撑了 TypeScript SDK 在将缓存策略参数编码进 GraphQL 查询时所需的元数据标记。在withCachePolicy的实现中可以看到它如何被引用(sdk/typescript/src/api/client.gen.ts):
/** * Returns the function updated to use the provided cache policy. * @param policy The cache policy to use. * @param opts.timeToLive The TTL for the cache policy, if applicable. Provided as a duration string, e.g. "5m", "1h30s". */ withCachePolicy = ( policy: FunctionCachePolicy, opts?: FunctionWithCachePolicyOpts, ): Function_ => { const metadata = { policy: { is_enum: true, value_to_name: FunctionCachePolicyValueToName }, } const ctx = this._ctx.select("withCachePolicy", { policy, ...opts, __metadata: metadata, }) return new Function_(ctx) }其中policy: { is_enum: true, value_to_name: FunctionCachePolicyValueToName }告知序列化层"这是一个枚举参数,需要经过value_to_name转换后再发送"。
使用方式:withCachePolicy 与 TTL 参数
枚举通过Function_.withCachePolicy(policy, opts?)方法应用到某个函数定义上。其 GraphQL 签名定义于 core/schema/testdata/base_schema.graphqls:
"""Returns the function updated to use the provided cache policy.""" withCachePolicy( """The cache policy to use.""" policy: FunctionCachePolicy! """ The TTL for the cache policy, if applicable. Provided as a duration string, e.g. "5m", "1h30s". """ timeToLive: String ): Function!参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
policy | FunctionCachePolicy | 是 | 要采用的缓存策略,取值为Default/Never/PerSession |
timeToLive | String(时长字符串) | 否 | 缓存 TTL,例如"5m"、"1h30s",仅适用于Default策略 |
服务端解析逻辑位于 core/schema/module.go 的functionWithCachePolicy:
func (s *moduleSchema) functionWithCachePolicy( ctx context.Context, fn *core.Function, args struct { Policy core.FunctionCachePolicy TimeToLive dagql.Optional[dagql.String] }, ) (*core.Function, error) { fn = fn.Clone() fn.CachePolicy = args.Policy if args.TimeToLive.Valid { // For now, restrict TTLs to the default policy. We could support it // for PerSession in the future if desired. if fn.CachePolicy != core.FunctionCachePolicyDefault { return nil, errors.New("time to live can only be set with default cache policy") } ttlDuration, err := time.ParseDuration(string(args.TimeToLive.Value)) if err != nil { return nil, fmt.Errorf("failed to parse time to live duration %q: %w", args.TimeToLive.Value, err) } switch { case ttlDuration == 0: // a TTL of 0 sounds an awful lot like "never cache", so we treat it that way. fn.CachePolicy = core.FunctionCachePolicyNever case ttlDuration < core.MinFunctionCacheTTLSeconds*time.Second: return nil, fmt.Errorf("time to live duration must be at least %q, got %q", (core.MinFunctionCacheTTLSeconds * time.Second).String(), args.TimeToLive.Value, ) case ttlDuration > core.MaxFunctionCacheTTLSeconds*time.Second: return nil, fmt.Errorf("time to live duration must be at most %q, got %q", (core.MaxFunctionCacheTTLSeconds * time.Second).String(), args.TimeToLive.Value, ) default: fn.CacheTTLSeconds = dagql.NonNull(dagql.Int(int(ttlDuration.Seconds()))) } } return fn, nil }这里有几个关键的校验规则,使用timeToLive时必须注意:
- TTL 只能与
Default策略搭配。对Never或PerSession传入 TTL 会直接报错:"time to live can only be set with default cache policy"; - TTL 为 0 会被当作"永不缓存",引擎会将其转换为
FunctionCachePolicyNever(代码注释也承认"TTL 为 0 听起来很像永不缓存"); - TTL 有上下限:下限为
MinFunctionCacheTTLSeconds = 1秒(core/modfunc.go),上限为MaxFunctionCacheTTLSeconds = 7 * 24 * 60 * 60秒,即1 周(core/modfunc.go)。超出范围会返回对应错误; - TTL 格式使用 Go 的
time.ParseDuration语法,如"5m"、"1h30s"、"300s",支持的单位包括ns、us/µs、ms、s、m、h。
TTL 最终以秒为单位写入函数的CacheTTLSeconds元数据字段(core/typedef.go 中Function结构体同时持有CachePolicy FunctionCachePolicy与CacheTTLSeconds dagql.Nullable[dagql.Int]),并随模块定义序列化传递。
模块运行时中的真实调用场景
FunctionCachePolicy并非仅存在于类型定义层面,它已被 TypeScript 模块运行时实际使用。在 sdk/typescript/src/module/entrypoint/register.ts 的addFunction中,运行时根据函数声明里的cache配置自动映射到对应策略:
switch (fct.cache) { case "never": { fnDef = fnDef.withCachePolicy(FunctionCachePolicy.Never) break } case "session": { fnDef = fnDef.withCachePolicy(FunctionCachePolicy.PerSession) break } case "": { break } default: { const opts: FunctionWithCachePolicyOpts = { timeToLive: fct.cache } fnDef = fnDef.withCachePolicy(FunctionCachePolicy.Default, opts) } }映射关系一目了然:
cache: "never"→FunctionCachePolicy.Never,函数结果永不缓存;cache: "session"→FunctionCachePolicy.PerSession,结果仅在当前会话内缓存;cache为空 → 不显式设置,走Default默认行为;cache为其他字符串(如"5m"、"1h")→ 视为 TTL 时长字符串,以Default+timeToLive调用。
这印证了在 Dagger 模块源码中声明函数缓存行为时,只需用@cache装饰/注解(例如 core/integration/testdata/modules/dang/test-directives/main.dang 中@cache(policy: FunctionCachePolicy.Never)的用法),运行时便会自动转换为上述withCachePolicy调用链。
引擎侧的持久化语义
综合前面的源码证据,三种策略在引擎层面的核心区别可以归纳如下(依据 core/typedef.go 与 core/modfunc.go):
| 策略 | 结果持久化(IsPersistable) | 缓存作用域 | TTL 行为 |
|---|---|---|---|
Default | 是(true) | 跨会话持久化 | 显式 TTL;未设置时使用上限 1 周 |
Never | 否(false) | 无(每次调用独立,PerCallInput) | 不可设置(TTL 仅限 Default) |
PerSession | 否(false) | 当前会话内(PerSessionInput) | 不可设置(TTL 仅限 Default) |
此外,Default与PerSession之间还存在一条兼容性回退路径:当模块显式设置DisableDefaultFunctionCaching(旧模块为保持旧行为而禁用默认函数缓存)时,Default会被derivedCachePolicy改写为PerSession(core/typedef.go),从而延续旧版"会话级缓存"的语义。
参考与延伸阅读
- 枚举权威定义:sdk/typescript/src/api/client.gen.ts
withCachePolicy客户端实现:sdk/typescript/src/api/client.gen.ts- 模块运行时策略映射:sdk/typescript/src/module/entrypoint/register.ts
- GraphQL Schema 定义:core/schema/testdata/base_schema.graphqls
- 服务端解析与 TTL 校验:core/schema/module.go
- 缓存策略推导与持久化标记:core/typedef.go
- 隐式缓存输入与 TTL 常量:core/modfunc.go、core/modfunc.go
- 相关枚举参考:TypeScript API 参考索引、模块概览
【免费下载链接】daggerAutomation engine to build, test and ship any codebase. Runs locally, in CI, or directly in the cloud项目地址: https://gitcode.com/GitHub_Trending/da/dagger
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考