Improved atomicity

This commit is contained in:
didier amyot committed 2023-06-13 12:24:57 -04:00
1 parent b70628264e
commit b8b3e348e8
3 files changed
+48 -15

No files matched your search

+19 -7
View File
@@ -14,6 +14,7 @@ interface SerializedFile {
path: string; path: string;
contentSize: number; contentSize: number;
compression: CompressionAlgorithm; compression: CompressionAlgorithm;
creationTime: number;
} }
interface SerializedFilePathIndex { interface SerializedFilePathIndex {
@@ -43,8 +44,11 @@ export class DenoKVBlobService implements BlobService {
} }
const blob_id = await this.sha1(content); const blob_id = await this.sha1(content);
await this.kv.set(['blob_index', blobIndex, chunckIndex], blob_id); await this.kv.atomic()
await this.kv.set(['blob', blob_id], content); .check({key: ['blob_index', blobIndex, chunckIndex], versionstamp: null })
.set(['blob_index', blobIndex, chunckIndex], blob_id)
.set(['blob', blob_id], content)
.commit();
} }
persistBlob(index: string, content: Uint8Array): Promise<number> { persistBlob(index: string, content: Uint8Array): Promise<number> {
@@ -107,12 +111,12 @@ export class DenoKVFileService implements FileService {
createFromFullContent( createFromFullContent(
path: string, path: string,
content: string | Uint8Array, content: string | Uint8Array,
): Promise<FileMetaData> { ): Promise<FileMetaData | null> {
const reader = content instanceof Uint8Array ? new Buffer(content) : new StringReader(content); const reader = content instanceof Uint8Array ? new Buffer(content) : new StringReader(content);
return this.createFromReader(path, reader); return this.createFromReader(path, reader);
} }
async createFromReader(path: string, content: Deno.Reader): Promise<FileMetaData> { async createFromReader(path: string, content: Deno.Reader): Promise<FileMetaData | null> {
const newId = crypto.randomUUID(); const newId = crypto.randomUUID();
const compressedContent = readableStreamFromReader(content).pipeThrough(new CompressionStream(CompressionAlgorithm.GZIP)) const compressedContent = readableStreamFromReader(content).pipeThrough(new CompressionStream(CompressionAlgorithm.GZIP))
@@ -123,12 +127,18 @@ export class DenoKVFileService implements FileService {
path, path,
contentSize: size, contentSize: size,
compression: CompressionAlgorithm.GZIP, compression: CompressionAlgorithm.GZIP,
creationTime: Date.now()
}; };
const idx: SerializedFilePathIndex = { fileId: newId }; const idx: SerializedFilePathIndex = { fileId: newId };
await this.kv.set(['file', newId], serializedFile);
await this.kv.set(['file_index_path', path], idx);
return { id: newId, path, contentSize: size }; const ret = await this.kv.atomic()
.check({key: ['file_index_path', path], versionstamp: null })
.set(['file', newId], serializedFile)
.set(['file_index_path', path], idx)
.commit();
return ret.ok ? { id: newId, path, contentSize: size } : null;
} }
async *list(): AsyncIterable<FileMetaData> { async *list(): AsyncIterable<FileMetaData> {
@@ -253,10 +263,12 @@ export class DenoKVFileService implements FileService {
id: fileObject.value.id, id: fileObject.value.id,
contentSize: fileObject.value.contentSize, contentSize: fileObject.value.contentSize,
compression: fileObject.value.compression, compression: fileObject.value.compression,
creationTime: fileObject.value.creationTime,
}; };
await this.kv.atomic() await this.kv.atomic()
.check(fileObject) .check(fileObject)
.check(idx) .check(idx)
.check({key: ['file_index_path', newPath], versionstamp: null })
.delete(idx.key) .delete(idx.key)
.set(['file_index_path', newPath], idx.value) .set(['file_index_path', newPath], idx.value)
.set(fileObject.key, newFile) .set(fileObject.key, newFile)
+2 -2
View File
@@ -10,9 +10,9 @@ export interface FileService {
createFromFullContent( createFromFullContent(
path: string, path: string,
content: string | Uint8Array, content: string | Uint8Array,
): Promise<FileMetaData>; ): Promise<FileMetaData | null>;
createFromReader(path: string, content: Deno.Reader): Promise<FileMetaData>; createFromReader(path: string, content: Deno.Reader): Promise<FileMetaData | null>;
getRawContentByPath(path: string): Promise<Uint8Array | null>; getRawContentByPath(path: string): Promise<Uint8Array | null>;
+27 -6
View File
@@ -45,7 +45,7 @@ Deno.test('DenoKVBlobService happy path', async (t) => {
kv.close(); kv.close();
}); });
Deno.test('DenoKVFileService happy path xxx ', async (t) => { Deno.test('DenoKVFileService happy path ', async (t) => {
const kv = await Deno.openKv(':memory:'); const kv = await Deno.openKv(':memory:');
const fs: FileService = new DenoKVFileService(kv); const fs: FileService = new DenoKVFileService(kv);
const fileName1 = 'didier/file1.txt'; const fileName1 = 'didier/file1.txt';
@@ -54,8 +54,8 @@ Deno.test('DenoKVFileService happy path xxx ', async (t) => {
let file2: FileMetaData | undefined; let file2: FileMetaData | undefined;
await t.step('Create 2 files xyzabc', async () => { await t.step('Create 2 files xyzabc', async () => {
file1 = await fs.createFromFullContent(fileName1, 'bob le chef'); file1 = (await fs.createFromFullContent(fileName1, 'bob le chef'))!;
file2 = await fs.createFromFullContent(fileName2, 'le geant vert ☀️'); file2 = (await fs.createFromFullContent(fileName2, 'le geant vert ☀️'))!;
assertEquals(file1.path, fileName1); assertEquals(file1.path, fileName1);
assertEquals(file2.contentSize, 47); assertEquals(file2.contentSize, 47);
@@ -97,7 +97,7 @@ Deno.test('DenoKVFileService happy path xxx ', async (t) => {
const newPath = 'boris/streamed-file.txt'; const newPath = 'boris/streamed-file.txt';
const ret = await fs.createFromReader(newPath, new StringReader('hello world 18')); const ret = await fs.createFromReader(newPath, new StringReader('hello world 18'));
assertEquals(ret.contentSize, 40); assertEquals(ret!.contentSize, 40);
const s = await fs.getRawContentByPath(newPath); const s = await fs.getRawContentByPath(newPath);
assertEquals(new TextDecoder().decode(s!), 'hello world 18'); assertEquals(new TextDecoder().decode(s!), 'hello world 18');
@@ -117,6 +117,27 @@ Deno.test('DenoKVFileService happy path xxx ', async (t) => {
kv.close(); kv.close();
}); });
Deno.test('DenoKVFileService file duplication',async (t) => {
const kv = await Deno.openKv(':memory:');
const fs: FileService = new DenoKVFileService(kv);
await t.step("Create two files with same name", async () => {
const file1 = await fs.createFromFullContent('create/one', 'file 1');
const file2 = await fs.createFromFullContent(file1!.path, 'file 2');
assertEquals(file2, null);
});
await t.step('Rename file to path that already exists',async () => {
const file1 = await fs.createFromFullContent('rename/one', 'file 1');
const file2 = await fs.createFromFullContent('rename/two', 'file 2');
const ret = await fs.renameById(file1!.id, file2!.path);
assertEquals(ret!.path, file1!.path) // assert file did not move
});
kv.close();
})
Deno.test('DenoKVFileService happy path with byte array', async (t) => { Deno.test('DenoKVFileService happy path with byte array', async (t) => {
const kv = await Deno.openKv(':memory:'); const kv = await Deno.openKv(':memory:');
const fs: FileService = new DenoKVFileService(kv); const fs: FileService = new DenoKVFileService(kv);
@@ -124,8 +145,8 @@ Deno.test('DenoKVFileService happy path with byte array', async (t) => {
const inputContent = new Uint8Array([240]); const inputContent = new Uint8Array([240]);
const file = await fs.createFromFullContent('hello', inputContent); const file = await fs.createFromFullContent('hello', inputContent);
assertEquals(file.path, 'hello'); assertEquals(file!.path, 'hello');
assertEquals(file.contentSize, 27); assertEquals(file!.contentSize, 27);
const persistedContent = await fs.getRawContentByPath('hello'); const persistedContent = await fs.getRawContentByPath('hello');
assertEquals(persistedContent, inputContent); assertEquals(persistedContent, inputContent);