Skip to content

getMaximumPropertyLength should degrade gracefully instead of throwing#6328

Open
dkayiwa wants to merge 1 commit into
openmrs:masterfrom
dkayiwa:fix-getmaximumpropertylength-robust
Open

getMaximumPropertyLength should degrade gracefully instead of throwing#6328
dkayiwa wants to merge 1 commit into
openmrs:masterfrom
dkayiwa:fix-getmaximumpropertylength-robust

Conversation

@dkayiwa

@dkayiwa dkayiwa commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

HibernateAdministrationDAO.getMaximumPropertyLength(aClass, fieldName) throws an APIException when metadata.getEntityBinding(aClass.getName()) returns null. That metadata is a boot-time Hibernate Metadata snapshot captured once in setApplicationContext, so if it is ever consulted in an incomplete state the method throws hard.

This method is called by validate() on every OpenmrsObject save. Two things show the hard throw is an inconsistent outlier:

  • validate() already guards every use with if (maxLength >= 0 && ...), i.e. a negative return means "length unknown, skip the check".
  • The method itself already returns -1 when it cannot determine a column's length (the catch block).

So a transient inability to resolve an entity should behave the same way, -1, not throw and fail the save.

How it surfaced

Intermittent CI failure: HibernateAdministrationDAOTest.getMaximumPropertyLength_* errored with Couldn't find a class in the hibernate configuration named: org.openmrs.event.outbox.OutboxEvent in some full-suite orderings, while passing in isolation. The throw is the sharp edge; the underlying "the boot Metadata snapshot sometimes can't resolve an annotation-scanned entity" is what varies by test order.

Change

Return -1 (unknown length) instead of throwing when the entity binding cannot be resolved, consistent with the existing sentinel and with validate()'s maxLength >= 0 guard. Adds a regression test asserting an unmapped class yields -1 rather than an exception (it errors on the pre-change code with the same APIException, and passes after).

Scope / what this does and doesn't do

This hardens the production validate() path so a transient metadata gap can no longer fail a save, and removes the exception that was erroring the test. It does not, on its own, make assertions that require a specific entity's column lengths deterministic; a test that asserts exact lengths still needs that entity registered. Verified: the class passes (5/5) with the change and spotless is clean; the new test errors on the pre-change code and passes after.

HibernateAdministrationDAO.getMaximumPropertyLength threw an APIException when
metadata.getEntityBinding(...) returned null. It looks the class up in a boot
Metadata snapshot captured once at context init, so if that snapshot is ever
consulted in an incomplete state the method throws. This is called from
validate() on every save, and validate() already skips the length check when
the value is negative (maxLength >= 0), and the method itself already returns
-1 when it cannot determine a column's length. So the hard throw on a missing
binding is an inconsistent outlier that can fail a save.

It surfaced as an intermittent test failure: getMaximumPropertyLength_* errored
with "Couldn't find a class in the hibernate configuration named: ..." in some
full-suite orderings while passing in isolation.

Return -1 (unknown length) instead of throwing, consistent with the existing
sentinel and validate()'s guard, and add a regression test asserting an
unmapped class yields -1 rather than an exception.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.47059% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.43%. Comparing base (d8fe136) to head (5f7815c).

Files with missing lines Patch % Lines
...s/api/db/hibernate/HibernateAdministrationDAO.java 76.47% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #6328      +/-   ##
============================================
- Coverage     59.47%   59.43%   -0.04%     
+ Complexity     9541     9538       -3     
============================================
  Files           731      731              
  Lines         38323    38325       +2     
  Branches       5587     5587              
============================================
- Hits          22794    22780      -14     
- Misses        13489    13516      +27     
+ Partials       2040     2029      -11     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

// every save, and validate() already skips the length check when it returns a negative value.
log.debug("Could not find a hibernate entity binding for {}; treating maximum length as unknown",
aClass.getName());
return -1;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since this changes what AdministrationService#getMaximumPropertyLength observably does for a class with no entity binding (it used to throw APIException, now it returns -1), it would be worth updating the @return javadoc on the service interface to document the sentinel, something like @return the max field length of a property, or -1 if it cannot be determined. Both in-core consumers already key off it (validate() checks maxLength >= 0 and ValidateUtil.validateFieldLengths checks length == -1), but a module author calling the service has no way to discover it without reading this DAO.

Comment on lines +228 to +231
// The boot Metadata is captured once at startup, so it can fail to resolve an entity if it is
// ever consulted in an incomplete state (for example after a session factory rebuild). Treat
// the length as unknown (-1) instead of throwing: this method is called from validate() on
// every save, and validate() already skips the length check when it returns a negative value.

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.

Isn't this comment somewhat inaccurate? The metadata should be captured at session start time, but that's not the same as "boot" time (e.g., when a module refresh happens. I also don't think it adds much value that can't be gleaned from the log statement.

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.

3 participants