Skip to content

Commit 3901904

Browse files
authored
fix: create queue for Tiles Find Single Row action (#865)
## Problem * The findSingleRow action in Tiles encounters ‘Throughput Limit Exceeded’ errors when there are a high number of executions attempting to find rows. ## Solution * Create a queue to rate limit the number of findSingleRow actions **BEFORE**: <img width="1512" alt="Screenshot 2025-02-17 at 3 27 40 PM" src="https://github.com/user-attachments/assets/f53ff49c-1cde-44c8-a7a1-faad17ddaf8f" /> **AFTER**: <img width="1496" alt="Screenshot 2025-02-17 at 3 29 49 PM" src="https://github.com/user-attachments/assets/bfef0b2e-2b27-4799-af16-8d2026435994" /> ## Tests - [x] Test that existing executions with Tile actions execute as execpted - [x] Verify from admin dashboard that multiple concurrent findSingleRow actions on the same Tile are rate limited, and that only ## Environment Variables Note that rate limit defaults to 1 findSingleRow action per second. If a different rate limit is desired, create a new environment variable in the parameter store named `plumber-<ENVIRONMENT>-tiles-interval-between-find-single-row-ms`
1 parent 37120aa commit 3901904

File tree

6 files changed

+134
-0
lines changed

6 files changed

+134
-0
lines changed

ecs/env.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@
221221
{
222222
"name": "ADMIN_JWT_SECRET_KEY",
223223
"valueFrom": "plumber-<ENVIRONMENT>-admin-jwt-secret-key"
224+
},
225+
{
226+
"name": "TILES_INTERVAL_BETWEEN_FIND_SINGLE_ROW_MS",
227+
"valueFrom": "plumber-<ENVIRONMENT>-tiles-interval-between-find-single-row-ms"
224228
}
225229
]
226230
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Avoid cyclic imports when importing m365ExcelApp
2+
import '@/apps'
3+
4+
import { afterEach, describe, expect, it, vi } from 'vitest'
5+
6+
import tilesApp from '..'
7+
8+
const mocks = vi.hoisted(() => ({
9+
stepQueryResult: vi.fn(),
10+
}))
11+
12+
vi.mock('@/models/step', () => ({
13+
default: {
14+
query: vi.fn(() => ({
15+
findById: vi.fn(() => ({
16+
throwIfNotFound: mocks.stepQueryResult,
17+
})),
18+
})),
19+
},
20+
}))
21+
22+
describe('Queue config', () => {
23+
afterEach(() => {
24+
vi.restoreAllMocks()
25+
})
26+
27+
it('configures a delayable queue', () => {
28+
expect(tilesApp.queue.isQueueDelayable).toEqual(true)
29+
})
30+
31+
it('sets group ID to the file ID', async () => {
32+
mocks.stepQueryResult.mockResolvedValueOnce({
33+
parameters: {
34+
tableId: 'mock-table-id',
35+
},
36+
key: 'findSingleRow',
37+
appKey: 'tiles',
38+
})
39+
const groupConfig = await tilesApp.queue.getGroupConfigForJob({
40+
flowId: 'test-flow-id',
41+
stepId: 'test-step-id',
42+
executionId: 'test-step-id',
43+
})
44+
expect(groupConfig).toEqual({
45+
id: 'mock-table-id-findSingleRow',
46+
})
47+
})
48+
49+
it('sets group ID to null', async () => {
50+
mocks.stepQueryResult.mockResolvedValueOnce({
51+
parameters: {
52+
tableId: 'mock-table-id',
53+
},
54+
key: 'createRow',
55+
appKey: 'tiles',
56+
})
57+
const groupConfig = await tilesApp.queue.getGroupConfigForJob({
58+
flowId: 'test-flow-id',
59+
stepId: 'test-step-id',
60+
executionId: 'test-step-id',
61+
})
62+
expect(groupConfig).toEqual(null)
63+
})
64+
65+
it('sets group concurrency to 1', () => {
66+
expect(tilesApp.queue.groupLimits).toEqual({
67+
type: 'concurrency',
68+
concurrency: 1,
69+
})
70+
})
71+
72+
it('avoids bursting via a leaky bucket approach', () => {
73+
expect(tilesApp.queue.queueRateLimit.max).toEqual(1)
74+
})
75+
})

packages/backend/src/apps/tiles/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IApp } from '@plumber/types'
33
import getTransferDetails from './common/get-transfer-details'
44
import actions from './actions'
55
import dynamicData from './dynamic-data'
6+
import queue from './queue'
67

78
const app: IApp = {
89
name: 'Tiles',
@@ -17,6 +18,7 @@ const app: IApp = {
1718
actions,
1819
dynamicData,
1920
getTransferDetails,
21+
queue,
2022
}
2123

2224
export default app
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { IAppQueue } from '@plumber/types'
2+
3+
import { TILES_INTERVAL_BETWEEN_FIND_SINGLE_ROW_MS } from '@/config/app-env-vars/tiles'
4+
import Step from '@/models/step'
5+
6+
// Define actions that should be queued
7+
const QUEUED_ACTIONS = new Set(['findSingleRow'])
8+
9+
//
10+
// This config sets up a per-Tile findSingleRow action queue, i.e., only findSingleRow
11+
// actions are grouped and rate limited.
12+
// All other Tile actions are not grouped and do not need to be rate limited.
13+
//
14+
// This is necessary because the findSingleRow action is the most expensive
15+
// operation in the Tile app, due to the need to scan the entire DynamoDB table.
16+
//
17+
18+
const getGroupConfigForJob: IAppQueue['getGroupConfigForJob'] = async (
19+
jobData,
20+
) => {
21+
const step = await Step.query().findById(jobData.stepId).throwIfNotFound()
22+
const tableId = step.parameters['tableId'] as string
23+
24+
if (QUEUED_ACTIONS.has(step.key)) {
25+
return {
26+
id: `${tableId}-${step.key}`,
27+
}
28+
}
29+
30+
// All other Tile actions are not grouped and do not need to be rate limited
31+
// as the write operations are fast and less expensive.
32+
return null
33+
}
34+
35+
const queueSettings = {
36+
getGroupConfigForJob,
37+
groupLimits: {
38+
type: 'concurrency',
39+
concurrency: 1,
40+
},
41+
isQueueDelayable: true,
42+
queueRateLimit: {
43+
max: 1,
44+
duration: TILES_INTERVAL_BETWEEN_FIND_SINGLE_ROW_MS,
45+
},
46+
} satisfies IAppQueue
47+
48+
export default queueSettings

packages/backend/src/config/app-env-vars/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@
2424
import './m365'
2525
import './formsg'
2626
import './postman-sms'
27+
import './tiles'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Default to 1 action per second.
2+
export const TILES_INTERVAL_BETWEEN_FIND_SINGLE_ROW_MS = Number(
3+
process.env.TILES_INTERVAL_BETWEEN_FIND_SINGLE_ROW_MS ?? '1000',
4+
)

0 commit comments

Comments
 (0)