files - lock all resources involved in atomic writes (#194009)

* files - lock all resources involved in atomic writes

* do not acquire locks in random order

* add test

* stop support for buffered atomic writes
This commit is contained in:
Benjamin Pasero
2023-09-26 05:12:04 +02:00
committed by GitHub
parent 166097a20c
commit 37b60398e5
3 changed files with 80 additions and 66 deletions

View File

@@ -14,7 +14,7 @@ import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { Schemas } from 'vs/base/common/network'; import { Schemas } from 'vs/base/common/network';
import { mark } from 'vs/base/common/performance'; import { mark } from 'vs/base/common/performance';
import { basename, dirname, extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath, joinPath } from 'vs/base/common/resources'; import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/common/resources';
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream'; import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
@@ -408,17 +408,7 @@ export class FileService extends Disposable implements IFileService {
// write file: buffered // write file: buffered
else { else {
const contents = bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream; await this.doWriteBuffered(provider, resource, writeFileOptions, bufferOrReadableOrStreamOrBufferedStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStreamOrBufferedStream) : bufferOrReadableOrStreamOrBufferedStream);
// atomic write
if (writeFileOptions?.atomic !== false && writeFileOptions?.atomic?.postfix) {
await this.doWriteBufferedAtomic(provider, resource, joinPath(dirname(resource), `${basename(resource)}${writeFileOptions.atomic.postfix}`), writeFileOptions, contents);
}
// non-atomic write
else {
await this.doWriteBuffered(provider, resource, writeFileOptions, contents);
}
} }
// events // events
@@ -442,7 +432,11 @@ export class FileService extends Disposable implements IFileService {
const atomic = !!options?.atomic; const atomic = !!options?.atomic;
if (atomic) { if (atomic) {
if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) { if (!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicWrite)) {
throw new Error(localize('writeFailedAtomicUnsupported', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource))); throw new Error(localize('writeFailedAtomicUnsupported1', "Unable to atomically write file '{0}' because provider does not support it.", this.resourceForError(resource)));
}
if (!(provider.capabilities & FileSystemProviderCapabilities.FileReadWrite)) {
throw new Error(localize('writeFailedAtomicUnsupported2', "Unable to atomically write file '{0}' because provider does not support unbuffered writes.", this.resourceForError(resource)));
} }
if (unlock) { if (unlock) {
@@ -1183,28 +1177,6 @@ export class FileService extends Disposable implements IFileService {
private readonly writeQueue = this._register(new ResourceQueue()); private readonly writeQueue = this._register(new ResourceQueue());
private async doWriteBufferedAtomic(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, tempResource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
// Write to temp resource first
await this.doWriteBuffered(provider, tempResource, options, readableOrStreamOrBufferedStream);
try {
// Rename over existing to ensure atomic replace
await provider.rename(tempResource, resource, { overwrite: true });
} catch (error) {
// Cleanup in case of rename error
try {
await provider.delete(tempResource, { recursive: false, useTrash: false, atomic: false });
} catch (error) {
// ignore - we want the outer error to bubble up
}
throw error;
}
}
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> { private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, options: IWriteFileOptions | undefined, readableOrStreamOrBufferedStream: VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream): Promise<void> {
return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => { return this.writeQueue.queueFor(resource, this.getExtUri(provider).providerExtUri).queue(async () => {

View File

@@ -252,8 +252,18 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
private async doWriteFileAtomic(resource: URI, tempResource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> { private async doWriteFileAtomic(resource: URI, tempResource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> {
// Ensure to create locks for all resources involved
// since atomic write involves mutiple disk operations
// and resources.
const locks = new DisposableStore();
try {
locks.add(await this.createResourceLock(resource));
locks.add(await this.createResourceLock(tempResource));
// Write to temp resource first // Write to temp resource first
await this.doWriteFile(tempResource, content, opts); await this.doWriteFile(tempResource, content, opts, true /* disable write lock */);
try { try {
@@ -271,9 +281,12 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
throw error; throw error;
} }
} finally {
locks.dispose();
}
} }
private async doWriteFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions): Promise<void> { private async doWriteFile(resource: URI, content: Uint8Array, opts: IFileWriteOptions, disableWriteLock?: boolean): Promise<void> {
let handle: number | undefined = undefined; let handle: number | undefined = undefined;
try { try {
const filePath = this.toFilePath(resource); const filePath = this.toFilePath(resource);
@@ -293,7 +306,7 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
} }
// Open // Open
handle = await this.open(resource, { create: true, unlock: opts.unlock }); handle = await this.open(resource, { create: true, unlock: opts.unlock }, disableWriteLock);
// Write content at once // Write content at once
await this.write(handle, 0, content, 0, content.byteLength); await this.write(handle, 0, content, 0, content.byteLength);
@@ -317,14 +330,14 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
DiskFileSystemProvider.canFlush = enabled; DiskFileSystemProvider.canFlush = enabled;
} }
async open(resource: URI, opts: IFileOpenOptions): Promise<number> { async open(resource: URI, opts: IFileOpenOptions, disableWriteLock?: boolean): Promise<number> {
const filePath = this.toFilePath(resource); const filePath = this.toFilePath(resource);
// Writes: guard multiple writes to the same resource // Writes: guard multiple writes to the same resource
// behind a single lock to prevent races when writing // behind a single lock to prevent races when writing
// from multiple places at the same time to the same file // from multiple places at the same time to the same file
let lock: IDisposable | undefined = undefined; let lock: IDisposable | undefined = undefined;
if (isFileOpenForWriteOptions(opts)) { if (isFileOpenForWriteOptions(opts) && !disableWriteLock) {
lock = await this.createResourceLock(resource); lock = await this.createResourceLock(resource);
} }
@@ -756,13 +769,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
const locks = new DisposableStore(); const locks = new DisposableStore();
try { try {
const [fromLock, toLock] = await Promise.all([ locks.add(await this.createResourceLock(from));
this.createResourceLock(from), locks.add(await this.createResourceLock(to));
this.createResourceLock(to)
]);
locks.add(fromLock);
locks.add(toLock);
if (mkdir) { if (mkdir) {
await Promises.mkdir(dirname(toFilePath), { recursive: true }); await Promises.mkdir(dirname(toFilePath), { recursive: true });

View File

@@ -146,16 +146,13 @@ flakySuite('Disk File Service', function () {
setup(async () => { setup(async () => {
const logService = new NullLogService(); const logService = new NullLogService();
service = new FileService(logService); service = disposables.add(new FileService(logService));
disposables.add(service);
fileProvider = new TestDiskFileSystemProvider(logService); fileProvider = disposables.add(new TestDiskFileSystemProvider(logService));
disposables.add(service.registerProvider(Schemas.file, fileProvider)); disposables.add(service.registerProvider(Schemas.file, fileProvider));
disposables.add(fileProvider);
testProvider = new TestDiskFileSystemProvider(logService); testProvider = disposables.add(new TestDiskFileSystemProvider(logService));
disposables.add(service.registerProvider(testSchema, testProvider)); disposables.add(service.registerProvider(testSchema, testProvider));
disposables.add(testProvider);
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice'); testDir = getRandomTestPath(tmpdir(), 'vsctests', 'diskfileservice');
@@ -1818,7 +1815,14 @@ flakySuite('Disk File Service', function () {
test('writeFile - buffered (atomic)', async () => { test('writeFile - buffered (atomic)', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAtomicWrite); setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAtomicWrite);
return testWriteFile(true); let e;
try {
await testWriteFile(true);
} catch (error) {
e = error;
}
assert.ok(e);
}); });
test('writeFile - unbuffered (atomic)', async () => { test('writeFile - unbuffered (atomic)', async () => {
@@ -1869,7 +1873,14 @@ flakySuite('Disk File Service', function () {
test('writeFile (large file) - buffered (atomic)', async () => { test('writeFile (large file) - buffered (atomic)', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAtomicWrite); setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileAtomicWrite);
return testWriteFileLarge(true); let e;
try {
await testWriteFileLarge(true);
} catch (error) {
e = error;
}
assert.ok(e);
}); });
test('writeFile (large file) - unbuffered (atomic)', async () => { test('writeFile (large file) - unbuffered (atomic)', async () => {
@@ -1890,6 +1901,29 @@ flakySuite('Disk File Service', function () {
assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent); assert.strictEqual(readFileSync(resource.fsPath).toString(), newContent);
} }
test('writeFile (large file) - unbuffered (atomic) - concurrent writes with multiple services', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileAtomicWrite);
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
const promises: Promise<IFileStatWithMetadata>[] = [];
let suffix = 0;
for (let i = 0; i < 10; i++) {
const service = disposables.add(new FileService(new NullLogService()));
disposables.add(service.registerProvider(Schemas.file, fileProvider));
promises.push(service.writeFile(resource, VSBuffer.fromString(`${newContent}${++suffix}`), { atomic: { postfix: '.vsctmp' } }));
await timeout(0);
}
await Promise.allSettled(promises);
assert.strictEqual(readFileSync(resource.fsPath).toString(), `${newContent}${suffix}`);
});
test('writeFile - buffered - readonly throws', async () => { test('writeFile - buffered - readonly throws', async () => {
setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.Readonly); setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.Readonly);