mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
fix(license): keep one boundary refetch chain and toast only its first failure
This commit is contained in:
@@ -1938,14 +1938,99 @@ describe("License.vue", () => {
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("drops a pending refetch on unmount", async () => {
|
||||
vi.mocked(licenseServer.get_license).mockResolvedValue(response({}));
|
||||
const deferred = () => {
|
||||
let resolve: (value: AxiosResponse) => void = () => {};
|
||||
const promise = new Promise<AxiosResponse>((r) => (resolve = r));
|
||||
return { promise, resolve };
|
||||
};
|
||||
const errorToasts = () =>
|
||||
mockToast.mock.calls.filter(([options]) => options?.variant === "error").length;
|
||||
|
||||
it("drops a pending refetch on unmount, including one a boundary set during a refetch", async () => {
|
||||
const inFlight = deferred();
|
||||
vi.mocked(licenseServer.get_license)
|
||||
.mockResolvedValueOnce(response({}))
|
||||
.mockReturnValueOnce(inFlight.promise)
|
||||
.mockResolvedValue(response({}));
|
||||
wrapper = createWrapper();
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
await vi.advanceTimersByTimeAsync(2500);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(2);
|
||||
wrapper.vm.refetchAtBoundary(graceEndUs);
|
||||
inFlight.resolve(response({}));
|
||||
await flushPromises();
|
||||
wrapper.unmount();
|
||||
await vi.advanceTimersByTimeAsync(LICENSE_BOUNDARY_RETRY_MS * 3);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(1);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("schedules nothing when a refetch in flight finishes after unmount", async () => {
|
||||
const inFlight = deferred();
|
||||
vi.mocked(licenseServer.get_license)
|
||||
.mockResolvedValueOnce(response({}))
|
||||
.mockReturnValueOnce(inFlight.promise)
|
||||
.mockResolvedValue(response({}));
|
||||
wrapper = createWrapper();
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(2500);
|
||||
wrapper.unmount();
|
||||
inFlight.resolve(response({}));
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(LICENSE_BOUNDARY_RETRY_MS * 3);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps a single retry chain when a boundary arrives during a refetch", async () => {
|
||||
const inFlight = deferred();
|
||||
vi.mocked(licenseServer.get_license)
|
||||
.mockResolvedValueOnce(response({}))
|
||||
.mockReturnValueOnce(inFlight.promise)
|
||||
.mockResolvedValue(response({}));
|
||||
wrapper = createWrapper();
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(2500);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(2);
|
||||
wrapper.vm.refetchAtBoundary(graceEndUs);
|
||||
inFlight.resolve(response({}));
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(3);
|
||||
for (let minute = 1; minute <= 3; minute++) {
|
||||
await vi.advanceTimersByTimeAsync(LICENSE_BOUNDARY_RETRY_MS);
|
||||
expect(licenseServer.get_license).toHaveBeenCalledTimes(3 + minute);
|
||||
}
|
||||
});
|
||||
|
||||
it("toasts only the first failure of a boundary retry chain", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const gets = () => vi.mocked(licenseServer.get_license).mock.calls.length;
|
||||
vi.mocked(licenseServer.get_license)
|
||||
.mockResolvedValueOnce(response({}))
|
||||
.mockRejectedValue({ response: { status: 503 } });
|
||||
wrapper = createWrapper();
|
||||
await flushPromises();
|
||||
mockToast.mockClear();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2500 + 5000);
|
||||
expect(gets()).toBeGreaterThan(1);
|
||||
expect(errorToasts()).toBe(1);
|
||||
|
||||
const userReload = wrapper.vm.reloadLicenseData();
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
await userReload;
|
||||
expect(errorToasts()).toBe(2);
|
||||
|
||||
const beforeRetries = gets();
|
||||
await vi.advanceTimersByTimeAsync(LICENSE_BOUNDARY_RETRY_MS * 3);
|
||||
expect(gets()).toBeGreaterThan(beforeRetries + 2);
|
||||
expect(errorToasts()).toBe(2);
|
||||
|
||||
wrapper.vm.refetchAtBoundary(graceEndUs + 1);
|
||||
await vi.advanceTimersByTimeAsync(1000 + 5000);
|
||||
expect(errorToasts()).toBe(3);
|
||||
await vi.advanceTimersByTimeAsync(LICENSE_BOUNDARY_RETRY_MS * 2);
|
||||
expect(errorToasts()).toBe(3);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -721,17 +721,21 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
// Only the first load may swap the page for the spinner; later reloads would unmount the form.
|
||||
const loadLicenseData = async ({ background = false } = {}) => {
|
||||
const loadLicenseData = async ({ background = false, quiet = false } = {}) => {
|
||||
try {
|
||||
if (!background) loading.value = true;
|
||||
licenseData.value = await queryClient.fetchQuery(licenseQuery());
|
||||
if (!background) checkAndAutoFillLicenseFromUrl();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error loading license data:", error);
|
||||
toast({
|
||||
variant: "error",
|
||||
message: t("about.failed_to_load_license_info"),
|
||||
});
|
||||
if (!quiet) {
|
||||
toast({
|
||||
variant: "error",
|
||||
message: t("about.failed_to_load_license_info"),
|
||||
});
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (!background) loading.value = false;
|
||||
}
|
||||
@@ -742,26 +746,36 @@ export default defineComponent({
|
||||
let boundaryRefetch: ReturnType<typeof setTimeout> | undefined;
|
||||
let boundaryUs = 0;
|
||||
let boundaryRetries = 0;
|
||||
let boundaryChain = 0;
|
||||
let boundaryErrorShown = false;
|
||||
let unmounted = false;
|
||||
const clearBoundaryRefetch = () => {
|
||||
if (boundaryRefetch !== undefined) clearTimeout(boundaryRefetch);
|
||||
boundaryRefetch = undefined;
|
||||
};
|
||||
const scheduleBoundaryRefetch = (chain: number, delayMs: number) => {
|
||||
clearBoundaryRefetch();
|
||||
boundaryRefetch = setTimeout(() => refetchAfterBoundary(chain), delayMs);
|
||||
};
|
||||
// the server re-evaluates on its own tick, so a decision from before the boundary is fetched again
|
||||
const refetchAfterBoundary = async () => {
|
||||
const refetchAfterBoundary = async (chain: number) => {
|
||||
boundaryRefetch = undefined;
|
||||
await reloadLicenseData();
|
||||
const loaded = await loadLicenseData({ background: true, quiet: boundaryErrorShown });
|
||||
if (!loaded && chain === boundaryChain) boundaryErrorShown = true;
|
||||
// a boundary that arrived during the fetch started a newer chain, which owns the timer
|
||||
if (unmounted || chain !== boundaryChain) return;
|
||||
const evaluatedAt = Number(licenseData.value?.evaluated_at ?? 0);
|
||||
if (unmounted || !(evaluatedAt > 0 && evaluatedAt < boundaryUs)) return;
|
||||
if (!(evaluatedAt > 0 && evaluatedAt < boundaryUs)) return;
|
||||
if (boundaryRetries >= LICENSE_BOUNDARY_RETRIES) return;
|
||||
boundaryRetries++;
|
||||
boundaryRefetch = setTimeout(refetchAfterBoundary, LICENSE_BOUNDARY_RETRY_MS);
|
||||
scheduleBoundaryRefetch(chain, LICENSE_BOUNDARY_RETRY_MS);
|
||||
};
|
||||
const refetchAtBoundary = (atUs: number) => {
|
||||
boundaryUs = Math.max(boundaryUs, atUs);
|
||||
boundaryRetries = 0;
|
||||
clearBoundaryRefetch();
|
||||
boundaryRefetch = setTimeout(refetchAfterBoundary, LICENSE_BOUNDARY_DEBOUNCE_MS);
|
||||
boundaryChain++;
|
||||
boundaryErrorShown = false;
|
||||
scheduleBoundaryRefetch(boundaryChain, LICENSE_BOUNDARY_DEBOUNCE_MS);
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
unmounted = true;
|
||||
|
||||
Reference in New Issue
Block a user