Skip to content

Commit 14145cc

Browse files
authored
Use process polling to ensure DVCLive only PIDs are still running (#3045)
1 parent b96e24c commit 14145cc

File tree

5 files changed

+104
-7
lines changed

5 files changed

+104
-7
lines changed

extension/src/experiments/index.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { createTypedAccumulator } from '../util/object'
4141
import { pickPaths } from '../path/selection/quickPick'
4242
import { Toast } from '../vscode/toast'
4343
import { ConfigKey } from '../vscode/config'
44-
import { checkSignalFile } from '../fileSystem'
44+
import { checkSignalFile, pollSignalFileForProcess } from '../fileSystem'
4545
import { DVCLIVE_ONLY_RUNNING_SIGNAL_FILE } from '../cli/dvc/constants'
4646

4747
export const ExperimentsScale = {
@@ -94,6 +94,9 @@ export class Experiments extends BaseRepository<TableData> {
9494
private readonly internalCommands: InternalCommands
9595
private readonly webviewMessages: WebviewMessages
9696

97+
private dvcLiveOnlyCleanupInitialized = false
98+
private dvcLiveOnlySignalFile: string
99+
97100
constructor(
98101
dvcRoot: string,
99102
internalCommands: InternalCommands,
@@ -105,6 +108,11 @@ export class Experiments extends BaseRepository<TableData> {
105108
) {
106109
super(dvcRoot, resourceLocator.beaker)
107110

111+
this.dvcLiveOnlySignalFile = join(
112+
this.dvcRoot,
113+
DVCLIVE_ONLY_RUNNING_SIGNAL_FILE
114+
)
115+
108116
this.internalCommands = internalCommands
109117

110118
this.onDidChangeIsParamsFileFocused = this.paramsFileFocused.event
@@ -164,9 +172,8 @@ export class Experiments extends BaseRepository<TableData> {
164172
}
165173

166174
public async setState(data: ExperimentsOutput) {
167-
const dvcLiveOnly = await checkSignalFile(
168-
join(this.dvcRoot, DVCLIVE_ONLY_RUNNING_SIGNAL_FILE)
169-
)
175+
const dvcLiveOnly = await this.checkSignalFile()
176+
170177
const commitMessages: { [sha: string]: string } =
171178
await this.internalCommands.executeCommand(
172179
AvailableCommands.GIT_GET_LAST_THREE_COMMIT_MESSAGES,
@@ -602,4 +609,19 @@ export class Experiments extends BaseRepository<TableData> {
602609
this.onDidChangeColumns
603610
)
604611
}
612+
613+
private async checkSignalFile() {
614+
const dvcLiveOnly = await checkSignalFile(this.dvcLiveOnlySignalFile)
615+
616+
if (dvcLiveOnly && !this.dvcLiveOnlyCleanupInitialized) {
617+
this.dvcLiveOnlyCleanupInitialized = true
618+
pollSignalFileForProcess(this.dvcLiveOnlySignalFile, () => {
619+
this.dvcLiveOnlyCleanupInitialized = false
620+
if (this.hasRunningExperiment()) {
621+
this.cliData.update()
622+
}
623+
})
624+
}
625+
return dvcLiveOnly
626+
}
605627
}

extension/src/fileSystem/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createValidInteger } from '../util/number'
1818
import { processExists } from '../processExecution'
1919
import { getFirstWorkspaceFolder } from '../vscode/workspaceFolders'
2020
import { DOT_DVC } from '../cli/dvc/constants'
21+
import { delay } from '../util/time'
2122

2223
export const exists = (path: string): boolean => existsSync(path)
2324

@@ -176,6 +177,19 @@ export const checkSignalFile = async (path: string): Promise<boolean> => {
176177
return !!(await getPidFromSignalFile(path))
177178
}
178179

180+
export const pollSignalFileForProcess = async (
181+
path: string,
182+
callback: () => void,
183+
ms = 5000
184+
): Promise<void> => {
185+
await delay(ms)
186+
const signalIsValid = await checkSignalFile(path)
187+
if (signalIsValid) {
188+
return pollSignalFileForProcess(path, callback, ms)
189+
}
190+
return callback()
191+
}
192+
179193
export const getBinDisplayText = (
180194
path: string | undefined
181195
): string | undefined => {

extension/src/test/suite/experiments/index.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { PlotSizeNumber } from '../../../plots/webview/contract'
7171
import { RegisteredCommands } from '../../../commands/external'
7272
import { ConfigKey } from '../../../vscode/config'
7373
import { EXPERIMENT_WORKSPACE_ID } from '../../../cli/dvc/contract'
74+
import * as Time from '../../../util/time'
7475

7576
suite('Experiments Test Suite', () => {
7677
const disposable = Disposable.fn()
@@ -1626,4 +1627,55 @@ suite('Experiments Test Suite', () => {
16261627
).to.deep.equal([])
16271628
})
16281629
})
1630+
1631+
describe('setState', () => {
1632+
it('should clean up after a killed DVCLive process that was running an experiment outside of the DVC context', async () => {
1633+
const defaultExperimentsData = { workspace: { baseline: { data: {} } } }
1634+
1635+
const { experiments, mockCheckSignalFile, mockUpdateExperimentsData } =
1636+
buildExperiments(disposable, defaultExperimentsData)
1637+
1638+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1639+
const getCleanupInitialized = (experiments: any) =>
1640+
experiments.dvcLiveOnlyCleanupInitialized
1641+
1642+
await experiments.isReady()
1643+
1644+
const mockDelay = stub(Time, 'delay')
1645+
mockDelay.callsFake(() => mockDelay.wrappedMethod(500))
1646+
1647+
let processKilled = false
1648+
1649+
const cleanupUpdate = new Promise(resolve =>
1650+
mockUpdateExperimentsData.callsFake(() => resolve(undefined))
1651+
)
1652+
1653+
mockCheckSignalFile.resetBehavior()
1654+
mockCheckSignalFile.callsFake(() => {
1655+
return Promise.resolve(!processKilled)
1656+
})
1657+
1658+
const dataUpdated = new Promise(resolve =>
1659+
disposable.track(
1660+
experiments.onDidChangeExperiments(() => resolve(undefined))
1661+
)
1662+
)
1663+
1664+
experiments.setState(defaultExperimentsData)
1665+
await dataUpdated
1666+
expect(experiments.hasRunningExperiment()).to.be.true
1667+
expect(getCleanupInitialized(experiments)).to.be.true
1668+
1669+
processKilled = true
1670+
1671+
mockUpdateExperimentsData.resetHistory()
1672+
1673+
await cleanupUpdate
1674+
1675+
expect(getCleanupInitialized(experiments)).to.be.false
1676+
expect(mockCheckSignalFile).to.be.called
1677+
expect(mockDelay).to.be.called
1678+
expect(mockUpdateExperimentsData).to.be.calledOnce
1679+
})
1680+
})
16291681
})

extension/src/test/suite/experiments/util.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,19 @@ export const buildExperiments = (
6060
resourceLocator
6161
} = buildDependencies(disposer, experimentShowData)
6262

63+
const mockUpdateExperimentsData = stub()
64+
const mockExperimentsData = buildMockData<ExperimentsData>(
65+
mockUpdateExperimentsData
66+
)
67+
6368
const experiments = disposer.track(
6469
new Experiments(
6570
dvcRoot,
6671
internalCommands,
6772
updatesPaused,
6873
resourceLocator,
6974
buildMockMemento(),
70-
buildMockData<ExperimentsData>(),
75+
mockExperimentsData,
7176
buildMockData<FileSystemData>()
7277
)
7378
)
@@ -90,6 +95,7 @@ export const buildExperiments = (
9095
messageSpy,
9196
mockCheckSignalFile,
9297
mockExperimentShow,
98+
mockUpdateExperimentsData,
9399
resourceLocator,
94100
updatesPaused
95101
}

extension/src/test/suite/util.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,13 @@ export const buildInternalCommands = (disposer: Disposer) => {
164164
}
165165
}
166166

167-
export const buildMockData = <T extends ExperimentsData | FileSystemData>() =>
167+
export const buildMockData = <T extends ExperimentsData | FileSystemData>(
168+
update = stub()
169+
) =>
168170
({
169171
dispose: stub(),
170-
onDidUpdate: stub()
172+
onDidUpdate: stub(),
173+
update
171174
} as unknown as T)
172175

173176
export const buildDependencies = (

0 commit comments

Comments
 (0)