Skip to content

feat(processor): fork event processing for rsources pipelines - #7235

Open
atzoum wants to merge 1 commit into
masterfrom
feat.procIsolationRsources
Open

feat(processor): fork event processing for rsources pipelines#7235
atzoum wants to merge 1 commit into
masterfrom
feat.procIsolationRsources

Conversation

@atzoum

@atzoum atzoum commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Description

Closes the rsources accounting gap #7207 left open: forking siphons an event to the intermediate proc jobsdb before it's transformed, but until now nothing recorded that in rsources until the proc pool eventually drained it. An arbitrarily later, unbounded window in which a retl jobRunId could be reported completed while its forked events were still sitting unaccounted-for in proc. That's why forking was hard-excluded for any event carrying a SourceJobRunID.

The mechanism
No new stage is introduced, it piggybacks on the existing source-level In/Out the same way dropped-job accounting already does (JobsDropped). JobsForked increments In by the number of consumers a job is forked to (e.g. one job forked to 2 destinations bumps In by 2), committed in the same transaction as the proc write itself so there's no window where the fork is durable but unaccounted-for. Out is unaffected: the proc pool's own rsources.StatsCollector (proc_partition_worker.go) already calls CollectStats once per (job, consumer) as each forked job drains to a terminal status. A source now only reports completed once every forked consumer has drained, in addition to the existing per-destination router/batch-router accounting, which is untouched.

Still opt-in
Forking retl-tracked events is gated by its own config, disabled by default:

Processor.DestinationIsolation.forkRsourcesTrackedJobs

Verification

A new integration test TestProcessorRsourcesStats covers both the forked and non-forked paths end to end against the rsources job-status API:

  • inline- baseline, isolation off.
  • retl_excluded - retl events still stay inline and the numbers match inline exactly.
  • all_forked - every destination is forked, proving the fork In balances against the proc consumer's drained Out.
  • mixed - a destination that's both forked and filtered by a user transformation (a proc-consumer status update with nothing ever reaching the router), and another one is both forked and aborted at the router.

Each scenario asserts exact In/Out/Failed at source and destination level, Completed via /internal/v2/job-status, failed-records resolution back to the sent recordId for aborted destinations, jobsdb drain counts, and webhook delivery counts.

Linear Ticket

resolves PIPE-3055

Security

  • The code changed/added as part of this pull request won't create any security issues with how the software is being used.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.85%. Comparing base (a6e29f2) to head (3ce5be1).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7235      +/-   ##
==========================================
- Coverage   79.88%   79.85%   -0.04%     
==========================================
  Files         600      600              
  Lines       67013    67034      +21     
