Skip to content

Commit 005b4b4

Browse files
terencehonlesngwattcos
authored andcommitted
perf(ngcc): allow immediately reporting a stale lock file (angular#37250)
Currently, if an ngcc process is killed in a manner that it doesn't clean up its lock file (or is killed too quickly) the compiler reports that it is waiting on the PID of a process that doesn't exist, and that it will wait up to a maximum of N seconds. This PR updates the locking code to additionally check if the process exists, and if it does not it will immediately bail out, and print the location of the lock file so a user may clean it up. PR Close angular#37250
1 parent b0afff9 commit 005b4b4

File tree

2 files changed

+104
-10
lines changed

2 files changed

+104
-10
lines changed

packages/compiler-cli/ngcc/src/locking/async_locker.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,68 @@ export class AsyncLocker {
5656
pid = newPid;
5757
}
5858
if (attempts === 0) {
59-
this.logger.info(
59+
// Check to see if the process identified by the PID is still running. Because the
60+
// process *should* clean up after itself, we only check for a stale lock file when the
61+
// PID changes and only once. This may mean you have to wait if the process is killed
62+
// after the first check and isn't given the chance to clean up after itself.
63+
if (!this.isProcessRunning(pid)) {
64+
// try to re-lock one last time in case there was a race condition checking the process.
65+
try {
66+
return this.lockFile.write();
67+
} catch (e2) {
68+
if (e2.code !== 'EEXIST') {
69+
throw e2;
70+
}
71+
}
72+
73+
// finally check that the lock was held by the same process this whole time.
74+
const finalPid = this.lockFile.read();
75+
if (finalPid === pid) {
76+
throw new TimeoutError(this.lockFileMessage(
77+
`Lock found, but no process with PID ${pid} seems to be running.`));
78+
} else {
79+
// attempts is still 0, but adjust the PID so the message below is correct.
80+
pid = finalPid;
81+
}
82+
}
83+
84+
this.logger.info(this.lockFileMessage(
6085
`Another process, with id ${pid}, is currently running ngcc.\n` +
61-
`Waiting up to ${this.retryDelay * this.retryAttempts / 1000}s for it to finish.\n` +
62-
`(If you are sure no ngcc process is running then you should delete the lock-file at ${
63-
this.lockFile.path}.)`);
86+
`Waiting up to ${this.retryDelay * this.retryAttempts / 1000}s for it to finish.`));
6487
}
6588
// The file is still locked by another process so wait for a bit and retry
6689
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
6790
}
6891
}
6992
// If we fall out of the loop then we ran out of rety attempts
70-
throw new TimeoutError(
71-
`Timed out waiting ${
72-
this.retryAttempts * this.retryDelay /
73-
1000}s for another ngcc process, with id ${pid}, to complete.\n` +
74-
`(If you are sure no ngcc process is running then you should delete the lock-file at ${
75-
this.lockFile.path}.)`);
93+
throw new TimeoutError(this.lockFileMessage(`Timed out waiting ${
94+
this.retryAttempts * this.retryDelay /
95+
1000}s for another ngcc process, with id ${pid}, to complete.`));
96+
}
97+
98+
protected isProcessRunning(pid: string): boolean {
99+
// let the normal logic run if this is not called with a valid PID
100+
if (isNaN(+pid)) {
101+
this.logger.debug(`Cannot check if invalid PID "${pid}" is running, a number is expected.`);
102+
return true;
103+
}
104+
105+
try {
106+
process.kill(+pid, 0);
107+
return true;
108+
} catch (e) {
109+
// If the process doesn't exist ESRCH will be thrown, if the error is not that, throw it.
110+
if (e.code !== 'ESRCH') {
111+
throw e;
112+
}
113+
114+
return false;
115+
}
116+
}
117+
118+
private lockFileMessage(message: string): string {
119+
return message +
120+
`\n(If you are sure no ngcc process is running then you should delete the lock-file at ${
121+
this.lockFile.path}.)`;
76122
}
77123
}

packages/compiler-cli/ngcc/test/locking/async_locker_spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ runInEachFileSystem(() => {
7171
}
7272
return lockFileContents;
7373
});
74+
spyOn(process, 'kill').and.returnValue();
7475

