Support locking cache file when writing and handles syntax error when reading (#21469)

This commit is contained in:
Cheena Malhotra
2023-01-04 11:50:17 -08:00
committed by GitHub
parent 5fbbc3a76b
commit 36484d38e6
3 changed files with 86 additions and 12 deletions

View File

@@ -4,42 +4,62 @@
*--------------------------------------------------------------------------------------------*/
import { ICachePlugin, TokenCacheContext } from '@azure/msal-node';
import { constants, promises as fsPromises } from 'fs';
import { promises as fsPromises } from 'fs';
import * as lockFile from 'lockfile';
import * as path from 'path';
import { AccountsClearTokenCacheCommand } from '../../constants';
import { Logger } from '../../utils/Logger';
export class MsalCachePluginProvider {
constructor(
private readonly _serviceName: string,
private readonly _msalFilePath: string
private readonly _msalFilePath: string,
) {
this._msalFilePath = path.join(this._msalFilePath, this._serviceName);
this._serviceName = this._serviceName.replace(/-/, '_');
Logger.verbose(`MsalCachePluginProvider: Using cache path ${_msalFilePath} and serviceName ${_serviceName}`);
}
private _lockTaken: boolean = false;
private getLockfilePath(): string {
return this._msalFilePath + '.lock';
}
public getCachePlugin(): ICachePlugin {
const lockFilePath = this.getLockfilePath();
const beforeCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
let exists = true;
await this.waitAndLock(lockFilePath);
try {
await fsPromises.access(this._msalFilePath, constants.R_OK | constants.W_OK);
} catch {
exists = false;
}
if (exists) {
const cache = await fsPromises.readFile(this._msalFilePath, { encoding: 'utf8' });
try {
const cache = await fsPromises.readFile(this._msalFilePath, { encoding: 'utf8' });
cacheContext.tokenCache.deserialize(cache);
Logger.verbose(`MsalCachePlugin: Token read from cache successfully.`);
} catch (e) {
Logger.error(`MsalCachePlugin: Failed to read from cache file. ${e}`);
// Handle deserialization error in cache file in case file gets corrupted.
// Clearing cache here will ensure account is marked stale so re-authentication can be triggered.
Logger.verbose(`MsalCachePlugin: Error occurred when trying to read cache file, file contents will be cleared: ${e.message}`);
await fsPromises.writeFile(this._msalFilePath, '', { encoding: 'utf8' });
}
Logger.verbose(`MsalCachePlugin: Token read from cache successfully.`);
} catch (e) {
if (e.code === 'ENOENT') {
// File doesn't exist, log and continue
Logger.verbose(`MsalCachePlugin: Cache file not found on disk: ${e.code}`);
}
else {
Logger.error(`MsalCachePlugin: Failed to read from cache file: ${e}`);
throw e;
}
} finally {
lockFile.unlockSync(lockFilePath);
this._lockTaken = false;
}
};
}
const afterCacheAccess = async (cacheContext: TokenCacheContext): Promise<void> => {
if (cacheContext.cacheHasChanged) {
await this.waitAndLock(lockFilePath);
try {
const data = cacheContext.tokenCache.serialize();
await fsPromises.writeFile(this._msalFilePath, data, { encoding: 'utf8' });
@@ -47,6 +67,9 @@ export class MsalCachePluginProvider {
} catch (e) {
Logger.error(`MsalCachePlugin: Failed to write to cache file. ${e}`);
throw e;
} finally {
lockFile.unlockSync(lockFilePath);
this._lockTaken = false;
}
}
};
@@ -61,4 +84,36 @@ export class MsalCachePluginProvider {
afterCacheAccess,
};
}
private async waitAndLock(lockFilePath: string): Promise<void> {
// Make 500 retry attempts with 100ms wait time between each attempt to allow enough time for the lock to be released.
const retries = 500;
const retryWait = 100;
// We cannot rely on lockfile.lockSync() to clear stale lockfile,
// so we check if the lockfile exists and if it does, calling unlockSync() will clear it.
if (lockFile.checkSync(lockFilePath) && !this._lockTaken) {
lockFile.unlockSync(lockFilePath);
Logger.verbose(`MsalCachePlugin: Stale lockfile found and has been removed.`);
}
let retryAttempt = 0;
while (retryAttempt <= retries) {
try {
// Use lockfile.lockSync() to ensure only one process is accessing the cache at a time.
// lockfile.lock() does not wait for async callback promise to resolve.
lockFile.lockSync(lockFilePath);
this._lockTaken = true;
break;
} catch (e) {
if (retryAttempt === retries) {
Logger.error(`MsalCachePlugin: Failed to acquire lock on cache file after ${retries} attempts.`);
throw new Error(`Failed to acquire lock on cache file after ${retries} attempts. Please attempt command: '${AccountsClearTokenCacheCommand}' to clear access token cache.`);
}
retryAttempt++;
Logger.verbose(`MsalCachePlugin: Failed to acquire lock on cache file. Retrying in ${retryWait} ms.`);
await new Promise(resolve => setTimeout(resolve, retryWait));
}
}
}
}