Skip to content

Commit da56473

Browse files
committed
Implement extra resources for test actions
Adds support for specifying extra resources available for the bazel invocation and requirement for test actions. Using --local_extra_resources=<resource name>=<amount of resource> one can specify the resources available for tests. Specifying a tag "resources:<resource name>:<amount of resources>" for a test action will tell bazel how much of the said resource this test will require. Bazel will then limit concurrently running test actions using the same resource if there isn't enough of said resource available.
1 parent 7a11752 commit da56473

15 files changed

+212
-30
lines changed

src/main/java/com/google/devtools/build/lib/actions/ExecutionRequirements.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,35 @@ public String parseIfMatches(String tag) throws ValidationException {
152152
return null;
153153
});
154154

155+
/** How many extra resources an action requires for execution. */
156+
public static final ParseableRequirement RESOURCES =
157+
ParseableRequirement.create(
158+
"resources:<str>:<int>",
159+
Pattern.compile("resources:(.+:.+)"),
160+
s -> {
161+
Preconditions.checkNotNull(s);
162+
163+
int splitIndex = s.indexOf(":");
164+
String resourceCount = s.substring(splitIndex+1);
165+
int value;
166+
try {
167+
value = Integer.parseInt(resourceCount);
168+
} catch (NumberFormatException e) {
169+
return "can't be parsed as an integer";
170+
}
171+
172+
// De-and-reserialize & compare to only allow canonical integer formats.
173+
if (!Integer.toString(value).equals(resourceCount)) {
174+
return "must be in canonical format (e.g. '4' instead of '+04')";
175+
}
176+
177+
if (value < 1) {
178+
return "can't be zero or negative";
179+
}
180+
181+
return null;
182+
});
183+
155184
/** If an action supports running in persistent worker mode. */
156185
public static final String SUPPORTS_WORKERS = "supports-workers";
157186

src/main/java/com/google/devtools/build/lib/actions/LocalHostResourceFallback.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class LocalHostResourceFallback {
2727
ResourceSet.create(
2828
3.0 * (Runtime.getRuntime().maxMemory() >> 20),
2929
Runtime.getRuntime().availableProcessors(),
30+
null,
3031
Integer.MAX_VALUE);
3132

3233
public static ResourceSet getLocalHostResources() {

src/main/java/com/google/devtools/build/lib/actions/LocalHostResourceManagerDarwin.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static ResourceSet getLocalHostResources() {
4444
return ResourceSet.create(
4545
ramMb,
4646
logicalCpuCount,
47+
null,
4748
Integer.MAX_VALUE);
4849
} catch (IOException e) {
4950
return null;

src/main/java/com/google/devtools/build/lib/actions/LocalHostResourceManagerLinux.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static ResourceSet getLocalHostResources() {
4848
return ResourceSet.create(
4949
ramMb,
5050
logicalCpuCount,
51+
null,
5152
Integer.MAX_VALUE);
5253
} catch (IOException e) {
5354
return null;

src/main/java/com/google/devtools/build/lib/actions/ResourceManager.java

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@
2525
import com.google.devtools.build.lib.util.OS;
2626
import com.google.devtools.build.lib.util.Pair;
2727
import java.io.IOException;
28+
import java.util.HashMap;
29+
import java.util.HashSet;
2830
import java.util.Iterator;
2931
import java.util.LinkedList;
3032
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Set;
3135
import java.util.concurrent.CountDownLatch;
3236

3337
/**
@@ -137,6 +141,10 @@ public static ResourceManager instance() {
137141
// definition in the ResourceSet class.
138142
private double usedRam;
139143

144+
// Used amount of extra resources. Corresponds to the extra resource
145+
// definition in the ResourceSet class.
146+
private Map<String, Float> usedExtraResources;
147+
140148
// Used local test count. Corresponds to the local test count definition in the ResourceSet class.
141149
private int usedLocalTestCount;
142150

@@ -145,6 +153,7 @@ public static ResourceManager instance() {
145153

146154
private ResourceManager() {
147155
requestList = new LinkedList<>();
156+
usedExtraResources = new HashMap<>();
148157
}
149158

150159
@VisibleForTesting public static ResourceManager instanceForTestingOnly() {
@@ -158,6 +167,7 @@ private ResourceManager() {
158167
public synchronized void resetResourceUsage() {
159168
usedCpu = 0;
160169
usedRam = 0;
170+
usedExtraResources = new HashMap<>();
161171
usedLocalTestCount = 0;
162172
for (Pair<ResourceSet, CountDownLatch> request : requestList) {
163173
// CountDownLatch can be set only to 0 or 1.
@@ -177,6 +187,7 @@ public synchronized void setAvailableResources(ResourceSet resources) {
177187
ResourceSet.create(
178188
staticResources.getMemoryMb(),
179189
staticResources.getCpuUsage(),
190+
staticResources.getExtraResourceUsage(),
180191
staticResources.getLocalTestCount());
181192
processWaitingThreads();
182193
}
@@ -264,14 +275,24 @@ ResourceHandle tryAcquire(ActionExecutionMetadata owner, ResourceSet resources)
264275
private void incrementResources(ResourceSet resources) {
265276
usedCpu += resources.getCpuUsage();
266277
usedRam += resources.getMemoryMb();
278+
279+
for (Map.Entry<String, Float> resource : resources.getExtraResourceUsage().entrySet()) {
280+
String key = (String)resource.getKey();
281+
float value = resource.getValue();
282+
if (usedExtraResources.containsKey(key)) {
283+
value += (float)usedExtraResources.get(key);
284+
}
285+
usedExtraResources.put(key, value);
286+
}
287+
267288
usedLocalTestCount += resources.getLocalTestCount();
268289
}
269290

270291
/**
271292
* Return true if any resources have been claimed through this manager.
272293
*/
273294
public synchronized boolean inUse() {
274-
return usedCpu != 0.0 || usedRam != 0.0 || usedLocalTestCount != 0 || !requestList.isEmpty();
295+
return usedCpu != 0.0 || usedRam != 0.0 || !usedExtraResources.isEmpty() || usedLocalTestCount != 0 || !requestList.isEmpty();
275296
}
276297

277298

@@ -322,6 +343,13 @@ private synchronized CountDownLatch acquire(ResourceSet resources) {
322343
private synchronized boolean release(ResourceSet resources) {
323344
usedCpu -= resources.getCpuUsage();
324345
usedRam -= resources.getMemoryMb();
346+
347+
for (Map.Entry<String, Float> resource : resources.getExtraResourceUsage().entrySet()) {
348+
String key = (String)resource.getKey();
349+
float value = (float)usedExtraResources.get(key) - resource.getValue();
350+
usedExtraResources.put(key, value);
351+
}
352+
325353
usedLocalTestCount -= resources.getLocalTestCount();
326354

327355
// TODO(bazel-team): (2010) rounding error can accumulate and value below can end up being
@@ -333,6 +361,19 @@ private synchronized boolean release(ResourceSet resources) {
333361
if (usedRam < epsilon) {
334362
usedRam = 0;
335363
}
364+
365+
Set<String> toRemove = new HashSet<>();
366+
for (Map.Entry<String, Float> resource : usedExtraResources.entrySet()) {
367+
String key = (String)resource.getKey();
368+
float value = (float)usedExtraResources.get(key);
369+
if (value < epsilon) {
370+
toRemove.add(key);
371+
}
372+
}
373+
for (String key : toRemove) {
374+
usedExtraResources.remove(key);
375+
}
376+
336377
if (!requestList.isEmpty()) {
337378
processWaitingThreads();
338379
return true;
@@ -365,7 +406,7 @@ private boolean areResourcesAvailable(ResourceSet resources) {
365406
Preconditions.checkNotNull(availableResources);
366407
// Comparison below is robust, since any calculation errors will be fixed
367408
// by the release() method.
368-
if (usedCpu == 0.0 && usedRam == 0.0 && usedLocalTestCount == 0) {
409+
if (usedCpu == 0.0 && usedRam == 0.0 && usedExtraResources.isEmpty() && usedLocalTestCount == 0) {
369410
return true;
370411
}
371412
// Use only MIN_NECESSARY_???_RATIO of the resource value to check for
@@ -409,9 +450,19 @@ private boolean areResourcesAvailable(ResourceSet resources) {
409450
// 3) If used resource amount is less than total available resource amount.
410451
boolean cpuIsAvailable = cpu == 0.0 || usedCpu == 0.0 || usedCpu + cpu <= availableCpu;
411452
boolean ramIsAvailable = ram == 0.0 || usedRam == 0.0 || ram <= remainingRam;
453+
boolean extraResourcesIsAvailable = true;
454+
for (Map.Entry<String, Float> resource : resources.getExtraResourceUsage().entrySet()) {
455+
String key = (String)resource.getKey();
456+
float used = (float)usedExtraResources.getOrDefault(key, 0f);
457+
float requested = resource.getValue();
458+
float available = (float)availableResources.getExtraResourceUsage().getOrDefault(key, 0f);
459+
if (requested != 0.0 && used != 0.0 && requested + used > available) {
460+
extraResourcesIsAvailable = false;
461+
}
462+
}
412463
boolean localTestCountIsAvailable = localTestCount == 0 || usedLocalTestCount == 0
413464
|| usedLocalTestCount + localTestCount <= availableLocalTestCount;
414-
return cpuIsAvailable && ramIsAvailable && localTestCountIsAvailable;
465+
return cpuIsAvailable && ramIsAvailable && extraResourcesIsAvailable && localTestCountIsAvailable;
415466
}
416467

417468
@VisibleForTesting
@@ -421,6 +472,6 @@ synchronized int getWaitCount() {
421472

422473
@VisibleForTesting
423474
synchronized boolean isAvailable(double ram, double cpu, int localTestCount) {
424-
return areResourcesAvailable(ResourceSet.create(ram, cpu, localTestCount));
475+
return areResourcesAvailable(ResourceSet.create(ram, cpu, null, localTestCount));
425476
}
426477
}

src/main/java/com/google/devtools/build/lib/actions/ResourceSet.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
2020
import com.google.devtools.common.options.Converter;
2121
import com.google.devtools.common.options.OptionsParsingException;
22+
import java.util.HashMap;
2223
import java.util.Iterator;
24+
import java.util.List;
25+
import java.util.Map;
2326
import java.util.NoSuchElementException;
2427

2528
/**
@@ -41,12 +44,24 @@ public class ResourceSet {
4144
/** The number of CPUs, or fractions thereof. */
4245
private final double cpuUsage;
4346

47+
/** Map of extra resources mapping name of the resource to a value. */
48+
private final Map<String, Float> extraResourceUsage;
49+
4450
/** The number of local tests. */
4551
private final int localTestCount;
4652

4753
private ResourceSet(double memoryMb, double cpuUsage, int localTestCount) {
54+
this(memoryMb, cpuUsage, null, localTestCount);
55+
}
56+
57+
private ResourceSet(double memoryMb, double cpuUsage, Map<String, Float> extraResourceUsage, int localTestCount) {
4858
this.memoryMb = memoryMb;
4959
this.cpuUsage = cpuUsage;
60+
if (extraResourceUsage == null) {
61+
this.extraResourceUsage = new HashMap<>();
62+
} else {
63+
this.extraResourceUsage = extraResourceUsage;
64+
}
5065
this.localTestCount = localTestCount;
5166
}
5267

@@ -79,11 +94,11 @@ public static ResourceSet createWithLocalTestCount(int localTestCount) {
7994
*/
8095
@AutoCodec.Instantiator
8196
public static ResourceSet create(
82-
double memoryMb, double cpuUsage, int localTestCount) {
83-
if (memoryMb == 0 && cpuUsage == 0 && localTestCount == 0) {
97+
double memoryMb, double cpuUsage, Map<String, Float> extraResourceUsage, int localTestCount) {
98+
if (memoryMb == 0 && cpuUsage == 0 && (extraResourceUsage == null || extraResourceUsage.size() == 0) && localTestCount == 0) {
8499
return ZERO;
85100
}
86-
return new ResourceSet(memoryMb, cpuUsage, localTestCount);
101+
return new ResourceSet(memoryMb, cpuUsage, extraResourceUsage, localTestCount);
87102
}
88103

89104
/** Returns the amount of real memory (resident set size) used in MB. */
@@ -103,16 +118,25 @@ public double getCpuUsage() {
103118
return cpuUsage;
104119
}
105120

121+
public Map<String, Float> getExtraResourceUsage() {
122+
return extraResourceUsage;
123+
}
124+
106125
/** Returns the local test count used. */
107126
public int getLocalTestCount() {
108127
return localTestCount;
109128
}
110129

111130
@Override
112131
public String toString() {
132+
StringBuilder sb = new StringBuilder();
133+
for (Map.Entry<String, Float> resource : extraResourceUsage.entrySet()) {
134+
sb.append(resource.getKey() + ": " + resource.getValue() + "\n");
135+
}
113136
return "Resources: \n"
114137
+ "Memory: " + memoryMb + "M\n"
115138
+ "CPU: " + cpuUsage + "\n"
139+
+ sb.toString()
116140
+ "Local tests: " + localTestCount + "\n";
117141
}
118142

@@ -135,7 +159,7 @@ public ResourceSet convert(String input) throws OptionsParsingException {
135159
if (memoryMb <= 0.0 || cpuUsage <= 0.0) {
136160
throw new OptionsParsingException("All resource values must be positive");
137161
}
138-
return create(memoryMb, cpuUsage, Integer.MAX_VALUE);
162+
return create(memoryMb, cpuUsage, null, Integer.MAX_VALUE);
139163
} catch (NumberFormatException | NoSuchElementException nfe) {
140164
throw new OptionsParsingException("Expected exactly 3 comma-separated float values", nfe);
141165
}

src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetProperties.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
3232
import com.google.devtools.build.lib.server.FailureDetails.TestAction;
3333
import com.google.devtools.build.lib.server.FailureDetails.TestAction.Code;
34+
import java.util.HashMap;
3435
import java.util.List;
3536
import java.util.Map;
3637

@@ -46,10 +47,10 @@ public class TestTargetProperties {
4647
* <p>When changing these values, remember to update the documentation at
4748
* attributes/test/size.html.
4849
*/
49-
private static final ResourceSet SMALL_RESOURCES = ResourceSet.create(20, 1, 1);
50-
private static final ResourceSet MEDIUM_RESOURCES = ResourceSet.create(100, 1, 1);
51-
private static final ResourceSet LARGE_RESOURCES = ResourceSet.create(300, 1, 1);
52-
private static final ResourceSet ENORMOUS_RESOURCES = ResourceSet.create(800, 1, 1);
50+
private static final ResourceSet SMALL_RESOURCES = ResourceSet.create(20, 1, null, 1);
51+
private static final ResourceSet MEDIUM_RESOURCES = ResourceSet.create(100, 1, null, 1);
52+
private static final ResourceSet LARGE_RESOURCES = ResourceSet.create(300, 1, null, 1);
53+
private static final ResourceSet ENORMOUS_RESOURCES = ResourceSet.create(800, 1, null, 1);
5354
private static final ResourceSet LOCAL_TEST_JOBS_BASED_RESOURCES =
5455
ResourceSet.createWithLocalTestCount(1);
5556

@@ -150,23 +151,21 @@ public ResourceSet getLocalResourceUsage(Label label, boolean usingLocalTestJobs
150151
ResourceSet testResourcesFromSize = TestTargetProperties.getResourceSetFromSize(size);
151152

152153
// Tests can override their CPU reservation with a "cpus:<n>" tag.
153-
ResourceSet testResourcesFromTag = null;
154+
// Tests can also specify requirements for extra resources using "resources:<resource name>:<n>" tag.
155+
double cpuCount = -1.0;
156+
Map<String, Float> extraResources = new HashMap<>();
154157
for (String tag : executionInfo.keySet()) {
155158
try {
156159
String cpus = ExecutionRequirements.CPU.parseIfMatches(tag);
157160
if (cpus != null) {
158-
if (testResourcesFromTag != null) {
161+
if (cpuCount != -1.0) {
159162
String message =
160163
String.format(
161164
"%s has more than one '%s' tag, but duplicate tags aren't allowed",
162165
label, ExecutionRequirements.CPU.userFriendlyName());
163166
throw new UserExecException(createFailureDetail(message, Code.DUPLICATE_CPU_TAGS));
164167
}
165-
testResourcesFromTag =
166-
ResourceSet.create(
167-
testResourcesFromSize.getMemoryMb(),
168-
Float.parseFloat(cpus),
169-
testResourcesFromSize.getLocalTestCount());
168+
cpuCount = Float.parseFloat(cpus);
170169
}
171170
} catch (ValidationException e) {
172171
String message =
@@ -178,9 +177,37 @@ public ResourceSet getLocalResourceUsage(Label label, boolean usingLocalTestJobs
178177
e.getMessage());
179178
throw new UserExecException(createFailureDetail(message, Code.INVALID_CPU_TAG));
180179
}
180+
try {
181+
String extras = ExecutionRequirements.RESOURCES.parseIfMatches(tag);
182+
if (extras != null) {
183+
int splitIndex = extras.indexOf(":");
184+
String resourceName = extras.substring(0, splitIndex);
185+
String resourceCount = extras.substring(splitIndex+1);
186+
if (extraResources.get(resourceName) != null) {
187+
String message =
188+
String.format(
189+
"%s has more than one '%s' tag, but duplicate tags aren't allowed",
190+
label, ExecutionRequirements.RESOURCES.userFriendlyName());
191+
throw new UserExecException(createFailureDetail(message, Code.DUPLICATE_CPU_TAGS));
192+
}
193+
extraResources.put(resourceName, Float.parseFloat(resourceCount));
194+
}
195+
} catch (ValidationException e) {
196+
String message =
197+
String.format(
198+
"%s has a '%s' tag, but its value '%s' didn't pass validation: %s",
199+
label,
200+
ExecutionRequirements.RESOURCES.userFriendlyName(),
201+
e.getTagValue(),
202+
e.getMessage());
203+
throw new UserExecException(createFailureDetail(message, Code.INVALID_CPU_TAG));
204+
}
181205
}
182-
183-
return testResourcesFromTag != null ? testResourcesFromTag : testResourcesFromSize;
206+
return ResourceSet.create(
207+
testResourcesFromSize.getMemoryMb(),
208+
cpuCount != -1.0 ? cpuCount : testResourcesFromSize.getCpuUsage(),
209+
extraResources,
210+
testResourcesFromSize.getLocalTestCount());
184211
}
185212

186213
private static FailureDetail createFailureDetail(String message, Code detailedCode) {

0 commit comments

Comments
 (0)