fix(forge): reject backoff caps below the active interval

This commit is contained in:
Yichen Jiang committed 2026-10-26 17:49:26 +08:00
1 parent 1ebb31915b
commit 7457789b98
5 files changed
+19 -4

No files matched your search

+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 2f8a621b991260a1
/deepseek-ai-dsh-experimental-forge/use-this-package:
en: a16d7eb7b6abee1f
zh: e2b93e1aa95e2aaa
en: 83fdebf81ffa8010
zh: fdda2e83a7b60072
/deepseek-ai-dsh-experimental-forge/understand-the-implementation:
en: e193cb547c47069b
zh: faa517522402ea41
+1 -1
View File
@@ -34,7 +34,7 @@ Mount the registry with at least one provider package, such as the GitHub provid
`ctx.forge.register(provider)` is an effect on the calling fiber and returns an async disposer that cancels and drains that registration's calls. Results arriving after cancellation are discarded. One provider serves one forge family; registering a second provider for the same family throws. Registration changes announce `forge/providers-changed`; registering a provider polls its existing watches immediately. `repositoryFromRemote(url)` and `changeRequestFromUrl(url)` ask providers in registration order and return the first match. `changeRequestUrl(ref)` and `findByHead(repository, branch, signal)` delegate to the provider of the reference's forge and throw when none is registered.
`ctx.forge.watch(ref, listener)` is also an effect. The first watcher of a change request polls it at once; `latest(ref)` returns the most recent result. Each forge has at most one fetch in flight, and that fetch includes every due change request of the forge. An open change request is polled every `activeIntervalMs` while checks are pending or mergeability is unknown, and every `idleIntervalMs` otherwise. Closed and merged change requests are not polled again. A failed read repeats after `activeIntervalMs` doubled for each consecutive failure, up to `maxBackoffMs`. Unregistering a provider settles its watched change requests as `unavailable`. Polling stops when the last watcher leaves. The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-forge) lists the defaults.
`ctx.forge.watch(ref, listener)` is also an effect. The first watcher of a change request polls it at once; `latest(ref)` returns the most recent result. Each forge has at most one fetch in flight, and that fetch includes every due change request of the forge. An open change request is polled every `activeIntervalMs` while checks are pending or mergeability is unknown, and every `idleIntervalMs` otherwise. Closed and merged change requests are not polled again. A failed read repeats after `activeIntervalMs` doubled for each consecutive failure, up to `maxBackoffMs`, which must be at least `activeIntervalMs`. Unregistering a provider settles its watched change requests as `unavailable`. Polling stops when the last watcher leaves. The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-forge) lists the defaults.
Reaching `fetchTimeoutMs` aborts the fetch signal with `FORGE_FETCH_TIMEOUT`. Providers must stop their work and settle when the signal aborts; rejected fetches become `unavailable` failures. A provider that ignores cancellation can stall polling and service disposal.
+1 -1
View File
@@ -34,7 +34,7 @@ kind: "package-reference"
`ctx.forge.register(provider)` 是调用方 fiber 上的 effect,返回异步 disposer,取消并等待该次注册的调用结束。取消后到达的结果会被丢弃。一个提供方服务一个 forge 家族;为同一家族注册第二个提供方会抛错。注册变化会发出 `forge/providers-changed`;注册提供方会立即轮询其已有监听。`repositoryFromRemote(url)` 与 `changeRequestFromUrl(url)` 按注册顺序询问提供方并返回第一个匹配结果。`changeRequestUrl(ref)` 与 `findByHead(repository, branch, signal)` 委托给引用所属 forge 的提供方;没有注册提供方时抛错。
`ctx.forge.watch(ref, listener)` 同样是 effect。变更请求的第一个监听方会立即触发轮询;`latest(ref)` 返回最近一次结果。每个 forge 同时最多只有一次读取在进行,这次读取包含该 forge 所有到期的变更请求。打开状态的变更请求在检查未完成或可合并性未知时每 `activeIntervalMs` 轮询一次,否则每 `idleIntervalMs` 轮询一次。已关闭和已合并的变更请求不再轮询。读取失败后,重试间隔为 `activeIntervalMs` 按连续失败次数翻倍,最长 `maxBackoffMs`。注销提供方会让其被监听的变更请求以 `unavailable` 结束。最后一个监听方离开后轮询停止。默认值见[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-forge)。
`ctx.forge.watch(ref, listener)` 同样是 effect。变更请求的第一个监听方会立即触发轮询;`latest(ref)` 返回最近一次结果。每个 forge 同时最多只有一次读取在进行,这次读取包含该 forge 所有到期的变更请求。打开状态的变更请求在检查未完成或可合并性未知时每 `activeIntervalMs` 轮询一次,否则每 `idleIntervalMs` 轮询一次。已关闭和已合并的变更请求不再轮询。读取失败后,重试间隔为 `activeIntervalMs` 按连续失败次数翻倍,最长 `maxBackoffMs`;该值必须大于或等于 `activeIntervalMs`。注销提供方会让其被监听的变更请求以 `unavailable` 结束。最后一个监听方离开后轮询停止。默认值见[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-forge)。
达到 `fetchTimeoutMs` 时,读取信号会以 `FORGE_FETCH_TIMEOUT` 中止。提供方必须在信号中止时停止工作并结束请求;被拒绝的读取会转为 `unavailable` 失败。忽略取消的提供方可能阻塞轮询和服务销毁。
+3
View File
@@ -86,6 +86,9 @@ export class ForgeService extends Service {
throw new Error(`forge: ${key} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`)
}
}
if (this.config.maxBackoffMs < this.config.activeIntervalMs) {
throw new Error('forge: maxBackoffMs must be greater than or equal to activeIntervalMs')
}
ctx.effect(() => async () => {
this.lifetime.abort(new Error('forge service disposed'))
clearTimeout(this.timer)
@@ -11,6 +11,18 @@ afterEach(async () => {
})
describe('forge registry', () => {
it('rejects a backoff cap shorter than the active poll interval', async () => {
ctx = new Context()
await expect(ctx.plugin(ForgeService, { activeIntervalMs: 30_000, maxBackoffMs: 1_000 }))
.rejects.toThrow('maxBackoffMs must be greater than or equal to activeIntervalMs')
})
it.each([1_000, 2_000])('accepts backoff cap %s at or above the active interval', async (maxBackoffMs) => {
ctx = new Context()
await ctx.plugin(ForgeService, { activeIntervalMs: 1_000, maxBackoffMs })
expect(ctx.forge).toBeDefined()
})
it('resolves remotes and change request URLs through the first provider that serves them', async () => {
ctx = await forgeContext()
await provide(ctx, new FakeProvider('fake'))