64
64
import com .google .devtools .build .skyframe .SkyValue ;
65
65
import com .google .devtools .build .skyframe .ValueOrException ;
66
66
import java .util .HashMap ;
67
+ import java .util .HashSet ;
67
68
import java .util .LinkedHashSet ;
68
69
import java .util .List ;
69
70
import java .util .Map ;
@@ -213,7 +214,7 @@ public SkyValue compute(SkyKey skyKey, Environment env)
213
214
* trying to resolve its top-level {@code load()} statements. Although this work proceeds in a
214
215
* single thread, multiple calling contexts may evaluate .bzls in parallel. To avoid redundant
215
216
* work, they share a single (global to this Skyfunction instance) cache in lieu of the regular
216
- * Skyframe cache.
217
+ * Skyframe cache. Unlike the regular Skyframe cache, this cache stores only successes.
217
218
*
218
219
* <p>If two calling contexts race to compute the same .bzl, each one will see a different copy of
219
220
* it, and only one will end up in the shared cache. This presents a hazard: Suppose A and B both
@@ -224,24 +225,35 @@ public SkyValue compute(SkyKey skyKey, Environment env)
224
225
* weren't concerned about racing, A may also reevaluate previously computed items due to cache
225
226
* evictions.
226
227
*
227
- * <p>To solve this, we keep a second cache, {@link InliningState#visitedBzls}, that is local to
228
- * the current calling context, and which never evicts entries. This cache is always checked in
229
- * preference to the shared one; it may deviate from the shared one in some of its entries, but
230
- * the calling context won't know the difference. (If bzl inlining is only used for the loading
231
- * phase, we don't need to worry about Starlark values from different packages interacting.) The
232
- * cache is stored as part of the {@code inliningState} passed in by the caller; the caller can
233
- * obtain this object using {@link InliningState#create}.
228
+ * <p>To solve this, we keep a second cache, {@link InliningState#successfulLoads}, that is local
229
+ * to the current calling context, and which never evicts entries. Like the global cache discussed
230
+ * above, this cache stores only successes. This cache is always checked in preference to the
231
+ * shared one; it may deviate from the shared one in some of its entries, but the calling context
232
+ * won't know the difference. (Since bzl inlining is only used for the loading phase, we don't
233
+ * need to worry about Starlark values from different packages interacting.) The cache is stored
234
+ * as part of the {@code inliningState} passed in by the caller; the caller can obtain this object
235
+ * using {@link InliningState#create}.
234
236
*
235
- * <p>As an aside, note that we can't avoid having a second cache by simply naively blocking
236
- * evaluation of .bzls on retrievals from the shared cache. This is because two contexts could
237
- * deadlock while trying to evaluate an illegal {@code load()} cycle from opposite ends. It would
238
- * be possible to construct a waits-for graph and perform cycle detection, or to monitor slow
239
- * threads and do detection lazily, but these do not address the cache eviction issue.
240
- * Alternatively, we could make Starlark tolerant of reloading, but that would be tantamount to
241
- * implementing full Starlark serialization.
237
+ * <p>As an aside, note that we can't avoid having {@link InliningState#successfulLoads} by simply
238
+ * naively blocking evaluation of .bzls on retrievals from the shared cache. This is because two
239
+ * contexts could deadlock while trying to evaluate an illegal {@code load()} cycle from opposite
240
+ * ends. It would be possible to construct a waits-for graph and perform cycle detection, or to
241
+ * monitor slow threads and do detection lazily, but these do not address the cache eviction
242
+ * issue. Alternatively, we could make Starlark tolerant of reloading, but that would be
243
+ * tantamount to implementing full Starlark serialization.
242
244
*
243
- * @return the requested {@code BzlLoadValue}, or null if there is a Skyframe restart or error
245
+ * <p>Since our local {@link InliningState#successfulLoads} stores only successes, a separate
246
+ * concern is that we don't want to unsuccessfully visit the same .bzl more than once in the same
247
+ * context. (A visitation is unsuccessful if it fails due to an error or if it cannot complete
248
+ * because of a missing Skyframe dep.) To address this concern we maintain a separate {@link
249
+ * InliningState#unsuccessfulLoads} set, and use this set to return null instead of duplicating an
250
+ * unsuccessful visitation.
251
+ *
252
+ * @return the requested {@code BzlLoadValue}, or null if there was a missing Skyframe dep, an
253
+ * unspecified exception in a Skyframe dep request, or if this was a duplicate unsuccessful
254
+ * visitation
244
255
*/
256
+ // TODO(brandjon): Pick one of the nouns "load" and "bzl" and use that term consistently.
245
257
@ Nullable
246
258
BzlLoadValue computeInline (BzlLoadValue .Key key , Environment env , InliningState inliningState )
247
259
throws InconsistentFilesystemException , BzlLoadFailedException , InterruptedException {
@@ -253,15 +265,17 @@ BzlLoadValue computeInline(BzlLoadValue.Key key, Environment env, InliningState
253
265
}
254
266
255
267
/**
256
- * Retrieves or creates the requested {@link CachedBzlLoadData} object, entering it into the
257
- * local and shared caches. This is the entry point for recursive calls to the inline code path.
268
+ * Retrieves or creates the requested {@link CachedBzlLoadData} object for the given bzl, entering
269
+ * it into the local and shared caches. This is the entry point for recursive calls to the inline
270
+ * code path.
258
271
*
259
272
* <p>Skyframe calls made underneath this function will be logged in the resulting {@code
260
273
* CachedBzlLoadData) (or its transitive dependencies). The given Skyframe environment must not
261
274
* be a {@link RecordingSkyFunctionEnvironment}, since that would imply that calls are being
262
275
* logged in both the returned value and the parent value.
263
276
*
264
- * <p>Returns null on Skyframe restart or error.
277
+ * @return null if there was a missing Skyframe dep, an unspecified exception in a Skyframe dep
278
+ * request, or if this was a duplicate unsuccessful visitation
265
279
*/
266
280
@ Nullable
267
281
private CachedBzlLoadData computeInlineCachedData (
@@ -270,28 +284,42 @@ private CachedBzlLoadData computeInlineCachedData(
270
284
// Note to refactorors: No Skyframe calls may be made before the RecordingSkyFunctionEnvironment
271
285
// is set up below in computeInlineForCacheMiss.
272
286
273
- // Try the caches. We must try the thread-local cache before the shared one.
274
- CachedBzlLoadData cachedData = inliningState .visitedBzls .get (key );
287
+ // Try the caches of successful loads. We must try the thread-local cache before the shared, for
288
+ // consistency purposes (see the javadoc of #computeInline).
289
+ CachedBzlLoadData cachedData = inliningState .successfulLoads .get (key );
275
290
if (cachedData == null ) {
276
291
cachedData = cachedBzlLoadDataManager .cache .getIfPresent (key );
277
292
if (cachedData != null ) {
278
293
// Found a cache hit from another thread's computation; register the recorded deps as if our
279
294
// thread required them for the current key. Incorporate into visitedBzls any transitive
280
295
// cache hits it does not already contain.
281
- cachedData .traverse (env ::registerDependencies , inliningState .visitedBzls );
296
+ cachedData .traverse (env ::registerDependencies , inliningState .successfulLoads );
282
297
}
283
298
}
284
299
285
- // If that didn't work, compute it ourselves and add to the caches on success.
300
+ // See if we've already unsuccessfully visited the bzl.
301
+ if (inliningState .unsuccessfulLoads .contains (key )) {
302
+ return null ;
303
+ }
304
+
305
+ // If we're here, the bzl must have never been visited before in this calling context. Compute
306
+ // it ourselves, updating the other data structures as appropriate.
286
307
if (cachedData == null ) {
287
- cachedData = computeInlineForCacheMiss (key , env , inliningState );
288
- if (cachedData != null ) {
289
- inliningState .visitedBzls .put (key , cachedData );
290
- cachedBzlLoadDataManager .cache .put (key , cachedData );
308
+ try {
309
+ cachedData = computeInlineForCacheMiss (key , env , inliningState );
310
+ } finally {
311
+ if (cachedData != null ) {
312
+ inliningState .successfulLoads .put (key , cachedData );
313
+ cachedBzlLoadDataManager .cache .put (key , cachedData );
314
+ } else {
315
+ inliningState .unsuccessfulLoads .add (key );
316
+ // Either propagate an exception or fall through for null return.
317
+ }
291
318
}
292
319
}
293
320
294
- // On success, notify the parent CachedBzlLoadData of its new child.
321
+ // On success (from cache hit or from scratch), notify the parent CachedBzlLoadData of its new
322
+ // child.
295
323
if (cachedData != null ) {
296
324
inliningState .childCachedDataHandler .accept (cachedData );
297
325
}
@@ -429,28 +457,50 @@ private static ContainingPackageLookupValue getContainingPackageLookupValue(
429
457
*/
430
458
static class InliningState {
431
459
/**
432
- * The set of .bzls we're currently in the process of loading. This is used for cycle detection
433
- * since we don't have the benefit of Skyframe's internal cycle detection. The set must use
434
- * insertion order for correct error reporting.
460
+ * The set of bzls we're currently in the process of loading but haven't fully visited yet. This
461
+ * is used for cycle detection since we don't have the benefit of Skyframe's internal cycle
462
+ * detection. The set must use insertion order for correct error reporting.
463
+ *
464
+ * <p>This is disjoint with {@link #successfulLoads} and {@link #unsuccessfulLoads}.
465
+ *
466
+ * <p>This is local to current calling context. See {@link #computeInline}.
435
467
*/
436
468
// Keyed on the SkyKey, not the label, since label could theoretically be ambiguous, even though
437
469
// in practice keys from BUILD / WORKSPACE / @builtins don't call each other. (Not sure if
438
470
// WORKSPACE chunking can cause duplicate labels to appear, but we're robust regardless.)
439
471
private final LinkedHashSet <BzlLoadValue .Key > loadStack ;
440
472
473
+ /**
474
+ * Cache of bzls that have been fully visited and successfully loaded to a value.
475
+ *
476
+ * <p>This and {@link #unsuccessfulLoads} partition the set of fully visited bzls.
477
+ *
478
+ * <p>This is local to current calling context. See {@link #computeInline}.
479
+ */
480
+ private final Map <BzlLoadValue .Key , CachedBzlLoadData > successfulLoads ;
481
+
482
+ /**
483
+ * Set of bzls that have been fully visited, but were not successfully loaded to a value.
484
+ *
485
+ * <p>This and {@link #successfulLoads} partition the set of fully visited bzls, and is disjoint
486
+ * with {@link #loadStack}.
487
+ *
488
+ * <p>This is local to current calling context. See {@link #computeInline}.
489
+ */
490
+ private final HashSet <BzlLoadValue .Key > unsuccessfulLoads ;
491
+
441
492
/** Called when a transitive {@code CachedBzlLoadData} is produced. */
442
493
private final Consumer <CachedBzlLoadData > childCachedDataHandler ;
443
494
444
- /** Cache local to current calling context. See {@link #computeInline}. */
445
- private final Map <BzlLoadValue .Key , CachedBzlLoadData > visitedBzls ;
446
-
447
495
private InliningState (
448
496
LinkedHashSet <BzlLoadValue .Key > loadStack ,
449
- Consumer <CachedBzlLoadData > childCachedDataHandler ,
450
- Map <BzlLoadValue .Key , CachedBzlLoadData > visitedBzls ) {
497
+ Map <BzlLoadValue .Key , CachedBzlLoadData > successfulLoads ,
498
+ HashSet <BzlLoadValue .Key > unsuccessfulLoads ,
499
+ Consumer <CachedBzlLoadData > childCachedDataHandler ) {
451
500
this .loadStack = loadStack ;
501
+ this .successfulLoads = successfulLoads ;
502
+ this .unsuccessfulLoads = unsuccessfulLoads ;
452
503
this .childCachedDataHandler = childCachedDataHandler ;
453
- this .visitedBzls = visitedBzls ;
454
504
}
455
505
456
506
/**
@@ -460,12 +510,15 @@ private InliningState(
460
510
static InliningState create () {
461
511
return new InliningState (
462
512
/*loadStack=*/ new LinkedHashSet <>(),
463
- /*childCachedDataHandler=*/ x -> {}, // No parent value to mutate.
464
- /*visitedBzls=*/ new HashMap <>());
513
+ /*successfulLoads=*/ new HashMap <>(),
514
+ /*unsuccessfulLoads=*/ new HashSet <>(),
515
+ // No parent value to mutate
516
+ /*childCachedDataHandler=*/ x -> {});
465
517
}
466
518
467
519
private InliningState createChildState (Consumer <CachedBzlLoadData > childCachedDataHandler ) {
468
- return new InliningState (loadStack , childCachedDataHandler , visitedBzls );
520
+ return new InliningState (
521
+ loadStack , successfulLoads , unsuccessfulLoads , childCachedDataHandler );
469
522
}
470
523
471
524
/** Records entry to a {@code load()}, throwing an exception if a cycle is detected. */
@@ -760,8 +813,10 @@ private static List<BzlLoadValue> computeBzlLoadsWithSkyframe(
760
813
761
814
/**
762
815
* Computes the BzlLoadValue for all given keys by reusing this instance of the BzlLoadFunction,
763
- * bypassing traditional Skyframe evaluation, returning {@code null} if Skyframe deps were missing
764
- * and have been requested.
816
+ * bypassing traditional Skyframe evaluation.
817
+ *
818
+ * @return null if there was a missing Skyframe dep, an unspecified exception in a Skyframe dep
819
+ * request, or if this was a duplicate unsuccessful visitation
765
820
*/
766
821
@ Nullable
767
822
private List <BzlLoadValue > computeBzlLoadsWithInlining (
@@ -799,10 +854,13 @@ private List<BzlLoadValue> computeBzlLoadsWithInlining(
799
854
continue ;
800
855
}
801
856
if (cachedData == null ) {
802
- Preconditions .checkState (env .valuesMissing (), "no starlark load value for %s" , key );
803
- // We continue making inline calls even if some requested values are missing, to maximize
804
- // the number of dependent (non-inlined) SkyFunctions that are requested, thus avoiding a
805
- // quadratic number of restarts.
857
+ // A null value for `cachedData` can occur when it (or its transitive loads) has a Skyframe
858
+ // dep that is missing or in error. It can also occur if there's a transitive load on a bzl
859
+ // that was already seen by inliningState and which returned null (note: in this case, it's
860
+ // not necessarily true that there are missing Skyframe deps because this bzl could have
861
+ // already been visited unsuccessfully). In both these cases, we want to continue making our
862
+ // inline calls, so as to maximize the number of dependent (non-inlined) SkyFunctions that
863
+ // are requested and avoid a quadratic number of restarts.
806
864
valuesMissing = true ;
807
865
} else {
808
866
bzlLoads .add (cachedData .getValue ());
0 commit comments