perf(jdbc): optimize incremental sync with MapScanConcurrent - #1062
perf(jdbc): optimize incremental sync with MapScanConcurrent#1062krishanu7 wants to merge 5 commits into
Conversation
|
I updated MapScanConcurrent to use a 512-slot buffered channel instead of being unbuffered. This allows the database fetcher to run at full speed without waiting for the consumer on every row. In my tests, this improved throughput by ~36%. I checked the edge cases, and it's completely safe: Errors/Cancellations: If either side fails, the channel closes safely and unconsumed rows are garbage collected. No data is lost.
The unbuffered channel took ~154 million ns (154 ms) to process the batch The benchmarking tool was able to run the buffered channel 9 full times within the time limit, compared to just 7 times for the unbuffered one Both implementations used the exact same amount of memory per batch (~54.4 MB) and exact same allocations (~1 million) Commit message of buffered channel Impl : 5e7a047 |
Just FYI, This does not compile driver code at all unless you ran this inside per driver folder manually... our driver code have its own go modules and ./... will be limited to the root module that builds only common code. To actually compile all the drivers, we need to use |
| } | ||
|
|
||
| return rows.Err() | ||
There was a problem hiding this comment.
Redundant line here, install go formatter in your IDE to avoid such mistakes...
| if err != nil { | ||
| return fmt.Errorf("failed to scan record: %s", err) | ||
| } | ||
| setter := jdbc.NewReader(ctx, incrementalQuery, func(ctx context.Context, query string, args ...any) (*sql.Rows, error) { |
There was a problem hiding this comment.
We need to handle closing the opened rows explicitly. JDBC backfills use defer tx.Rollback() which handles this but I don't think this is the case here...
Find out if there is a way to do this from here, but if we need to change the jdbc.MapScanConcurrent then we will need to test it thoroughly
|
@krishanu7 I see we still use MapScan in Also we can then remove the function and the TODO completely then |
|
Hey @mihir-datazip — addressed your review feedback. Quick summary: Fixed the rows leak: Reader.Capture() now calls defer rows.Close() Tests Rango test -v ./pkg/jdbc/... -count=1 # 9/9 pass built postgres, mysql, mssql, oracle modules — all goodManual CDC check Mssqlmake db.mssql.start make dev.mssql.build ./drivers/mssql/olake discover --config /tmp/olake-mssql-cdc-test/source.json Sync 1 — backfill (CDC initial load)./drivers/mssql/olake sync Insert a row, wait for CDC capture agentINSERT INTO dbo.users (email) VALUES ('cdc-test@example.com'); Sync 2 — CDC insert./drivers/mssql/olake sync ... --state /tmp/olake-mssql-cdc-test/state.json Result: Update + delete, wait for CDC agentUPDATE dbo.users SET email='updated@example.com' WHERE id=1; Sync 3 — CDC update + delete./drivers/mssql/olake sync ... --state /tmp/olake-mssql-cdc-test/state.json |
|
@krishanu7 lint is failing on your PR, please enable precommit hooks and also add go formatter in your IDE to avoid lint failures |
Okay thanks |
There was a problem hiding this comment.
I don't think these UTs are helping to assert the changes we are adding...
database/sql auto-closes Rows whenever Next() returns false — both on exhaustion (EOF) and on an iteration error (nextLocked sets doClose whenever lasterr != nil). That means the driver-level closeCount reaches 1 with or without the defer Close() you added to Capture:
- TestReaderCapture_closesRowsOnSuccess — passes even if you delete the defer (auto-close on EOF).
- TestReaderCapture_closesRowsOnRowsErr — passes without the defer (auto-close on iteration error).
- TestReaderCapture_doesNotLeakConnection — iterates to exhaustion every loop, so the conn is returned via auto-close; passes without the defer.
- TestReaderCapture_closesRowsOnCaptureError — the only test that actually fails without your fix, because an early return from onCapture is the one path where rows are still open.
So the suite mostly re-verifies database/sql's own behavior. You can confirm with a quick mutation test: comment out the defer block in reader.go:46-48 and run go test ./pkg/jdbc/ — only closesRowsOnCaptureError fails.
There was a problem hiding this comment.
I would recommend to add testing for Reader with a fake iterable, that way we can assert the correct behaviour of Capture easily without need for sql at all.
| if err != nil { | ||
| return err | ||
| } | ||
| if closer, ok := any(rows).(interface{ Close() error }); ok { |
There was a problem hiding this comment.
Instead of doing this, we should just add Close() in iterable as we already use sql.Rows which needs closing so its better to have it in the interface by design rather than guessing
|
@mihir-datazip I found a potential loophole in MapConncurent around producer consumer cancellation. When the consumer fails and ctx.Done() fires, the producer currently returns nill "case <-ctx.Done(): return nil " , which I think may cause Capture to continue scanning the remaining rows instead of stopping early. let me know whether my understanding is correct. |
Add Close() to types.Iterable so Capture always releases cursors by contract instead of a runtime type assertion. Replace sql-mock close tests with a fake iterable that does not auto-close, and stop the MapScanConcurrent producer when context is canceled so rows close promptly on downstream errors.



Description
This PR refactors the incremental sync pipeline to use
MapScanConcurrentinstead of the synchronousMapScanacross all SQL-based relational database drivers (Postgres,MySQL,MSSQL, andOracle).Previously, incremental sync used synchronous scanning, which blocked database I/O while waiting for data type conversion and column sizing calculations to complete on each row. By switching to
MapScanConcurrent(which uses a concurrent Producer/Consumer pipeline), data fetching from the database and CPU processing are decoupled. This increases throughput during incremental syncs (especially over networked DB connections) and drastically reduces memory allocations (~40% reduction) via pointer array reuse.Fixes # (issue)
Type of change
How Has This Been Tested?
pkg/jdbc/reader_test.goverifying thatMapScanConcurrentgenerates output identical toMapScan.go build ./...).Screenshots or Recordings
N/A
Documentation
Related PR's (If Any):
MapScanConcurrentfor full refresh backfill)