7576
const promise = locker.lock(async () => log.push('fn()'));
7677
// The lock is now waiting on the lock-file becoming free, so no `fn()` in the log.
@@ -80,6 +81,7 @@ runInEachFileSystem(() => {
8081
`(If you are sure no ngcc process is running then you should delete the lock-file at ${
8182
lockFile.path}.)`
8283
]]);
84+
expect(process.kill).toHaveBeenCalledWith(188, 0);
8385

8486
lockFileContents = null;
8587
// The lock-file has been removed, so we can create our own lock-file, call `fn()` and then
@@ -88,6 +90,47 @@ runInEachFileSystem(() => {
8890
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'fn()', 'remove()']);
8991
});
9092

93+
it('should fail fast when waiting on a dead process', async () => {
94+
const fs = getFileSystem();
95+
const log: string[] = [];
96+
const lockFile = new MockLockFile(fs, log);
97+
const logger = new MockLogger();
98+
const locker = new AsyncLocker(lockFile, logger, 100, 10);
99+
100+
let lockFileContents: string|null = '188';
101+
spyOn(lockFile, 'write').and.callFake(() => {
102+
log.push('write()');
103+
if (lockFileContents) {
104+
throw {code: 'EEXIST'};
105+
}
106+
});
107+
spyOn(lockFile, 'read').and.callFake(() => {
108+
log.push('read() => ' + lockFileContents);
109+
if (lockFileContents === null) {
110+
throw {code: 'ENOENT'};
111+
}
112+
return lockFileContents;
113+
});
114+
spyOn(process, 'kill').and.callFake(() => {
115+
throw {code: 'ESRCH'};
116+
});
117+
118+
const promise = locker.lock(async () => log.push('fn()'));
119+
// The lock has already failed so no `fn()` in the log.
120+
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 188']);
121+
expect(logger.logs.info).toEqual([]);
122+
expect(process.kill).toHaveBeenCalledWith(188, 0);
123+
// Check that a missing process errors out.
124+
let error: Error;
125+
await promise.catch(e => error = e);
126+
expect(log).toEqual(['write()', 'read() => 188', 'write()', 'read() => 188']);
127+
expect(error!.message)
128+
.toEqual(
129+
`Lock found, but no process with PID 188 seems to be running.\n` +
130+
`(If you are sure no ngcc process is running then you should delete the lock-file at ${
131+
lockFile.path}.)`);
132+
});
133+
91134
it('should extend the retry timeout if the other process locking the file changes', async () => {
92135
const fs = getFileSystem();
93136
const log: string[] = [];
@@ -109,6 +152,7 @@ runInEachFileSystem(() => {
109152
}
110153
return lockFileContents;
111154
});
155+
spyOn(process, 'kill').and.returnValue();
112156

113157
const promise = locker.lock(async () => log.push('fn()'));
114158
// The lock is now waiting on the lock-file becoming free, so no `fn()` in the log.
@@ -118,6 +162,7 @@ runInEachFileSystem(() => {
118162
`(If you are sure no ngcc process is running then you should delete the lock-file at ${
119163
lockFile.path}.)`
120164
]]);
165+
expect(process.kill).toHaveBeenCalledWith(188, 0);
121166

122167
lockFileContents = '444';
123168
// The lock-file has been taken over by another process - wait for the next attempt
@@ -131,6 +176,7 @@ runInEachFileSystem(() => {
131176
`(If you are sure no ngcc process is running then you should delete the lock-file at ${
132177
lockFile.path}.)`]
133178
]);
179+
expect(process.kill).toHaveBeenCalledWith(444, 0);
134180

135181
lockFileContents = null;
136182
// The lock-file has been removed, so we can create our own lock-file, call `fn()` and
@@ -163,11 +209,13 @@ runInEachFileSystem(() => {
163209
}
164210
return lockFileContents;
165211
});
212+
spyOn(process, 'kill').and.returnValue();
166213

167214
const promise = locker.lock(async () => log.push('fn()'));
168215

169216
// The lock is now waiting on the lock-file becoming free, so no `fn()` in the log.
170217
expect(log).toEqual(['write()', 'read() => 188']);
218+
expect(process.kill).toHaveBeenCalledWith(188, 0);
171219
// Do not remove the lock-file and let the call to `lock()` timeout.
172220
let error: Error;
173221
await promise.catch(e => error = e);

0 commit comments

Comments
 (0)