==========================================
- Hits        53534    53530       -4     
- Misses      10335    10352      +17     
- Partials     3144     3152       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@atzoum
atzoum marked this pull request as draft July 30, 2026 10:03
@atzoum
atzoum requested review from ktgowtham and mihir20 July 30, 2026 12:41
Base automatically changed from feat.procPoolFork to master July 30, 2026 19:41
atzoum added a commit that referenced this pull request Jul 30, 2026
…ations (#7207)

# Description

Adds the **write side** of the destination-isolated pipeline: the gw
pool now **forks** events for configured destinations into the
intermediate **`proc` jobsdb** (consumed by the proc pool from #7202)
instead of transforming them inline, so a slow/failing destination's
events are siphoned off and no longer hold up the shared pipeline.

Forking is opt-in per destination via a hierarchical, config which is by
default disabled:

```
Processor.DestinationIsolation.enabledDestinations.<destinationID>
Processor.DestinationIsolation.enabledDestinations.all
```

**The fork.** At the `pretransform` fan-out stage, each event's forked
destinations are split off from the inline `groupedEvents`. One `proc`
job is written per forked event, carrying the forked destination IDs as
its **consumers** (multi-consumer job) and a source-level payload. The
forked-job list is carried over through the transform stages and finally
committed during the `store` stage.

**Atomicity.** The `proc` write commits in the **same transaction** as
the gateway status update so there is no room for duplicate jobs to be
written in **`proc` jobsdb** in case of a crash.

**rsources/retl excluded (for now).** Events carrying a `sourceJobRunID`
stay inline until the intermediate stage is modelled in rsources
accounting, otherwise a retl source could be reported complete while its
events still sit in the queue.

### Supporting jobsdb changes

- **Post-commit listeners register only after the store/update
succeeds** (`internalStoreJobsInTx`, `pendingEventsJobsDB`). With
`WithStoreSafeTxFromTx` the caller's transaction is reused across the
internal stale-dataset-list retry, so registering before the op
double-fired the pending-events / cache / stat listeners on every retry.
- **Single-consumer handles now allow persisting an explicit `consumers`
array.** The partition-migration buffer (`proc_buf`) is a
single-consumer handle that relays multi-consumer `proc` jobs between
nodes; it previously collapsed their consumers to the legacy `''`, so
migrated jobs resurfaced under an unknown destination and were dropped.
`proc` is the first multi-consumer jobsdb to be migrated, so this was
never hit before.
- **`checkIfCompactDS` rewrite**: the terminal-job-count check (single-
and multi-consumer) now does an index-only grouped scan instead of a
per-job correlated subquery against the `v_last_*` views — pure perf,
same result. Needed once `proc` compaction is exercised at the volumes
below.
- **`WithDefaultSkipStatusCompaction`**: pass-through jobsdbs (gw, esch,
arc, proc, and their `_buf` migration-buffer counterparts, err_idx) skip
status compaction by default — they have no router-style retry backlog,
so there's nothing to reclaim.

### Deciding when to enable: fan-out visibility

An event on a source wired to N destinations produces N forked jobs,
each needing its own proc consumer status update. So isolating all
destinations is cheap on narrowly-wired sources but gets progressively
more expensive as its source's fan-out grows — even if most of those
destinations are filtered out downstream by a user transformation before
they'd ever reach the router. For narrow-fanout scenarios we can safely
isolate all destinations, while for wide-fanout ones we may need to
isolate only specific slow/failing destinations rather than paying the
fan-out cost for all of them. Two new stats provide visibility to make
that call: `processor_source_to_destination_fanout` (histogram) and
`processor_max_source_to_destination_fanout` (gauge), recorded per event
at fan-out, before filtering.

`BenchmarkProcessorDestinationIsolation` quantifies this trade-off
directly: 100k events fanned out to 10/20/50 destinations (all but 2
filtered downstream), with and without forking, across a few
`JobsDB.proc.maxDSSize` settings.

### Benchmark results

Single-run wall-clock drain times (local Docker Postgres + transformer —
directional, not a production SLA):

| Destinations | No fork | Forked (best `maxDSSize`) | Overhead |
|---|---|---|---|
| 10 | 32.9s | 36.7s (20k) | +12% |
| 20 | 58.8s | 1m08.5s (10k) | +16% |
| 50 | 2m12.9s | 3m12.9s (10k) | +45% |

<details>
<summary>Full results</summary>

| Scenario | Time |
|---|---|
| 10 dest, no fork | 32.90s |
| 10 dest, forked, `maxDSSize`=100k | 43.52s |
| 10 dest, forked, `maxDSSize`=20k | 36.71s |
| 10 dest, forked, `maxDSSize`=10k | 37.23s |
| 20 dest, no fork | 58.81s |
| 20 dest, forked, `maxDSSize`=100k | 1m16.45s |
| 20 dest, forked, `maxDSSize`=20k | 1m10.40s |
| 20 dest, forked, `maxDSSize`=10k | 1m08.48s |
| 50 dest, no fork | 2m12.95s |
| 50 dest, forked, `maxDSSize`=20k | 3m48.67s |
| 50 dest, forked, `maxDSSize`=10k | 3m12.88s |
| 50 dest, forked, `maxDSSize`=5k | 3m18.04s |

</details>

### Tests

- Processor integration test (`TestProcessorDestinationIsolation`),
generalized to N destinations with partial forking and
user-transformation filtering: all-forked / mixed / none /
wide-fanout-mostly-filtered — asserts every event reaches the router
regardless of path, the expected `proc` job count and per-consumer
fan-out, and full drain.
- `BenchmarkProcessorDestinationIsolation` — proc consumer status-update
throughput at scale, exercising `maxProcDSSize`/compaction settings.
- jobsdb unit tests for both supporting fixes.
- Partition-migration tests (embedded + gw/proc) extended with a forked
destination (plus a second, router-aborted destination so proc jobs
carry multiple consumers) to verify migration moves all pending
consumers.

## Linear Ticket

resolves PIPE-3049

## Security

- [x] The code changed/added as part of this pull request won't create
any security issues with how the software is being used.

<!-- GitButler Footer Boundary Top -->
---
This is **part 1 of 2 in a stack** made with GitButler:
- <kbd>&nbsp;2&nbsp;</kbd> #7235 
- <kbd>&nbsp;1&nbsp;</kbd> #7207 👈 
<!-- GitButler Footer Boundary Bottom -->
@atzoum
atzoum force-pushed the feat.procIsolationRsources branch from 8262038 to d9d02a0 Compare July 30, 2026 19:41
@atzoum
atzoum marked this pull request as ready for review July 30, 2026 19:41
@atzoum
atzoum force-pushed the feat.procIsolationRsources branch from d9d02a0 to 847d9b5 Compare August 3, 2026 07:57
@atzoum
atzoum force-pushed the feat.procIsolationRsources branch from 847d9b5 to 3ce5be1 Compare August 4, 2026 07:06
Comment thread processor/processor.go
in.rsourcesStats.CollectStats(statusList)
// forkedJobs are only ever populated in the gw pool (the proc pool never re-forks),
// so this is a no-op when storeStage is invoked via procStoreStage.
in.rsourcesStats.JobsForked(in.forkedJobs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this mess up the stats for Rsources?
For this scenario, an event is ingested from a source connected to 2 destinations. Earlier we would have reported in = 1 and out = 1 for source-level stats. but now we will report in = 3 and out = 3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants