fix(server): index page-layout FKs to fix workspace cleanup timeout (#23289)

The cleanSuspendedWorkspacesJob cron timed out every run (Sentry monitor
"a timeout check-in was detected"): hard-deleting soft-deleted
workspaces hung on `DELETE FROM core.pageLayout`, hit the 10s query
timeout, rolled back, so those workspaces were never destroyed and got
retried hourly.

Root cause: the FKs in the pageLayout -> pageLayoutTab ->
pageLayoutWidget tree had no usable index on the referencing column. The
existing indexes lead with workspaceId and are partial ("deletedAt" IS
NULL), so ON DELETE CASCADE / SET NULL fell back to full sequential
scans of the shared core tables per deleted row; on layout-heavy
workspaces this exceeded 10s.

- Add non-partial FK-column indexes on pageLayoutTab(pageLayoutId) and
pageLayoutWidget(pageLayoutTabId)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23289?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-07-24 18:50:57 +02:00
committed by GitHub
parent 45f92b6763
commit 3a1067ec6d
4 changed files with 36 additions and 2 deletions
@@ -0,0 +1,30 @@
import { type QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
const PAGE_LAYOUT_TAB_PAGE_LAYOUT_ID_INDEX_NAME =
'IDX_PAGE_LAYOUT_TAB_PAGE_LAYOUT_ID';
const PAGE_LAYOUT_WIDGET_PAGE_LAYOUT_TAB_ID_INDEX_NAME =
'IDX_PAGE_LAYOUT_WIDGET_PAGE_LAYOUT_TAB_ID';
@RegisteredInstanceCommand('2.25.0', 1784904030251)
export class AddPageLayoutCascadeDeleteIndexesFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "${PAGE_LAYOUT_TAB_PAGE_LAYOUT_ID_INDEX_NAME}" ON "core"."pageLayoutTab" ("pageLayoutId")`,
);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "${PAGE_LAYOUT_WIDGET_PAGE_LAYOUT_TAB_ID_INDEX_NAME}" ON "core"."pageLayoutWidget" ("pageLayoutTabId")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."${PAGE_LAYOUT_WIDGET_PAGE_LAYOUT_TAB_ID_INDEX_NAME}"`,
);
await queryRunner.query(
`DROP INDEX IF EXISTS "core"."${PAGE_LAYOUT_TAB_PAGE_LAYOUT_ID_INDEX_NAME}"`,
);
}
}
@@ -124,6 +124,7 @@ import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23
import { AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784712843602-add-on-connect-logic-function-to-connection-provider';
import { RepairKeyValuePairApplicationIdFastInstanceCommand } from './2-24/2-24-instance-command-fast-1784897347051-repair-key-value-pair-application-id';
import { AddAgentForeignKeyToRoleTargetFastInstanceCommand } from './2-25/2-25-instance-command-fast-1784820332810-add-agent-foreign-key-to-role-target';
import { AddPageLayoutCascadeDeleteIndexesFastInstanceCommand } from './2-25/2-25-instance-command-fast-1784904030251-add-page-layout-cascade-delete-indexes';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -250,4 +251,5 @@ export const INSTANCE_COMMANDS = [
AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand,
RepairKeyValuePairApplicationIdFastInstanceCommand,
AddAgentForeignKeyToRoleTargetFastInstanceCommand,
AddPageLayoutCascadeDeleteIndexesFastInstanceCommand,
];
@@ -15,8 +15,8 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutEntity } from 'src/engine/metadata-modules/page-layout/entities/page-layout.entity';
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
@@ -34,6 +34,7 @@ export type PageLayoutTabOverrides = {
['workspaceId', 'pageLayoutId'],
{ where: '"deletedAt" IS NULL' },
)
@Index('IDX_PAGE_LAYOUT_TAB_PAGE_LAYOUT_ID', ['pageLayoutId'])
export class PageLayoutTabEntity
extends OverridableEntity<PageLayoutTabOverrides>
implements Required<PageLayoutTabEntity>
@@ -19,8 +19,8 @@ import {
type Relation,
} from 'typeorm';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
@@ -45,6 +45,7 @@ export type PageLayoutWidgetOverrides = {
{ where: '"deletedAt" IS NULL' },
)
@Index('IDX_PAGE_LAYOUT_WIDGET_OBJECT_METADATA_ID', ['objectMetadataId'])
@Index('IDX_PAGE_LAYOUT_WIDGET_PAGE_LAYOUT_TAB_ID', ['pageLayoutTabId'])
export class PageLayoutWidgetEntity<
TWidgetConfigurationType extends WidgetConfigurationType =
WidgetConfigurationType,