You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@devlake.apache.org by "michaelgriskified (via GitHub)" <gi...@apache.org> on 2026/09/24 06:28:41 UTC

[I] [Bug][Collector] Stateful collector silently discards the final page when ResponseParser returns items with ErrFinishCollect [devlake]

michaelgriskified opened a new issue, #9167:
URL: https://github.com/apache/devlake/issues/9167

   
   ### 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`:
   
   ```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`:
   
   ```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:
   
   ```go
   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
   
   1. 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.
   2. Set a blueprint `timeAfter` such that the boundary page contains both in-window and
      out-of-window pipelines.
   3. Run the blueprint. It completes `TASK_COMPLETED`, zero failed subtasks.
   4. `SELECT count(*) FROM _tool_circleci_pipelines WHERE project_slug = '...'` returns an exact
      multiple of the page size.
   5. Compare with `GET /v2/project/{slug}/pipeline` counted directly over the same window — the
      difference is the in-window records on the discarded page.
   
   Confirming signals, all consistent:
   
   - `_raw_circleci_api_pipelines` holds the same truncated count, so the loss is at collection,
     not extraction.
   - Collected pipeline `number` values are perfectly contiguous — the tail is amputated, records
     are not dropped at random.
   - A `fullSync: true` re-run reproduces the identical count.
   - **In-band detector:** `collectPipelines.finishedRecords` counts pages fetched while
     `extractPipelines.finishedRecords` counts records kept; `pages - kept/PageSize` is the number
     of discarded pages. This agreed with the `a fetch stop by parser` log line on exactly the runs
     that lost data.
   
   ### Suggested fix
   
   Preserve the partial page when the stop signal is `ErrFinishCollect`:
   
   ```go
   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 chart `1.0.3-beta9`
   - PostgreSQL backend
   - Plugins: circleci, github_graphql
   - `PIPELINE_MAX_PARALLEL=1`, `API_REQUESTS_PER_HOUR=3000`
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@devlake.apache.org.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] [Bug][Collector] Stateful collector silently discards the final page when ResponseParser returns items with ErrFinishCollect [devlake]

Posted by "klesh (via GitHub)" <gi...@apache.org>.
klesh closed issue #9167: [Bug][Collector] Stateful collector silently discards the final page when ResponseParser returns items with ErrFinishCollect
URL: https://github.com/apache/devlake/issues/9167


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@devlake.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] [Bug][Collector] Stateful collector silently discards the final page when ResponseParser returns items with ErrFinishCollect [devlake]

Posted by "ashok-gk (via GitHub)" <gi...@apache.org>.
ashok-gk commented on issue #9167:
URL: https://github.com/apache/devlake/issues/9167#issuecomment-5848739251

   Hi, I’m interested in working on this issue. I reviewed the `ResponseParser `behavior in the stateful collector and understand the problem to be that items returned together with `ErrFinishCollect` can be discarded, causing the final page of an otherwise successful collection to be lost. Before I start, could you confirm that preserving the returned items while still terminating collection is the intended fix and that this issue is available for an external contribution? I would also plan to add regression coverage for the final-page boundary case.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscribe@devlake.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org