Skip to content

Commit b66e4c0

Browse files
authored
fix: full app profiling for changing worker ids (#1192)
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent 2258734 commit b66e4c0

5 files changed

Lines changed: 545 additions & 39 deletions

File tree

src/http/routes/admin/pprof.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const runtimeApiClient = vi.hoisted(() => ({
1919
close: vi.fn(),
2020
}))
2121
const waitForMultipartPprofWindowMock = vi.hoisted(() => vi.fn())
22+
const getManagementMock = vi.hoisted(() => vi.fn())
2223

2324
vi.mock('@platformatic/control', () => ({
2425
RuntimeApiClient: class MockRuntimeApiClient {
@@ -31,6 +32,7 @@ vi.mock('@platformatic/control', () => ({
3132

3233
vi.mock('@platformatic/globals', () => ({
3334
getGlobal: vi.fn(),
35+
getManagement: getManagementMock,
3436
}))
3537

3638
vi.mock('../../plugins/apikey', () => ({
@@ -296,6 +298,7 @@ describe('admin pprof routes', () => {
296298
applicationId: 'storage',
297299
workerId: 2,
298300
} as never)
301+
getManagementMock.mockReturnValue(undefined)
299302

300303
runtimeApiClient.getRuntimeApplications.mockResolvedValue({
301304
applications: [{ id: 'storage', workers: 2 }],
@@ -621,6 +624,80 @@ describe('admin pprof routes', () => {
621624
}
622625
})
623626

627+
it('retries whole-app captures with live worker ids from worker-not-found errors', async () => {
628+
runtimeApiClient.startApplicationProfiling.mockImplementation(
629+
async (_pid, applicationId: string) => {
630+
if (applicationId === 'storage:0' || applicationId === 'storage:1') {
631+
throw Object.assign(
632+
new Error(
633+
`Failed to start profiling for service "${applicationId}": ` +
634+
'Worker 0 of application storage not found. Available applications are: 4, 5, 6'
635+
),
636+
{
637+
code: 'PLT_CTR_FAILED_TO_START_PROFILING',
638+
}
639+
)
640+
}
641+
}
642+
)
643+
runtimeApiClient.stopApplicationProfiling
644+
.mockResolvedValueOnce(buildProfile('worker-four', 44).buffer)
645+
.mockResolvedValueOnce(buildProfile('worker-five', 55).buffer)
646+
.mockResolvedValueOnce(buildProfile('worker-six', 66).buffer)
647+
648+
const app = await buildApp()
649+
650+
try {
651+
const response = await app.inject({
652+
method: 'GET',
653+
url: '/heap?seconds=1',
654+
})
655+
656+
expect(response.statusCode).toBe(200)
657+
expect(response.headers['x-platformatic-worker-count']).toBe('3')
658+
expect(runtimeApiClient.startApplicationProfiling).toHaveBeenCalledTimes(5)
659+
for (const workerId of [0, 1, 4, 5, 6]) {
660+
expect(runtimeApiClient.startApplicationProfiling).toHaveBeenCalledWith(
661+
process.pid,
662+
`storage:${workerId}`,
663+
{
664+
type: 'heap',
665+
}
666+
)
667+
}
668+
expect(runtimeApiClient.stopApplicationProfiling).toHaveBeenCalledTimes(3)
669+
for (const [call, workerId] of [
670+
[1, 4],
671+
[2, 5],
672+
[3, 6],
673+
]) {
674+
expect(runtimeApiClient.stopApplicationProfiling).toHaveBeenNthCalledWith(
675+
call,
676+
process.pid,
677+
`storage:${workerId}`,
678+
{
679+
type: 'heap',
680+
}
681+
)
682+
}
683+
684+
const parts = parseMultipartParts(response.rawPayload, response.headers['content-type'])
685+
expect(JSON.parse(parts[0].body.toString('utf8'))).toMatchObject({
686+
event: 'started',
687+
workerCount: 3,
688+
})
689+
690+
const mergedProfile = Profile.decode(parts[1].body)
691+
expect(
692+
mergedProfile.sample
693+
.map((sample) => sample.value[0])
694+
.sort((left, right) => Number(left) - Number(right))
695+
).toEqual([44, 55, 66])
696+
} finally {
697+
await app.close()
698+
}
699+
})
700+
624701
it('rolls back already-started workers when a whole-app start partially fails', async () => {
625702
runtimeApiClient.startApplicationProfiling
626703
.mockResolvedValueOnce(undefined)

src/http/routes/admin/pprof.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
normalizeNodeModulesSourceMaps,
1515
PPROF_CONTROL_ERROR_CODES,
1616
resolvePprofFilenameTarget,
17+
resolveRuntimeWorkerIdsFromError,
1718
resolveWattPprofSelection,
19+
resolveWattPprofSelectionForWorkerIds,
1820
} from '@internal/monitoring/pprof/runtime'
1921
import type {
2022
ActivePprofSession,
@@ -200,18 +202,33 @@ async function startPprofSession(
200202
return session
201203
} catch (error) {
202204
activePprofSessions.delete(key)
205+
await stopProfilingTargets(client, startedTargets, options.type)
206+
throw error
207+
}
208+
}
203209

204-
if (startedTargets.length > 0) {
205-
await Promise.allSettled(
206-
startedTargets.map((target) =>
207-
client.stopApplicationProfiling(target.runtimePid, target.targetApplicationId, {
208-
type: options.type,
209-
})
210-
)
211-
)
210+
async function startPprofSessionWithLiveWorkerRetry(
211+
client: ProfilingRuntimeApiClient,
212+
selection: WattPprofSelection,
213+
options: PprofCaptureOptions
214+
) {
215+
try {
216+
return await startPprofSession(client, selection, options)
217+
} catch (error) {
218+
if (options.signal.aborted || selection.requestedWorkerId !== undefined) {
219+
throw error
212220
}
213221

214-
throw error
222+
const liveWorkerSelection = resolveWattPprofSelectionForWorkerIds(
223+
selection,
224+
resolveRuntimeWorkerIdsFromError(error)
225+
)
226+
227+
if (!liveWorkerSelection) {
228+
throw error
229+
}
230+
231+
return startPprofSession(client, liveWorkerSelection, options)
215232
}
216233
}
217234

@@ -221,7 +238,10 @@ async function captureAndSendPprof(
221238
client: ProfilingRuntimeApiClient = asProfilingRuntimeApiClient(new RuntimeApiClient())
222239
) {
223240
try {
224-
const selection = await resolveWattPprofSelection(client, options.workerId)
241+
let selection: WattPprofSelection | null = await resolveWattPprofSelection(
242+
client,
243+
options.workerId
244+
)
225245

226246
if (!selection) {
227247
return reply.status(501).send({
@@ -233,7 +253,8 @@ async function captureAndSendPprof(
233253
let writer: MultipartPprofWriter | undefined
234254

235255
try {
236-
session = await startPprofSession(client, selection, options)
256+
session = await startPprofSessionWithLiveWorkerRetry(client, selection, options)
257+
selection = session
237258
writer = createMultipartPprofWriter(reply, selection, options.type, options.seconds)
238259
await waitForMultipartPprofWindow(reply, writer, options.seconds, options.signal)
239260

0 commit comments

Comments
 (0)