Skip to content

GH-3696: Cache ParsedVersion in FileMetaData to eliminate redundant parsing - #3700

Open
asifsmohammed wants to merge 2 commits into
apache:masterfrom
asifsmohammed:gh-3696-cache-parsed-version
Open

GH-3696: Cache ParsedVersion in FileMetaData to eliminate redundant parsing#3700
asifsmohammed wants to merge 2 commits into
apache:masterfrom
asifsmohammed:gh-3696-cache-parsed-version

Conversation

@asifsmohammed

@asifsmohammed asifsmohammed commented Aug 1, 2026

Copy link
Copy Markdown

Rationale for this change

VersionParser.parse(createdBy) is called from 7 production sites, all parsing the same constant string from FileMetaData.getCreatedBy(). Since FileMetaData is constructed once per file and already stores the createdBy string, it is the natural place to parse once and cache the result.

This allows callers that already hold a FileMetaData reference to use getWriterVersion() directly instead of re-parsing the createdBy string on every access.

What changes are included in this PR?

  • Add a transient ParsedVersion writerVersion field to FileMetaData
  • Parse createdBy once in the canonical constructor and cache the result
  • Expose via getWriterVersion() getter (annotated @JsonIgnore to preserve JSON serialization)
  • Add FileMetaDataTest with coverage for valid, null, empty, and unparseable version strings

Are these changes tested?

Yes. New FileMetaDataTest with 5 test cases.

Are there any user-facing changes?

No breaking changes. Adds a new public method FileMetaData.getWriterVersion() that downstream call sites can use to avoid redundant parsing.

Closes #3696

@wgtmac wgtmac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for fixing this! If this gets checked in, the PR that fixes malformed stats checking can be closed, right?

this.keyValueMetaData =
unmodifiableMap(Objects.requireNonNull(keyValueMetaData, "keyValueMetaData cannot be null"));
this.createdBy = createdBy;
this.writerVersion = parseVersion(createdBy);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seems reasonable. Do we want to initialize this field lazily?

@wgtmac wgtmac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for pushing this cleaner direction. I think caching the parsed writer version in FileMetaData is the right foundation, but this PR is not a complete fix yet. It currently adds the cache, but does not migrate the production call sites that still parse created_by repeatedly. Please update the hot/footer/page/reader/rewrite paths to consume the cached ParsedVersion, and cover that behavior in tests.

@@ -42,6 +44,7 @@ public enum EncryptionType {
private final MessageType schema;
private final Map<String, String> keyValueMetaData;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This cached field is transient final. After Java deserialization it will stay null even when createdBy is valid. Recompute lazily in getWriterVersion(), or add serialization handling and tests.

* @return the parsed writer version, or {@code null} if {@code createdBy} is null, empty, or unparseable
*/
@JsonIgnore
public ParsedVersion getWriterVersion() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This adds the cached value, but no production call site uses it yet. Please migrate the callers listed in the issue, especially footer stats, page stats, reader init, rewrite, and lazy encrypted metadata paths.

return VersionParser.parse(createdBy);
} catch (RuntimeException | VersionParser.VersionParseException e) {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Returning null loses the difference between missing createdBy and parse failure. CorruptStatistics currently logs different warnings for these cases. Preserve the parse failure state, or keep enough context for migrated callers to preserve existing behavior.

FileMetaData meta =
new FileMetaData(SCHEMA, Collections.emptyMap(), "parquet-mr version 1.12.0 (build abc123)");

assertThat(meta.getWriterVersion()).isNotNull();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These tests only cover the getter. They do not prove the repeated parsing problem is fixed. Please add coverage for at least one migrated production path using the cached ParsedVersion instead of reparsing createdBy.

@asifsmohammed
asifsmohammed force-pushed the gh-3696-cache-parsed-version branch from d7c4f6d to 2f01073 Compare August 2, 2026 16:59
…dant parsing

Parse the createdBy version string once during FileMetaData construction
and cache the result as a transient field. This avoids redundant
VersionParser.parse() calls at every downstream call site (R×C times
during footer decode alone).
@asifsmohammed
asifsmohammed force-pushed the gh-3696-cache-parsed-version branch from 2f01073 to bc23cf5 Compare August 2, 2026 17:08
- Change writerVersion to lazy computation on first getWriterVersion() call
- Fixes deserialization correctness (transient fields recompute from createdBy)
- Add writerVersionParsed flag to avoid retrying on parse failure
- Document contract for distinguishing missing vs. unparseable in javadoc
@asifsmohammed

Copy link
Copy Markdown
Author

This adds the cached value, but no production call site uses it yet. Please migrate the callers listed in the issue, especially footer stats, page stats, reader init, rewrite, and lazy encrypted metadata paths.

These tests only cover the getter. They do not prove the repeated parsing problem is fixed. Please add coverage for at least one migrated production path using the cached ParsedVersion instead of reparsing createdBy.

@wgtmac I'd prefer to keep this PR focused on the caching foundation and migrate callers in a follow-up. The migration touches multiple files which is a larger change that's easier to review separately. The follow-up will include tests proving the production path uses the cached version.
If needed I can add fixes for 1 caller using the ParsedVersion in this PR or I create a separate PR to fix all callers using ParsedVersion in parallel to this PR. Wdyt? I have no concerns on fixing all of them in this PR itself.

@wgtmac wgtmac left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the quick response!

I agree with keeping this PR small and moving the full migration to follow-ups. However, I do think it is worth migrating at least one production caller so this PR provides a concrete fix, not only an unused cache.

Please also narrow the title and description and remove “Closes #3696”, since the remaining paths still parse created_by repeatedly.

private final Map<String, String> keyValueMetaData;
private final String createdBy;
private transient volatile ParsedVersion writerVersion;
private transient volatile boolean writerVersionParsed;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please replace writerVersion and writerVersionParsed with one private immutable WriterVersionResult field. Use a null field only for “not initialized”. The result should represent valid, missing, or invalid and retain the original parse exception. Initialize it once in a synchronized block. Keep only getWriterVersion() public: return the parsed version, return null for missing createdBy, and rethrow the cached exception for invalid createdBy so callers can preserve the existing fallback without reparsing.

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.

Cache ParsedVersion in FileMetaData to eliminate redundant created_by parsing

2 participants