Skip to content

Commit 0a914ff

Browse files
authored
fix(telemetry): cache installation token to prevent per-event regeneration (#1458)
generateInstallationToken() called MacAddressUtil.defaultMachineId() on every phone-home event. In environments where Netty cannot resolve a real MAC address (Docker, cloud VMs), it falls back to random bytes — producing a unique token per event (~every 60s). BigQuery analysis showed 25 pathological sessions generating 97% of all distinct installation tokens. Replace uncached call with initialization-on-demand holder pattern, guaranteeing a single computation per JVM lifetime. Apply the same pattern to sessionToken for consistency. EDG-280
1 parent 3ed0451 commit 0a914ff

File tree

2 files changed

+91
-11
lines changed

2 files changed

+91
-11
lines changed

hivemq-edge/src/main/java/com/hivemq/edge/utils/HiveMQEdgeEnvironmentUtils.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,20 @@
2727
*/
2828
public class HiveMQEdgeEnvironmentUtils {
2929

30-
private static volatile UUID sessionToken;
31-
private static Object lock = new Object();
30+
private static class InstallationTokenHolder {
31+
static final @NotNull String VALUE = getDefaultMachineId();
32+
}
33+
34+
private static class SessionTokenHolder {
35+
static final @NotNull String VALUE = UUID.randomUUID().toString();
36+
}
3237

3338
public static @NotNull String generateInstallationToken() {
34-
return HiveMQEdgeEnvironmentUtils.getDefaultMachineId();
39+
return InstallationTokenHolder.VALUE;
3540
}
3641

3742
public static @NotNull String getSessionToken() {
38-
if (sessionToken == null) {
39-
synchronized (lock) {
40-
if (sessionToken == null) {
41-
sessionToken = UUID.randomUUID();
42-
}
43-
}
44-
}
45-
return sessionToken.toString();
43+
return SessionTokenHolder.VALUE;
4644
}
4745

4846
public static @NotNull Map<String, String> generateEnvironmentMap() {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2019-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.hivemq.edge.utils;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
22+
import java.util.Set;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.concurrent.CountDownLatch;
25+
import java.util.concurrent.ExecutorService;
26+
import java.util.concurrent.Executors;
27+
import java.util.concurrent.TimeUnit;
28+
import org.junit.jupiter.api.Test;
29+
30+
class HiveMQEdgeEnvironmentUtilsTest {
31+
32+
@Test
33+
void installationToken_returnsSameValueOnRepeatedCalls() {
34+
final String first = HiveMQEdgeEnvironmentUtils.generateInstallationToken();
35+
final String second = HiveMQEdgeEnvironmentUtils.generateInstallationToken();
36+
final String third = HiveMQEdgeEnvironmentUtils.generateInstallationToken();
37+
38+
assertNotNull(first);
39+
assertFalse(first.isBlank());
40+
assertEquals(first, second, "Installation token must be stable across calls");
41+
assertEquals(first, third, "Installation token must be stable across calls");
42+
}
43+
44+
@Test
45+
void installationToken_stableUnderConcurrentAccess() throws InterruptedException {
46+
final int threadCount = 32;
47+
final Set<String> tokens = ConcurrentHashMap.newKeySet();
48+
final CountDownLatch ready = new CountDownLatch(threadCount);
49+
final CountDownLatch go = new CountDownLatch(1);
50+
final ExecutorService executor = Executors.newFixedThreadPool(threadCount);
51+
52+
for (int i = 0; i < threadCount; i++) {
53+
executor.submit(() -> {
54+
ready.countDown();
55+
try {
56+
go.await(5, TimeUnit.SECONDS);
57+
} catch (final InterruptedException e) {
58+
Thread.currentThread().interrupt();
59+
}
60+
tokens.add(HiveMQEdgeEnvironmentUtils.generateInstallationToken());
61+
});
62+
}
63+
64+
ready.await(5, TimeUnit.SECONDS);
65+
go.countDown();
66+
executor.shutdown();
67+
executor.awaitTermination(10, TimeUnit.SECONDS);
68+
69+
assertEquals(1, tokens.size(),
70+
"All threads must observe the same installation token, but got " + tokens.size() + " distinct values");
71+
}
72+
73+
@Test
74+
void sessionToken_returnsSameValueOnRepeatedCalls() {
75+
final String first = HiveMQEdgeEnvironmentUtils.getSessionToken();
76+
final String second = HiveMQEdgeEnvironmentUtils.getSessionToken();
77+
78+
assertNotNull(first);
79+
assertFalse(first.isBlank());
80+
assertEquals(first, second, "Session token must be stable across calls");
81+
}
82+
}

0 commit comments

Comments
 (0)