Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4597142

Browse files
authored
feat: set default workspace proxy based on latency (#17812)
Auto select the proxy on first load (stored in local storage, so per browser), then defer to user selection. The auto selected proxy will not update again once set.
1 parent 80b7947 commit 4597142

File tree

7 files changed

+82
-12
lines changed

7 files changed

+82
-12
lines changed

site/src/contexts/ProxyContext.test.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ import type * as ProxyLatency from "./useProxyLatency";
2626
// here and not inside a unit test.
2727
jest.mock("contexts/useProxyLatency", () => ({
2828
useProxyLatency: () => {
29-
return { proxyLatencies: hardCodedLatencies, refetch: jest.fn() };
29+
return {
30+
proxyLatencies: hardCodedLatencies,
31+
refetch: jest.fn(),
32+
loaded: true,
33+
};
3034
},
3135
}));
3236

@@ -115,7 +119,7 @@ describe("ProxyContextGetURLs", () => {
115119
preferredPathAppURL,
116120
preferredWildcardHostname,
117121
) => {
118-
const preferred = getPreferredProxy(regions, selected, latencies);
122+
const preferred = getPreferredProxy(regions, selected, latencies, true);
119123
expect(preferred.preferredPathAppURL).toBe(preferredPathAppURL);
120124
expect(preferred.preferredWildcardHostname).toBe(
121125
preferredWildcardHostname,
@@ -138,10 +142,22 @@ const TestingComponent = () => {
138142

139143
// TestingScreen just mounts some components that we can check in the unit test.
140144
const TestingScreen = () => {
141-
const { proxy, userProxy, isFetched, isLoading, clearProxy, setProxy } =
142-
useProxy();
145+
const {
146+
proxy,
147+
userProxy,
148+
isFetched,
149+
isLoading,
150+
latenciesLoaded,
151+
clearProxy,
152+
setProxy,
153+
} = useProxy();
154+
143155
return (
144156
<>
157+
<div
158+
data-testid="latenciesLoaded"
159+
title={latenciesLoaded.toString()}
160+
></div>
145161
<div data-testid="isFetched" title={isFetched.toString()}></div>
146162
<div data-testid="isLoading" title={isLoading.toString()}></div>
147163
<div data-testid="preferredProxy" title={proxy.proxy?.id}></div>
@@ -206,7 +222,6 @@ describe("ProxyContextSelection", () => {
206222
};
207223

208224
it.each([
209-
// Not latency behavior
210225
[
211226
"empty",
212227
{
@@ -220,6 +235,7 @@ describe("ProxyContextSelection", () => {
220235
"regions_no_selection",
221236
{
222237
expProxyID: MockPrimaryWorkspaceProxy.id,
238+
expUserProxyID: MockPrimaryWorkspaceProxy.id,
223239
regions: MockWorkspaceProxies,
224240
storageProxy: undefined,
225241
},
@@ -261,11 +277,12 @@ describe("ProxyContextSelection", () => {
261277
expUserProxyID: MockHealthyWildWorkspaceProxy.id,
262278
},
263279
],
264-
// Latency behavior is disabled, so the primary should be selected.
280+
// First page load defers to the proxy by latency
265281
[
266282
"regions_default_low_latency",
267283
{
268-
expProxyID: MockPrimaryWorkspaceProxy.id,
284+
expProxyID: MockHealthyWildWorkspaceProxy.id,
285+
expUserProxyID: MockHealthyWildWorkspaceProxy.id,
269286
regions: MockWorkspaceProxies,
270287
storageProxy: undefined,
271288
latencies: {
@@ -362,6 +379,10 @@ describe("ProxyContextSelection", () => {
362379
TestingComponent();
363380
await waitForLoaderToBeRemoved();
364381

382+
await screen.findByTestId("latenciesLoaded").then((x) => {
383+
expect(x.title).toBe("true");
384+
});
385+
365386
if (afterLoad) {
366387
await afterLoad();
367388
}

site/src/contexts/ProxyContext.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export interface ProxyContextValue {
5454
// then the latency has not been fetched yet. Calculations happen async for each proxy in the list.
5555
// Refer to the returned report for a given proxy for more information.
5656
proxyLatencies: ProxyLatencies;
57+
// latenciesLoaded is true when the latencies have been initially loaded.
58+
// Once set to true, it will not be set to false again.
59+
latenciesLoaded: boolean;
5760
// refetchProxyLatencies will trigger refreshing of the proxy latencies. By default the latencies
5861
// are loaded once.
5962
refetchProxyLatencies: () => Date;
@@ -122,8 +125,11 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
122125

123126
// Every time we get a new proxiesResponse, update the latency check
124127
// to each workspace proxy.
125-
const { proxyLatencies, refetch: refetchProxyLatencies } =
126-
useProxyLatency(proxiesResp);
128+
const {
129+
proxyLatencies,
130+
refetch: refetchProxyLatencies,
131+
loaded: latenciesLoaded,
132+
} = useProxyLatency(proxiesResp);
127133

128134
// updateProxy is a helper function that when called will
129135
// update the proxy being used.
@@ -136,7 +142,8 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
136142
loadUserSelectedProxy(),
137143
proxyLatencies,
138144
// Do not auto select based on latencies, as inconsistent latencies can cause this
139-
// to behave poorly.
145+
// to change on each call. updateProxy should be stable when selecting a proxy to
146+
// prevent flickering.
140147
false,
141148
),
142149
);
@@ -149,6 +156,34 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
149156
updateProxy();
150157
}, [proxiesResp, proxyLatencies]);
151158

159+
// This useEffect will auto select the best proxy if the user has not selected one.
160+
// It must wait until all latencies are loaded to select based on latency. This does mean
161+
// the first time a user loads the page, the proxy will "flicker" to the best proxy.
162+
//
163+
// Once the page is loaded, or the user selects a proxy, this will not run again.
164+
// biome-ignore lint/correctness/useExhaustiveDependencies: Only update if the source data changes
165+
useEffect(() => {
166+
if (loadUserSelectedProxy() !== undefined) {
167+
return; // User has selected a proxy, do not auto select.
168+
}
169+
if (!latenciesLoaded) {
170+
// Wait until the latencies are loaded first.
171+
return;
172+
}
173+
174+
const best = getPreferredProxy(
175+
proxiesResp ?? [],
176+
loadUserSelectedProxy(),
177+
proxyLatencies,
178+
true,
179+
);
180+
181+
if (best?.proxy) {
182+
saveUserSelectedProxy(best.proxy);
183+
updateProxy();
184+
}
185+
}, [latenciesLoaded, proxiesResp, proxyLatencies]);
186+
152187
return (
153188
<ProxyContext.Provider
154189
value={{
@@ -157,6 +192,7 @@ export const ProxyProvider: FC<PropsWithChildren> = ({ children }) => {
157192
userProxy: userSavedProxy,
158193
proxy: proxy,
159194
proxies: proxiesResp,
195+
latenciesLoaded: latenciesLoaded,
160196
isLoading: proxiesLoading,
161197
isFetched: proxiesFetched,
162198
error: proxiesError,
@@ -214,12 +250,12 @@ export const getPreferredProxy = (
214250

215251
// If no proxy is selected, or the selected proxy is unhealthy default to the primary proxy.
216252
if (!selectedProxy || !selectedProxy.healthy) {
217-
// By default, use the primary proxy.
253+
// Default to the primary proxy
218254
selectedProxy = proxies.find((proxy) => proxy.name === "primary");
219255

220256
// If we have latencies, then attempt to use the best proxy by latency instead.
221257
const best = selectByLatency(proxies, latencies);
222-
if (autoSelectBasedOnLatency && best) {
258+
if (autoSelectBasedOnLatency && best !== undefined) {
223259
selectedProxy = best;
224260
}
225261
}

site/src/contexts/useProxyLatency.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export const useProxyLatency = (
4848
// Until the new values are loaded, the old values will still be used.
4949
refetch: () => Date;
5050
proxyLatencies: Record<string, ProxyLatencyReport>;
51+
// loaded signals all latency requests have completed. Once set to true, this will not change.
52+
// Latencies at this point should be loaded from local storage, and updated asynchronously as needed.
53+
// If local storage has updated latencies, then this will be set to true with 0 actual network requests.
54+
// The loaded latencies will all be from the cache.
55+
loaded: boolean;
5156
} => {
5257
// maxStoredLatencies is the maximum number of latencies to store per proxy in local storage.
5358
let maxStoredLatencies = 1;
@@ -73,6 +78,8 @@ export const useProxyLatency = (
7378
new Date(new Date().getTime() - proxyIntervalSeconds * 1000).toISOString(),
7479
);
7580

81+
const [loaded, setLoaded] = useState(false);
82+
7683
// Refetch will always set the latestFetchRequest to the current time, making all the cached latencies
7784
// stale and triggering a refetch of all proxies in the list.
7885
const refetch = () => {
@@ -231,6 +238,7 @@ export const useProxyLatency = (
231238

232239
// Local storage cleanup
233240
garbageCollectStoredLatencies(proxies, maxStoredLatencies);
241+
setLoaded(true);
234242
});
235243

236244
return () => {
@@ -241,6 +249,7 @@ export const useProxyLatency = (
241249
return {
242250
proxyLatencies,
243251
refetch,
252+
loaded,
244253
};
245254
};
246255

site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const meta: Meta<typeof MobileMenu> = {
2323
component: MobileMenu,
2424
args: {
2525
proxyContextValue: {
26+
latenciesLoaded: true,
2627
proxy: {
2728
preferredPathAppURL: "",
2829
preferredWildcardHostname: "",

site/src/modules/dashboard/Navbar/NavbarView.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { renderWithAuth } from "testHelpers/renderHelpers";
66
import { NavbarView } from "./NavbarView";
77

88
const proxyContextValue: ProxyContextValue = {
9+
latenciesLoaded: true,
910
proxy: {
1011
preferredPathAppURL: "",
1112
preferredWildcardHostname: "",

site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { withDesktopViewport } from "testHelpers/storybook";
1515
import { ProxyMenu } from "./ProxyMenu";
1616

1717
const defaultProxyContextValue = {
18+
latenciesLoaded: true,
1819
proxyLatencies: MockProxyLatencies,
1920
proxy: getPreferredProxy(MockWorkspaceProxies, undefined),
2021
proxies: MockWorkspaceProxies,

site/src/testHelpers/storybook.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export const withProxyProvider =
167167
return (
168168
<ProxyContext.Provider
169169
value={{
170+
latenciesLoaded: true,
170171
proxyLatencies: MockProxyLatencies,
171172
proxy: getPreferredProxy([], undefined),
172173
proxies: [],

0 commit comments

Comments
 (0)