Repository navigation
MATH-1694: Fix racy Jacobi coefficient cache in PolynomialsUtils - #340
Conversation
BaseRuleFactory.getRule read and updated the pointsAndWeightsDouble TreeMap without holding the lock used by getRuleInternal. Factory instances are shared (IterativeLegendreGaussIntegrator keeps a static GaussIntegratorFactory), so concurrent requests for different rules could corrupt the map and leave callers spinning in TreeMap.getEntry. PolynomialsUtils.createJacobiPolynomial published the coefficient list for a new (v, w) key before seeding it and then read it outside the lock guarding the seeding, so a racing caller could extend a partial list and return a wrong polynomial. buildPolynomial also read the cached coefficients after releasing the list lock.
aherbert
left a comment
There was a problem hiding this comment.
The suggested updates will minimize locking duration for concurrent threads.
| list.add(BigFraction.of(v - w, 2)); | ||
| list.add(BigFraction.of(2 + v + w, 2)); | ||
| } | ||
| final List<BigFraction> coefficients; |
There was a problem hiding this comment.
Since this is using a Map this is more explicit in its support of concurrency using a ConcurrentHashMap:
JACOBI_COEFFICIENTS = new ConcurrentHashMap<>();
// ...
List<BigFraction> coefficients = JACOBI_COEFFICIENTS.computeIfAbsent(key, k -> {
// allocate a new list for v, w
final List<BigFraction> list = new ArrayList<>();
// Pv,w,0(x) = 1;
list.add(BigFraction.ONE);
// P1(x) = (v - w) / 2 + (2 + v + w) * X / 2
list.add(BigFraction.of(v - w, 2));
list.add(BigFraction.of(2 + v + w, 2));
return list;
});Using a ConcurrentHashMap will not lock the entire table allowing other concurrent threads to obtain coefficients that exist.
There was a problem hiding this comment.
Done, switched JACOBI_COEFFICIENTS to a ConcurrentHashMap and seed the list inside computeIfAbsent. Lookups for existing keys no longer contend with a new key being seeded. The HashMap import went with it.
| // build the polynomial | ||
| return new PolynomialFunction(a); | ||
| // build the polynomial | ||
| return new PolynomialFunction(a); |
There was a problem hiding this comment.
building the polynomial does not have to be synchronized once the coefficients to the required degreee are created:
final double[] a;
synchronized (coefficients) {
// ...
}
// build the polynomial
return new PolynomialFunction(a);There was a problem hiding this comment.
Agreed, only the growth and the copy into the double[] need the list lock. Moved the PolynomialFunction construction outside the synchronized block. PolynomialsUtilsTest, including the concurrent test, still passes.
|
Thanks for the modifications. Note that these fixes now have associated Jira tickets: MATH-1693 (BaseRuleFactory) and MATH-1694 (PolynomialUtils). Can you remove the modification of BaseRuleFactory in this PR so it corresponds only to MATH-1694. Then create a new PR for MATH-1693 with the single method change to use |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #340 +/- ##
============================================
+ Coverage 86.54% 87.22% +0.68%
+ Complexity 9787 91 -9696
============================================
Files 532 499 -33
Lines 35516 33530 -1986
Branches 6194 5851 -343
============================================
- Hits 30738 29248 -1490
+ Misses 3518 3164 -354
+ Partials 1260 1118 -142 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
https://issues.apache.org/jira/browse/MATH-1694
PolynomialsUtils.createJacobiPolynomialraces on its static coefficient cache. Its double-checked locking on a plain HashMap publishes the list for a new(v, w)before seeding it, andbuildPolynomialthen locks the list rather thanJACOBI_COEFFICIENTS, so a racing caller extends a partially seeded list. With 16 threads per fresh key, 335 of 32000 calls returned a wrong polynomial (P_3^(1,0)(1) = 5.8, expected 4) and 29 threw.JACOBI_COEFFICIENTSis now aConcurrentHashMapand the list is seeded insidecomputeIfAbsent, so it's only published once complete and lookups of existing keys don't contend.buildPolynomialcopies the coefficients into thedouble[]before releasing the list lock, as another thread may be growing the same ArrayList; thePolynomialFunctionis built outside the lock.testJacobiConcurrentCreationfailed in 3 of 3 runs on master and passes with the patch.The
BaseRuleFactory.getRulerace is tracked separately as MATH-1693.