Repository navigation
YARN-12012. Collect Capacity Scheduler application ordering policy parameters without copying the configuration - #8800
Conversation
…rameters without copying the configuration CapacitySchedulerConfiguration.getAppOrderingPolicy iterated the whole configuration once per leaf queue through Configuration.iterator(), which copies every property into a new map, so building Q leaf queues copied the N properties Q times. The parameters are now collected from the properties directly, under their lock, with the filter that Configuration.iterator() applies, so each policy receives the same map. This is independent of the configuration resolver series that follows, and makes it possible to benchmark that series against trunk with this change applied.
joseluisll
left a comment
There was a problem hiding this comment.
Thanks, the change looks correct to me. I compared it with Configuration.iterator(): same String-only filter, same lock on props, same raw values with no variable expansion. So OrderingPolicy.configure() gets the same map as before, and configure() still runs outside the lock.
Yetus is green apart from test4tests (-1). That covers compile on JDK 17/21, checkstyle (the patch removes one warning), spotbugs, javadoc, and the resourcemanager unit tests (3405 run, 0 failures). The one flake, TestCapacitySchedulerSchedulingRequestUpdate#testNodePartitionPendingResourceUpdate, fails in CSMaxRunningAppsEnforcer.untrackRunnableApp, which looks unrelated to this change.
A few questions and nits are inline. The main ones are about how big the gain is, and whether the per-queue scan could be removed altogether.
Generated by Claude Code
| config.put(kv.getKey().substring(confPrefix.length()), kv.getValue()); | ||
| Properties props = getProps(); | ||
| synchronized (props) { | ||
| for (Map.Entry<Object, Object> kv : props.entrySet()) { |
There was a problem hiding this comment.
This removes the per-queue copy, but each leaf queue still scans every property, so it's still Q×N entry visits and only the constant factor changes. Could you add your before/after numbers (Q, N, time or allocations) to the JIRA? Is the remaining O(Q·N) cost something the resolver series will remove?
Generated by Claude Code
There was a problem hiding this comment.
Yes, this only removes the per-queue copy; each leaf queue still scans all properties, so it is still O(Q·N) with a smaller constant.
I'll add the numbers to the JIRA. With 1000 queues (medians of 3 interleaved forks), trunk vs. trunk with this patch: load 167 → 124 ms, refresh 227 → 162 ms.
And yes, the resolver series will remove the remaining scan: from YARN-12016 on, leaf queues get their ordering policy parameters from the resolver, which reads them through a prefix index built once per configuration (ConfigSnapshot). I just didn't want to create the PR yet, as it would contain this commit as well.
| config.put(kv.getKey().substring(confPrefix.length()), kv.getValue()); | ||
| Properties props = getProps(); | ||
| synchronized (props) { | ||
| for (Map.Entry<Object, Object> kv : props.entrySet()) { |
There was a problem hiding this comment.
getConfiguredNodeLabels(QueuePath) has the same scan-per-queue pattern. It was solved by building ConfiguredNodeLabels once per configuration, and QueueNodeLabelsSettings only falls back to the per-queue scan when that's missing. Could the *.ordering-policy.* parameters be collected for all queues in one pass at reinit? That would make it O(N).
Generated by Claude Code
There was a problem hiding this comment.
Agreed that one pass is the right shape, and that is what YARN-12016 does: the configuration is indexed by key prefix once, and every queue's ordering policy parameters come from that index.
I'd rather not add a second mechanism like ConfiguredNodeLabels here only for it to be replaced later on. This patch is meant to stay minimal so it also works as the benchmark baseline for the series.
| for (Map.Entry<String, String> kv : this) { | ||
| if (kv.getKey().startsWith(confPrefix)) { | ||
| config.put(kv.getKey().substring(confPrefix.length()), kv.getValue()); | ||
| Properties props = getProps(); |
There was a problem hiding this comment.
ConfigurationProperties already indexes properties by prefix (it's used in getConfiguredNodeLabelsByQueue()). My guess is you avoided it because set() doesn't invalidate that cache, and TestLeafQueue#testFairConfiguration calls set() and then getAppOrderingPolicy again. If that's the reason, it's worth a line in a code comment here.
Generated by Claude Code
There was a problem hiding this comment.
Yep, that is the reason: set() does not invalidate the cached ConfigurationProperties, and TestLeafQueue#testFairConfiguration depends on set() followed by getAppOrderingPolicy. It also casts the properties to Map<String, String> without filtering non-String entries. Added code comments.
| for (Map.Entry<String, String> kv : this) { | ||
| if (kv.getKey().startsWith(confPrefix)) { | ||
| config.put(kv.getKey().substring(confPrefix.length()), kv.getValue()); | ||
| Properties props = getProps(); |
There was a problem hiding this comment.
The commit message explains why this doesn't iterate over this, but the code doesn't, so someone could "simplify" it back. Something like:
// Avoid Configuration.iterator(), which copies all properties on each call;
// same filter and lock as iterator().Generated by Claude Code
| Properties props = getProps(); | ||
| synchronized (props) { | ||
| for (Map.Entry<Object, Object> kv : props.entrySet()) { | ||
| if (kv.getKey() instanceof String && kv.getValue() instanceof String |
There was a problem hiding this comment.
Yetus voted -1 on test4tests. Since this is meant not to change behaviour, it's fine either to say in the PR that TestLeafQueue#testFairConfiguration already covers this path, or to add a small test. The test could put a non-String value into the configuration's Properties and check that it's skipped, which pins down the "same filter as iterator()" promise.
Generated by Claude Code
There was a problem hiding this comment.
This is meant not to change behaviour, and the path is already covered by TestLeafQueue#testFairConfiguration, which sets ordering policy parameters with set() and reads them back through getAppOrderingPolicy. It passes along with the rest of the resourcemanager tests.
I'd skip the non-String test: getProps() is protected and the public Configuration API never stores non-String values, so the filter is only there to match iterator().
| if (kv.getKey().startsWith(confPrefix)) { | ||
| config.put(kv.getKey().substring(confPrefix.length()), kv.getValue()); | ||
| Properties props = getProps(); | ||
| synchronized (props) { |
There was a problem hiding this comment.
nit (design): This copies Configuration.iterator()'s internals (the non-String filter and the lock on props) into a YARN class, so the two could drift apart. A helper in Configuration that returns raw properties for a prefix would keep this logic in one place. The existing getPropsWithPrefix can't be swapped in directly, because it goes through get() (which expands variables) and doesn't take the lock. Fine to leave as is if you'd rather not touch hadoop-common.
Generated by Claude Code
There was a problem hiding this comment.
Agreed that a raw prefix accessor in Configuration would be cleaner, but I'd rather keep this patch YARN-only. Later in the series the queue build path reads from ConfigSnapshot instead, so this loop stops being the hot path.
| if (kv.getKey() instanceof String && kv.getValue() instanceof String | ||
| && ((String) kv.getKey()).startsWith(confPrefix)) { | ||
| config.put(((String) kv.getKey()).substring(confPrefix.length()), | ||
| (String) kv.getValue()); |
There was a problem hiding this comment.
nit: optional, but this does each cast once:
for (Map.Entry<Object, Object> kv : props.entrySet()) {
if (kv.getKey() instanceof String && kv.getValue() instanceof String) {
String key = (String) kv.getKey();
if (key.startsWith(confPrefix)) {
config.put(key.substring(confPrefix.length()), (String) kv.getValue());
}
}
}Generated by Claude Code
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Properties; |
There was a problem hiding this comment.
nit: java.util.Properties should go after java.util.Map.Entry. Checkstyle didn't flag it, so this is cosmetic only.
Generated by Claude Code
|
💔 -1 overall
This message was automatically generated. |
joseluisll
left a comment
There was a problem hiding this comment.
Thanks @brumi1024, all points are addressed, and I agree with keeping this minimal as the baseline for YARN-12016's one-pass index.
Please name TestLeafQueue#testFairConfiguration under "How was this patch tested?" to answer test4tests. +1 (non-binding).
|
Thanks @joseluisll. @slfan1989 can you please approve if you have the chance? |
slfan1989
left a comment
There was a problem hiding this comment.
LGTM.
Thanks for addressing the review comments and providing the benchmark results. The existing TestLeafQueue#testFairConfiguration coverage is sufficient for this behavior-preserving optimization. All build and test checks passed; the remaining Yetus -1 is only the test4tests warning, which is explained in the PR. +1.
|
Thanks @joseluisll and @slfan1989 for the reviews! Merging to trunk. |
Description of PR
CapacitySchedulerConfiguration.getAppOrderingPolicy iterated the whole configuration once per leaf queue through Configuration.iterator(), which copies every property into a new map, so building Q leaf queues copied the N properties Q times. The parameters are now collected from the properties directly, under their lock, with the filter that Configuration.iterator() applies, so each policy receives the same map.
This is independent of the configuration resolver series that follows, and makes it possible to benchmark that series against trunk with this change applied.
For more details see: YARN-12010
How was this patch tested?
TestLeafQueue#testFairConfiguration
For code changes:
declared according to the connector-specific documentation? Note: Automated CI
testing doesn't cover all cases so manual testing with cloud storage is still
required.
LICENSE,LICENSE-binary,NOTICE-binaryfiles?AI Tooling
If an AI tool was used:
where is the name of the AI tool used.
https://www.apache.org/legal/generative-tooling.html
Contains content generated by Claude Code