Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10824-fixed-1724428840947.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Event handlers making a proportional number of GET requests to the number of incoming events ([#10824](https://github.com/linode/manager/pull/10824))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks @bnussman-akamai! This is awesome, great to have coverage for this.

I also ran this test against develop to confirm that it will fail when multiple requests get made, and it does πŸŽ‰

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { eventFactory } from 'src/factories';
import { mockGetEvents } from 'support/intercepts/events';
import { mockGetVolumes } from 'support/intercepts/volumes';

describe('Event Handlers', () => {
it('invokes event handlers when new events are polled and makes the correct number of requests', () => {
// See https://github.com/linode/manager/pull/10824

mockGetEvents([]).as('getEvents');
mockGetVolumes([]).as('getInitialVolumes');

cy.visitWithLogin('/volumes');

// Wait for the initial events fetch and initial volumes fetch
cy.wait(['@getEvents', '@getInitialVolumes']);

// Wait for the first polling interval to happen
cy.wait('@getEvents');

const polledEvents = [
eventFactory.build({ action: 'volume_update', status: 'notification' }),
eventFactory.build({ action: 'volume_update', status: 'notification' }),
eventFactory.build({ action: 'volume_update', status: 'notification' }),
];

// Pretend a volume was updated 3 times in a row
mockGetEvents(polledEvents).as('getEvents');

// Intercept GET volumes so we can later check how many times it is fetched
mockGetVolumes([]).as('getVolumes');

// Wait for volume update events to be polled
cy.wait('@getEvents');

// On the next interval, mock no new events
mockGetEvents([]).as('getEvents');
cy.wait('@getEvents');

// Finally, verify the volume endpoint was only fetched once
cy.get('@getVolumes.all').should('have.length', 1);
});
});
Empty file.
18 changes: 16 additions & 2 deletions packages/manager/src/hooks/useEventHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ import { supportTicketEventHandler } from 'src/queries/support';
import { volumeEventsHandler } from 'src/queries/volumes/events';

import type { Event } from '@linode/api-v4';
import type { QueryClient } from '@tanstack/react-query';
import type {
InvalidateQueryFilters,
QueryClient,
} from '@tanstack/react-query';

export interface EventHandlerData {
event: Event;
invalidateQueries: (filters: InvalidateQueryFilters) => Promise<void>;
queryClient: QueryClient;
}

Expand Down Expand Up @@ -91,14 +95,24 @@ export const eventHandlers: {
export const useEventHandlers = () => {
const queryClient = useQueryClient();

/*
* We wrap invalidateQueries because we need to enforce some options.
*
* We set `cancelRefetch` to `false` because it ensures no refetch will
* be made if there is already a request running. This is important for
* event handlers because they are envoked once for every event polled.
*/
const invalidateQueries = (filters: InvalidateQueryFilters) =>
queryClient.invalidateQueries(filters, { cancelRefetch: false });

/**
* Given an event, this function finds the corresponding
* event handler and invokes it.
*/
const handleEvent = (event: Event) => {
for (const eventHandler of eventHandlers) {
if (eventHandler.filter(event)) {
eventHandler.handler({ event, queryClient });
eventHandler.handler({ event, invalidateQueries, queryClient });
return;
}
}
Expand Down
7 changes: 5 additions & 2 deletions packages/manager/src/queries/account/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ export const useAllAccountPayments = (
});
};

export const taxIdEventHandler = ({ event, queryClient }: EventHandlerData) => {
export const taxIdEventHandler = ({
event,
invalidateQueries,
}: EventHandlerData) => {
if (event.action === 'tax_id_invalid' || event.action === 'tax_id_valid') {
queryClient.invalidateQueries({
invalidateQueries({
queryKey: accountQueries.notifications.queryKey,
});
}
Expand Down
6 changes: 4 additions & 2 deletions packages/manager/src/queries/account/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ export const useUpdateOAuthClientMutation = (id: string) => {
});
};

export const oauthClientsEventHandler = ({ queryClient }: EventHandlerData) => {
export const oauthClientsEventHandler = ({
invalidateQueries,
}: EventHandlerData) => {
// We may over-fetch because on `onSuccess` also invalidates, but this will be
// good for UX because Cloud will always be up to date
queryClient.invalidateQueries({
invalidateQueries({
queryKey: accountQueries.oauthClients._def,
});
};
6 changes: 3 additions & 3 deletions packages/manager/src/queries/databases/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import type { EventHandlerData } from 'src/hooks/useEventHandlers';

export const databaseEventsHandler = ({
event,
queryClient,
invalidateQueries,
}: EventHandlerData) => {
if (['failed', 'finished', 'notification'].includes(event.status)) {
queryClient.invalidateQueries({
invalidateQueries({
queryKey: databaseQueries.databases.queryKey,
});

Expand All @@ -35,7 +35,7 @@ export const databaseEventsHandler = ({
);
}

queryClient.invalidateQueries({
invalidateQueries({
queryKey: databaseQueries.database(engine as Engine, event.entity.id)
.queryKey,
});
Expand Down
8 changes: 4 additions & 4 deletions packages/manager/src/queries/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const useUpdateDomainMutation = () => {

export const domainEventsHandler = ({
event,
queryClient,
invalidateQueries,
}: EventHandlerData) => {
const domainId = event.entity?.id;

Expand All @@ -196,17 +196,17 @@ export const domainEventsHandler = ({

if (event.action.startsWith('domain_record')) {
// Invalidate the domain's records because they may have changed
queryClient.invalidateQueries({
invalidateQueries({
queryKey: domainQueries.domain(domainId)._ctx.records.queryKey,
});
} else {
// Invalidate paginated lists
queryClient.invalidateQueries({
invalidateQueries({
queryKey: domainQueries.domains.queryKey,
});

// Invalidate the domain's details
queryClient.invalidateQueries({
invalidateQueries({
exact: true,
queryKey: domainQueries.domain(domainId).queryKey,
});
Expand Down
33 changes: 13 additions & 20 deletions packages/manager/src/queries/firewalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ export const useUpdateFirewallRulesMutation = (firewallId: number) => {

export const firewallEventsHandler = ({
event,
invalidateQueries,
queryClient,
}: EventHandlerData) => {
if (!event.entity) {
Expand All @@ -418,7 +419,7 @@ export const firewallEventsHandler = ({
switch (event.action) {
case 'firewall_delete':
// Invalidate firewall lists
queryClient.invalidateQueries({
invalidateQueries({
queryKey: firewallQueries.firewalls.queryKey,
});

Expand All @@ -428,7 +429,7 @@ export const firewallEventsHandler = ({
});
case 'firewall_create':
// Invalidate firewall lists
queryClient.invalidateQueries({
invalidateQueries({
queryKey: firewallQueries.firewalls.queryKey,
});
case 'firewall_device_add':
Expand All @@ -438,13 +439,9 @@ export const firewallEventsHandler = ({

// If a Linode is added or removed as a firewall device, invalidate it's firewalls
if (event.secondary_entity && event.secondary_entity.type === 'linode') {
queryClient.invalidateQueries({
queryKey: [
'linodes',
'linode',
event.secondary_entity.id,
'firewalls',
],
invalidateQueries({
queryKey: linodeQueries.linode(event.secondary_entity.id)._ctx
.firewalls.queryKey,
});
}

Expand All @@ -453,35 +450,31 @@ export const firewallEventsHandler = ({
event.secondary_entity &&
event.secondary_entity.type === 'nodebalancer'
) {
queryClient.invalidateQueries({
queryKey: [
'nodebalancers',
'nodebalancer',
event.secondary_entity.id,
'firewalls',
],
invalidateQueries({
queryKey: nodebalancerQueries.nodebalancer(event.secondary_entity.id)
._ctx.firewalls.queryKey,
});
}

// Invalidate the firewall
queryClient.invalidateQueries({
invalidateQueries({
queryKey: firewallQueries.firewall(event.entity.id).queryKey,
});

// Invalidate firewall lists
queryClient.invalidateQueries({
invalidateQueries({
queryKey: firewallQueries.firewalls.queryKey,
});
case 'firewall_disable':
case 'firewall_enable':
case 'firewall_rules_update':
case 'firewall_update':
// invalidate the firewall
queryClient.invalidateQueries({
invalidateQueries({
queryKey: firewallQueries.firewall(event.entity.id).queryKey,
});
// Invalidate firewall lists
queryClient.invalidateQueries({
invalidateQueries({
queryKey: firewallQueries.firewalls.queryKey,
});
}
Expand Down
8 changes: 4 additions & 4 deletions packages/manager/src/queries/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,13 @@ export const useUpdateImageRegionsMutation = (imageId: string) => {

export const imageEventsHandler = ({
event,
queryClient,
invalidateQueries,
}: EventHandlerData) => {
if (['failed', 'finished', 'notification'].includes(event.status)) {
queryClient.invalidateQueries({
invalidateQueries({
queryKey: imageQueries.all._def,
});
queryClient.invalidateQueries({ queryKey: imageQueries.paginated._def });
invalidateQueries({ queryKey: imageQueries.paginated._def });

if (event.entity) {
/*
Expand All @@ -194,7 +194,7 @@ export const imageEventsHandler = ({
*/

const imageId = `private/${event.entity.id}`;
queryClient.invalidateQueries({
invalidateQueries({
queryKey: imageQueries.image(imageId).queryKey,
});
}
Expand Down
Loading