[Bug][Collector] Stateful collector silently discards the final page when ResponseParser returns items with ErrFinishCollect
维护者通常 3 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 3/5
- 预计耗时
- 1-2 天
- 新手友好度
- 65/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- go
调研方向
The bug is in backend/helpers/pluginhelper/api/api_collector_stateful.go. Start by reading the ResponseParser wrapper and the fetchAsync method. The fix is to modify the wrapper to return items when err is ErrFinishCollect. Test with the CircleCI plugin's pipeline_collector.go, ensuring the last page is not discarded. Run the reproduction steps to verify the fix.
由索引模型根据 Issue 内容生成。
描述
Description
api_collector_stateful.go's ResponseParser wrapper discards the records returned by the
plugin's own parser whenever that parser signals ErrFinishCollect. The result is a silent,
page-aligned data loss at the timeAfter boundary: the last page of a time-bounded collection
is dropped in full, including the records on it that are inside the requested window.
Nothing errors. The task reports TASK_COMPLETED with zero failed subtasks, and every
internal consistency check passes, because the tool tables and the domain tables agree with
each other — they are both derived from the same truncated raw data. The loss is only visible
by comparing against the upstream API directly.
Root cause
backend/helpers/pluginhelper/api/api_collector_stateful.go:
ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) {
items, err := args.CollectNewRecordsByList.ResponseParser(res)
if err != nil {
return nil, err // <-- discards `items` even when err is ErrFinishCollect
}
Plugin parsers are written to return the valid prefix of a page together with the stop signal.
For example backend/plugins/circleci/tasks/pipeline_collector.go:
for _, item := range data.Items {
pipelineCreatedAt, err := extractCreatedAt(item)
if err != nil { return nil, err }
if pipelineCreatedAt.Before(*timeAfter) {
return filteredItems, api.ErrFinishCollect // in-window items + stop
}
filteredItems = append(filteredItems, item)
}
The wrapper replaces filteredItems with nil. ApiCollector.fetchAsync then takes the
count == 0 early return and never reaches db.Create, so the page is never persisted:
items, err := collector.args.ResponseParser(res)
if err != nil {
if errors.Is(err, ErrFinishCollect) {
logger.Info("a fetch stop by parser, reqInput: #%s", reqData.Params)
handler = nil
} ...
}
count := len(items)
if count == 0 {
collector.args.Ctx.IncProgress(1)
return nil // <-- page silently dropped
}
Note fetchAsync itself handles this correctly — it persists whatever it is given before
stopping pagination. The loss is introduced purely by the wrapper nulling the slice.
This affects any plugin using NewStatefulApiCollectorForFinalizableEntity whose
ResponseParser returns a partial page with ErrFinishCollect, not only circleci.
Regression lineage
The circleci parser gained its return filteredItems, api.ErrFinishCollect line in #7820
(merged 2024-08-15), which fixed #7797 "CircleCI pipelines collected from before time range".
That plugin-side change is correct on its own terms — it stops collection at the window
boundary instead of running past it. But because the stateful wrapper nulls the slice whenever
the parser returns an error, the fix effectively traded collecting too much for silently
collecting too little. #7797's symptom was visible in the data; this one is not.
Impact
Loss is bounded by one page, so it is proportionally worst for low-volume scopes — the
opposite of where it is likely to be noticed. Measured on three repositories with
timeAfter = 90 days, PageSize = 20 (the CircleCI default):
| repository | upstream API | collected | lost | loss |
|---|---|---|---|---|
| A (0.38 pipelines/day) | 34 | 20 | 14 | 41% |
| B (3.1 pipelines/day) | 276 | 260 | 16 | 5.8% |
| C (6.3 pipelines/day) | 568 | 560* | 8 | 1.4% |
* predicted from the same model; C was collected after the workaround was applied and
returned the full 568.
Reproduction
- Configure a CircleCI connection and a project scope for a repository with more pipelines in
the window than one page (>20), where the window boundary falls mid-page. - Set a blueprint
timeAftersuch that the boundary page contains both in-window and
out-of-window pipelines. - Run the blueprint. It completes
TASK_COMPLETED, zero failed subtasks. SELECT count(*) FROM _tool_circleci_pipelines WHERE project_slug = '...'returns an exact
multiple of the page size.- Compare with
GET /v2/project/{slug}/pipelinecounted directly over the same window — the
difference is the in-window records on the discarded page.
Confirming signals, all consistent:
_raw_circleci_api_pipelinesholds the same truncated count, so the loss is at collection,
not extraction.- Collected pipeline
numbervalues are perfectly contiguous — the tail is amputated, records
are not dropped at random. - A
fullSync: truere-run reproduces the identical count. - In-band detector:
collectPipelines.finishedRecordscounts pages fetched while
extractPipelines.finishedRecordscounts records kept;pages - kept/PageSizeis the number
of discarded pages. This agreed with thea fetch stop by parserlog line on exactly the runs
that lost data.
Suggested fix
Preserve the partial page when the stop signal is ErrFinishCollect:
items, err := args.CollectNewRecordsByList.ResponseParser(res)
if err != nil {
if errors.Is(err, ErrFinishCollect) {
return items, err
}
return nil, err
}
fetchAsync already persists the returned items and then stops paginating, so no other change
is needed.
Workaround for operators
Set timeAfter earlier than the window you actually need, so the discarded page falls in the
buffer rather than in the data you rely on. The buffer must exceed one page expressed in
time, which varies with each scope's rate: at PageSize 20 that is ~3 days for a repository
at 6.3 pipelines/day but ~53 days for one at 0.38/day. A fixed buffer chosen for busy scopes
will not protect quiet ones.
Environment
- DevLake
v1.0.3-beta17(8fe26f4), Helm chart1.0.3-beta9 - PostgreSQL backend
- Plugins: circleci, github_graphql
PIPELINE_MAX_PARALLEL=1,API_REQUESTS_PER_HOUR=3000
- 主要语言
- Go
- 星标
- 3.2k
- 派生
- 819
- 平均合并
- 2 天 21 小时
- 30 天内合并 PR
- 46
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 没有贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
apache/devlake 的其他 Issue
-
[Bug][e2e] Several plugin e2e tests fail on a fresh database because they depend on tables created by other tests可能已有人在做 @chenwei791129 今天认领。 未关闭
难度 4/5 3-5 天 新手友好度 35/100
维护者通常 3 天内回复
-
难度 5/5 一周以上 新手友好度 15/100
维护者通常 3 天内回复
-
难度 3/5 半天 新手友好度 62/100
维护者通常 3 天内回复
-
难度 3/5 1-2 天 新手友好度 42/100
维护者通常 3 天内回复
-
难度 4/5 3-5 天 新手友好度 35/100
维护者通常 3 天内回复
相似的 Issue
-
Say in the changelog that the text-index update cannot be rolled back可能已有人在做 @praetorianer777 今天认领。 未关闭
难度 1/5 1 小时以内 新手友好度 88/100
praetorianer777/GoTome#275 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
SAP/crossplane-provider-btp#1118 ·
维护者通常 1 天内回复
-
benchmark: `make setup` fails because audit-exporter builds with Go 1.25.0 while go.mod requires 1.26.0可能已有人在做 @xsvm 今天认领。 未关闭
难度 1/5 1 小时以内 新手友好度 84/100
volcano-sh/volcano#6097 · 1 条评论 ·
维护者通常 1 天内回复
-
area/infra backlog bug
难度 2/5 1-3 小时 新手友好度 72/100
praetorianer777/Stator#417 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
维护者通常 1 天内回复