Skip to content

Commit 9b52656

Browse files
authored
Create OptionCallbackRegistry.java
1 parent 8ad2554 commit 9b52656

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.config;
7+
8+
9+
import java.util.List;
10+
import java.util.Map;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.CopyOnWriteArrayList;
13+
import java.util.concurrent.Executors;
14+
import java.util.concurrent.ScheduledExecutorService;
15+
import java.util.concurrent.TimeUnit;
16+
import java.util.logging.Logger;
17+
import javax.annotation.Nullable;
18+
19+
// MutableConfigProvider: If MutableConfigProvider was supported, this registry could be enhanced to:
20+
// - Register as a listener with the MutableConfigProvider to receive immediate notifications on config changes
21+
// - Eliminate the need for periodic polling (executor) and instead react to provider-driven change events
22+
// - Support dynamic config updates without the 30-second polling delay
23+
24+
/**
25+
* Registry for callbacks that are invoked when configuration option values change.
26+
*
27+
* <p>This singleton can be loaded by multiple classloaders, creating separate instances that each monitor
28+
* the globally consistent System properties. For instances that don't need periodic checking (e.g., only
29+
* used for {@link #updateOption(String, String)}), {@link #shutdownPeriodicChecker()} can be called
30+
* to stop the background thread. Typically this would mean the extension that loads this class to use
31+
* {@link #updateOption(String, String)}) and no other capability of this class, should shutdown the
32+
* periodic checker during the extension initialization task. If this is not done, there are no adverse
33+
* effects other than the additional very low overhead thread.
34+
*
35+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change at
36+
* any time.
37+
*/
38+
public final class OptionCallbackRegistry {
39+
40+
private static final Logger logger = Logger.getLogger(OptionCallbackRegistry.class.getName());
41+
42+
private static final OptionCallbackRegistry INSTANCE = new OptionCallbackRegistry();
43+
44+
// Allow multiple registrations on any key
45+
private final Map<String, List<OptionChangeListener>> callbacks = new ConcurrentHashMap<>();
46+
47+
// Keep previous values so that only changes get notified
48+
private final Map<String, String> previousValues = new ConcurrentHashMap<>();
49+
50+
// MutableConfigProvider: This executor could be unnecessary if MutableConfigProvider was available,
51+
// as we could register directly with the provider for immediate change notifications instead of polling
52+
private final ScheduledExecutorService executor =
53+
Executors.newSingleThreadScheduledExecutor(
54+
r -> {
55+
Thread t = new Thread(r, "otel-option-callback-checker");
56+
t.setDaemon(true);
57+
return t;
58+
});
59+
60+
private OptionCallbackRegistry() {
61+
// Start periodic checking
62+
// MutableConfigProvider: Instead of polling, register with MutableConfigProvider like:
63+
// mutableConfigProvider.addChangeListener(this::onConfigChanged);
64+
executor.scheduleWithFixedDelay(this::checkForChanges, 30, 30, TimeUnit.SECONDS);
65+
}
66+
67+
/** Returns the global OptionCallbackRegistry instance. */
68+
public static OptionCallbackRegistry getInstance() {
69+
return INSTANCE;
70+
}
71+
72+
/** Shuts down the periodic checking executor. */
73+
public void shutdownPeriodicChecker() {
74+
executor.shutdown();
75+
}
76+
77+
/**
78+
* Registers a listener to be invoked when the value of the specified option key changes.
79+
*
80+
* <p>The listener receives the key and the new value. If the value becomes null, the listener is
81+
* still invoked.
82+
*
83+
* <p>MutableConfigProvider: With MutableConfigProvider, this method would register the listener
84+
* directly with the provider for the specific key, ensuring immediate notification of changes
85+
* without relying on polling.
86+
*
87+
* @param key the configuration key to monitor
88+
* @param currentValue the current value of the key
89+
* @param listener the listener to invoke on change
90+
* @param isDeclarative if true, the currentValue is passed into {@link #updateOption(String, String)}. Where the config is declarative, this should be true, otherwise this should be false
91+
*/
92+
public void registerCallback(String key, String currentValue, OptionChangeListener listener, boolean isDeclarative) {
93+
// If instrumentations could be unloaded, we would wrap listeners in a WeakReference to prevent memory leaks:
94+
// callbacks.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(new WeakReference<>(listener));
95+
96+
callbacks.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(listener);
97+
if (isDeclarative) {
98+
updateOption(key, currentValue);
99+
}
100+
previousValues.put(key, currentValue);
101+
}
102+
103+
/**
104+
* Updates the value of an option. Registered callbacks will be notified asynchronously
105+
* when the periodic check detects the change.
106+
*
107+
* <p>MutableConfigProvider: If MutableConfigProvider was available, this method would be
108+
* replaced by provider-driven updates. The MutableConfigProvider could notify this registry
109+
* directly when configurations change, eliminating the need for manual updates via this method.
110+
* Alternatively or additionally, this method could remain the primary runtime update mechanism,
111+
* handling the required updates to MutableConfigProvider.
112+
*
113+
* @param key the option key
114+
* @param value the new value, or {@code null} to remove the option
115+
*/
116+
public void updateOption(String key, String value) {
117+
if (value != null) {
118+
// System.setProperty is thread-safe and provides consistent writes so no need to synchronize
119+
System.setProperty(key, value);
120+
} else {
121+
// Remove the property if value is null
122+
// System.clearProperty is thread-safe and provides consistent writes so no need to synchronize
123+
System.clearProperty(key);
124+
}
125+
}
126+
127+
// MutableConfigProvider: This polling method could be replaced by direct callback from MutableConfigProvider.
128+
// The provider could call a method like onConfigChanged(String key, String newValue) directly,
129+
// eliminating the need to check all keys periodically and providing immediate updates.
130+
private void checkForChanges() {
131+
for (String key : callbacks.keySet()) {
132+
String currentValue = getCurrentValue(key);
133+
String previousValue = previousValues.get(key);
134+
if (!java.util.Objects.equals(currentValue, previousValue)) {
135+
previousValues.put(key, currentValue);
136+
notifyCallbacks(key, currentValue, previousValue);
137+
}
138+
}
139+
}
140+
141+
private static String getCurrentValue(String key) {
142+
// System.getProperty is thread-safe and provides consistent reads so no need to synchronize
143+
return System.getProperty(key);
144+
}
145+
146+
private void notifyCallbacks(String key, @Nullable String newValue, @Nullable String oldValue) {
147+
List<OptionChangeListener> listenerList = callbacks.get(key);
148+
if (listenerList != null) {
149+
// If we were using WeakReferences, this would need cleanup logic:
150+
// listenerList.removeIf(ref -> ref.get() == null);
151+
for (OptionChangeListener listener : listenerList) {
152+
try {
153+
listener.onOptionChanged(key, newValue, oldValue);
154+
} catch (Throwable t) {
155+
logger.info("Warning, exception thrown when trying to notify listener for key '" + key + "': " + t.getMessage());
156+
}
157+
}
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)