-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DefaultSyscallCache
can export the latest LoadingCache
objects fo…
…r instrumentation. The `LatestObjectMetricExporter` supports a pattern for lazily registering metrics that instrument objects that are effectively singletons: objects for which there are never more than one instance, but which are destroyed and recreated over the course of the server's lifetime. PiperOrigin-RevId: 594053031 Change-Id: Ic616af55b20c3b789ae2037ef4e829954194fae1
- Loading branch information
1 parent
673d4d3
commit 4c4a1a7
Showing
4 changed files
with
293 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
src/main/java/com/google/devtools/build/lib/util/LatestObjectMetricExporter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// Copyright 2023 The Bazel Authors. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.devtools.build.lib.util; | ||
|
||
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; | ||
import java.lang.ref.Reference; | ||
import java.lang.ref.SoftReference; | ||
import java.lang.ref.WeakReference; | ||
import java.util.function.Supplier; | ||
import javax.annotation.concurrent.GuardedBy; | ||
|
||
/** | ||
* Exporter for a callback metric instrumenting a singleton that may not be created, and when | ||
* created, may be discarded and re-created. | ||
* | ||
* <p>Lazily registers a callback-metric with a thread-safe {@link Supplier} of the latest value of | ||
* that reference. Lazily registering the callback metric reduces metric pollution when the | ||
* instrumented codepaths are never executed. | ||
* | ||
* <p>Weak/soft references must be used to allow the instrumented object to be GCed; callbacks must | ||
* expect {@code null} values. Note that in some instrumentation libraries it is impossible to stop | ||
* exporting a given metric. | ||
* | ||
* <p>Simple usage example based on the open-source {@code io.opentelemetry.api.metrics} API: | ||
* | ||
* <pre> | ||
* class FooManager { | ||
* private static final ObservableLongMeasurement fooMetric = | ||
* MyMeterProvider.get().gaugeBuilder("foo").ofLongs().buildObserver(); | ||
* private static final ObservableLongMeasurement barMetric = | ||
* MyMeterProvider.get().gaugeBuilder("bar").ofLongs().buildObserver(); | ||
* static void updateMetric(FooManager manager) { | ||
* fooMetric.record(manager == null ? 0L : manager.getFoo()); | ||
* barMetric.record(manager == null ? 0L : manager.getBar()); | ||
* } | ||
* private static final LatestObjectMetricExporter<FooManager> FOO_MANAGER_EXPORTER = | ||
* new LatestObjectMetricExporter<>( | ||
* LatestObjectMetricExporter.Strength.WEAK, | ||
* (supplier) -> MyMeterProvider.get().batchCallback( | ||
* () -> updateMetric(supplier.get()), | ||
* fooMetric, | ||
* barMetric)); | ||
* | ||
* // Need some state fields to export. | ||
* \@GuardedBy("this") private Foo foo; | ||
* \@GuardedBy("this") private Bar bar; | ||
* FooManager(Foo foo, Bar bar) { | ||
* // Initialize state fields before exporting the FooManager. | ||
* this.bar = bar; | ||
* this.bar = bar; | ||
* FOO_MANAGER_EXPORTER.setLatestInstance(this); | ||
* } | ||
* // Measurements must be thread-safe. | ||
* synchronized long getFoo() { | ||
* return bar.getFooSize(); | ||
* } | ||
* synchronized long getBar() { | ||
* return bar.getBarSize(); | ||
* } | ||
* } | ||
* </pre> | ||
* | ||
* @param <T> Type of the <em>latest object</em> being tracked. | ||
*/ | ||
@ThreadSafe | ||
public final class LatestObjectMetricExporter<T> { | ||
|
||
/** | ||
* Metric-specific callback, run once the first time a {@link LatestObjectMetricExporter} is used. | ||
*/ | ||
public interface CallbackRegistration<T> { | ||
/** | ||
* One-time setup method expected to register callback metrics with the instrumentation | ||
* library's metric registry. | ||
* | ||
* <p>Callbacks are expected to use the given {@link Supplier} to get the latest instance (or | ||
* {@code null} if the latest instance has been GCed). | ||
*/ | ||
void register(Supplier<T> refSupplier); | ||
} | ||
|
||
/** Kind of reference held by the exporter. */ | ||
public enum Strength { | ||
/** Creates {@link WeakReference} instances. */ | ||
WEAK, | ||
/** Creates {@link SoftReference} instances. */ | ||
SOFT; | ||
|
||
/** Create a new Reference for the given value, which may be {@code null}. */ | ||
<T> Reference<T> makeRef(T value) { | ||
switch (this) { | ||
case WEAK: | ||
return new WeakReference<>(value); | ||
case SOFT: | ||
return new SoftReference<>(value); | ||
} | ||
throw new IllegalStateException("unexpected reference strength: " + name()); | ||
} | ||
} | ||
|
||
/** The reference strength used for the latest object. */ | ||
private final Strength strength; | ||
|
||
/** | ||
* Registration callback that will be invoked at most once, the first time {@link | ||
* LatestObjectMetricExporter#setLatestInstance(T)} is called. | ||
*/ | ||
private final CallbackRegistration<T> registration; | ||
|
||
/** Flag that is set after the callback registration method has been called. */ | ||
@GuardedBy("this") | ||
private boolean callbackRegistered = false; | ||
|
||
/** | ||
* Reference to the last {@link T object} created by Blaze; as a weak/soft reference, will be null | ||
* if it has been GCed. | ||
* | ||
* <p>We don't use an {@link java.util.concurrent.atomic.AtomicReference} because we don't know | ||
* (other than a finalizer) when to clear the reference to avoid leaking memory. | ||
*/ | ||
@GuardedBy("this") | ||
private Reference<T> reference; | ||
|
||
/** Create a singleton exporter with the given reference strength and registration callback. */ | ||
public LatestObjectMetricExporter(Strength strength, CallbackRegistration<T> registration) { | ||
this.strength = strength; | ||
this.registration = registration; | ||
reference = strength.makeRef(null); | ||
} | ||
|
||
/** | ||
* Sets the latest instance of the instrumented singleton (through the Supplier passed to the | ||
* exporter's {@link CallbackRegistration}). | ||
* | ||
* <p>If this is the first time the method has been called, {@code registration#register()} will | ||
* be called after changing {@link #reference}. | ||
*/ | ||
public synchronized void setLatestInstance(T value) { | ||
reference = strength.makeRef(value); | ||
if (!callbackRegistered) { | ||
registration.register( | ||
() -> { | ||
synchronized (LatestObjectMetricExporter.this) { | ||
return reference.get(); | ||
} | ||
}); | ||
callbackRegistered = true; | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
src/test/java/com/google/devtools/build/lib/util/LatestObjectMetricExporterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Copyright 2023 The Bazel Authors. All rights reserved. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.google.devtools.build.lib.util; | ||
|
||
import static com.google.common.truth.Truth.assertThat; | ||
|
||
import java.util.concurrent.atomic.AtomicReference; | ||
import java.util.function.Supplier; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.junit.runners.JUnit4; | ||
|
||
/** Unit test for {@link LatestObjectMetricExporter}. */ | ||
@RunWith(JUnit4.class) | ||
public class LatestObjectMetricExporterTest { | ||
|
||
@Test | ||
public void weakReferencesAreGarbageCollected() { | ||
// Create an exporter with the given strength and whose registration stores the Supplier<Object> | ||
// in an AtomicReference. | ||
LatestObjectMetricExporter.Strength strength = LatestObjectMetricExporter.Strength.WEAK; | ||
AtomicReference<Supplier<Object>> registeredSupplierRef = new AtomicReference<>(null); | ||
LatestObjectMetricExporter.CallbackRegistration<Object> registration = | ||
registeredSupplierRef::set; | ||
LatestObjectMetricExporter<Object> exporter = | ||
new LatestObjectMetricExporter<>(strength, registration); | ||
assertThat(registeredSupplierRef.get()).isNull(); | ||
// Set up three Objects to serve as dummy "latest objects" to pass through the exporter. | ||
Object first = new Object(); | ||
Object second = new Object(); | ||
Object third = new Object(); | ||
// Set the first value, at which point the registration will run and we will get the Supplier | ||
// that tells us the currently exported object. | ||
exporter.setLatestInstance(first); | ||
Supplier<Object> latestObjectSupplier = registeredSupplierRef.get(); | ||
assertThat(latestObjectSupplier).isNotNull(); | ||
assertThat(latestObjectSupplier.get()).isSameInstanceAs(first); | ||
|
||
// Remove only reference to the latest object and run the GC. The supplier should start | ||
// producing null, not `first`. | ||
first = null; | ||
Runtime runtime = Runtime.getRuntime(); | ||
runtime.gc(); | ||
assertThat(latestObjectSupplier.get()).isNull(); | ||
|
||
// Remove only reference to the latest object but don't run the GC. The supplier should still | ||
// return `second` until we change the latest inatance to `third`, at which point GC has no | ||
// observable effect.. | ||
exporter.setLatestInstance(second); | ||
assertThat(latestObjectSupplier.get()).isSameInstanceAs(second); | ||
second = null; | ||
exporter.setLatestInstance(third); | ||
assertThat(latestObjectSupplier.get()).isSameInstanceAs(third); | ||
runtime.gc(); | ||
assertThat(latestObjectSupplier.get()).isSameInstanceAs(third); | ||
|
||
// Repeat the first assertion: removing the reference and GCing will cause the Supplier | ||
// to produce null, not `third`. | ||
third = null; | ||
runtime.gc(); | ||
assertThat(latestObjectSupplier.get()).isNull(); | ||
} | ||
} |