Skip to content

Exclude runId from DatasetVersion hash to prevent version explosion - #3112

Open
kalra-mohit wants to merge 1 commit into
MarquezProject:mainfrom
kalra-mohit:fix/3082-remove-runid-from-dataset-version-hash
Open

Exclude runId from DatasetVersion hash to prevent version explosion#3112
kalra-mohit wants to merge 1 commit into
MarquezProject:mainfrom
kalra-mohit:fix/3082-remove-runid-from-dataset-version-hash

Conversation

@kalra-mohit

@kalra-mohit kalra-mohit commented Jul 22, 2026

Copy link
Copy Markdown

DatasetVersion is supposed to identify the content of a dataset at a point in time — namespace, source, physical name, schema, lifecycle state. But the hash that produces its UUID (Utils.newDatasetVersionFor(DatasetVersionData), the single private method backing all three public overloads used by OpenLineageDao, RunDao, and DatasetVersionDao) also folded in data.getRunId():

final byte[] bytes =
    VERSION_JOINER
        .join(
            data.getNamespace(),
            data.getSourceName(),
            data.getDatasetName(),
            data.getPhysicalName(),
            data.getSchemaLocation(),
            data.getFields().stream().map(Utils::joinField).collect(joining(VERSION_DELIM)),
            data.getLifecycleState(),
            data.getRunId())   // <-- shouldn't be here
        .getBytes(UTF_8);

Since runId is unique per run, every single run that writes to a dataset mints a brand new DatasetVersion — and new dataset_versions / dataset_versions_field_mapping rows — even when the dataset's content is byte-for-byte identical to the previous run. High-frequency jobs writing the same dataset over and over blow this table up, which is exactly the query timeouts described in #3082.

flowchart TD
    W["Run writes dataset X
(same schema/content as the previous run)"]
    W --> H1["Old hash input:
namespace + source + name + physicalName +
schema + fields + lifecycleState + runId"]
    H1 --> V1["Different UUID every run
dataset_versions row explosion"]
    W --> H2["New hash input:
namespace + source + name + physicalName +
schema + fields + lifecycleState"]
    H2 --> V2["Same UUID as previous run
ON CONFLICT(version) DO UPDATE fires"]
    V2 --> T["Trade-off: dataset_versions.run_uuid
flips to the most recent writer of that content"]
Loading

Fix: dropped data.getRunId() from the hash. It's now purely content-based, matching what the docs say DatasetVersion is, and matching the sibling method newDatasetSchemaVersionFor, which never included runId in the first place. Because newDatasetVersionFor(DatasetVersionData) is the single choke point for all three overloads, this fixes it everywhere a DatasetVersion gets minted — no call sites changed, runId is still accepted as a parameter and still recorded via dataset_versions.run_uuid, it's just out of the content hash now.

Testing: added UtilsTest#testDatasetVersionEqualAcrossDifferentRunsWithSameDatasetContent — builds two DatasetVersions from identical content but two different, randomly generated runIds and asserts they're equal. Fails pre-fix (two different UUIDs), passes post-fix. Full unit suite is green: ./gradlew :api:testUnit, 119 tests.

I wasn't able to run the Postgres-backed DAO/integration tests (RunDaoTest, DatasetDaoTest, etc.) locally — Testcontainers in my sandbox can't negotiate with the local Docker Engine (the bundled docker-java client defaults to API v1.32, the engine here needs >= v1.40). Not related to this change, just a local environment gap. Would appreciate CI running those.

One trade-off I want to flag explicitly

DatasetVersionDao.upsert(...) already has ON CONFLICT(version) DO UPDATE SET run_uuid = EXCLUDED.run_uuid, .... Before this fix, runId being in the hash guaranteed every write produced a unique version, so that conflict branch never actually fired for OpenLineage-ingested writes.

After this fix, two different runs writing identical content now collide on version and hit that DO UPDATE. Since dataset_versions has one row per distinct version, the update reassigns that row's run_uuid to whichever run most recently wrote the content.

That matters because RunDao's output_versions (surfaced via GET /runs/{id}) is essentially a one-to-one join on dv.run_uuid = r.uuid. So: Run A writes content X, its output_versions includes that version. Run B later writes the exact same content X — the shared row's run_uuid flips to Run B, and Run A's output_versions silently loses that dataset, even though Run A genuinely produced it at the time.

I think this is an acceptable trade-off — deduping identical content is the whole point of the fix, and keeping runId in the hash is the bug we're fixing here. It only affects "which run(s) produced this exact version" provenance for content that's genuinely unchanged across runs, not the correctness of the version itself or its schema/fields.

A complete fix would need dataset_versions.run_uuid to model a proper one-to-many relationship (one version legitimately "produced" by many runs that happened to write the same content) — something like a many-to-many run-to-dataset-version join table, mirroring how runs_input_mapping already works for inputs, plus updates to RunDao.output_versions and the API contract. That's bigger than this PR; happy to scope it as a follow-up if maintainers want it.

Question for maintainers: is the shrinking-output_versions-over-time behavior an acceptable trade-off given what this fixes, or would you rather pair this with a minimal many-to-many link now?

Fixes #3082.

@boring-cyborg boring-cyborg Bot added the api API layer changes label Jul 22, 2026
…arquezProject#3082)

Utils.newDatasetVersionFor(DatasetVersionData) included the run's
UUID as an input to the SHA/UUID-based hash used to compute a
dataset's Version. Since a DatasetVersion is meant to identify the
*content* of a dataset (namespace, source, physical name, schema
fields, lifecycle state), including runId meant a brand new
DatasetVersion (and corresponding dataset_versions /
dataset_versions_field_mapping rows) was created on every single run
that wrote to a dataset - even when the dataset's schema/content was
completely unchanged from the previous run. For datasets written by
high-frequency jobs this causes unbounded growth of dataset version
rows and the query timeouts described in the issue.

This method is the single choke point used by all three
DatasetVersion-hashing call sites (OpenLineageDao, RunDao, and
DatasetVersionDao), so removing runId from the hash here fixes the
issue everywhere a DatasetVersion is minted, without needing to touch
any call sites or public method signatures.

Adds a regression test,
testDatasetVersionEqualAcrossDifferentRunsWithSameDatasetContent,
which asserts that two DatasetVersions computed from identical
dataset content but different runIds are equal. Verified this test
fails before the fix (two different UUIDs) and passes after (same
UUID).

Fixes MarquezProject#3082

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Mohit Kalra <mohit2494@gmail.com>
@kalra-mohit
kalra-mohit force-pushed the fix/3082-remove-runid-from-dataset-version-hash branch from 83d00a7 to c890432 Compare July 22, 2026 01:35
@kalra-mohit
kalra-mohit marked this pull request as ready for review July 22, 2026 02:30
@kalra-mohit

Copy link
Copy Markdown
Author

@merobi-hub whenever you get a chance to take a look, happy to address any feedback!

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

Labels

api API layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Inclusion of runId in DatasetVersion hash causes version explosion and query timeouts in high-frequency jobs

1 participant