Skip to content

Random errors occurs for custom file system #810

@vitonsky

Description

@vitonsky

My use case

I use https://pglite.dev/ and want to keep FS in RAM, but dump this FS as ArrayBuffer once changes occurs.

PGLite have method https://pglite.dev/docs/api#dumpdatadir that even with no compression takes 100ms and this is too long time, since data dump may be called frequently.

Since PGLite uses a MEMFS i think the problem is they iterate whole FS content to build archive every time we call dump.

As potential solution I see a FS that will store data in single ArrayBuffer and will let us dump this buffer immediately (only time to copy buffer).

I found package @zenfs/core that implements exactly I need

import { configureSingle, fs, SingleBuffer } from "@zenfs/core";

const sharedArrayBuffer = new ArrayBuffer(100000);

await configureSingle({
	backend: SingleBuffer,
	buffer: sharedArrayBuffer,
});

fs.writeFileSync('/test.txt', 'You can do this anywhere, including browsers!');

The problem

Now I trying to wrap this FS via pglite custom FS API

/**
* Abstract base class for all custom virtual filesystems.
* Each custom filesystem needs to implement an interface similar to the NodeJS FS API.
*/
export abstract class BaseFilesystem implements Filesystem {

It does not work for me and throws random errors and behave randomly too.

My code with debug prints is:

/* eslint-disable spellcheck/spell-checker */
import { PGlite } from '@electric-sql/pglite';
import { BaseFilesystem } from '@electric-sql/pglite/dist/fs/base';
import { x } from '@electric-sql/pglite/dist/pglite-CyDq4d4K';
import { configure, fs, SingleBuffer } from '@zenfs/core';

class MyFS extends BaseFilesystem {
	protected readonly dataDir = '/';
	chmod(path: string, mode: number): void {
		fs.chmodSync(path, mode);
	}
	close(fd: number): void {
		fs.closeSync(fd);
	}
	fstat(fd: number): x {
		console.log('fstat', fd);
		const stats = fs.fstatSync(fd);

		return {
			blocks: stats.blocks,
			atime: stats.atimeMs,
			mtime: stats.mtimeMs,
			ctime: stats.ctimeMs,
			dev: stats.dev,
			ino: stats.ino,
			mode: stats.mode,
			nlink: stats.nlink,
			uid: stats.uid,
			gid: stats.gid,
			rdev: stats.rdev,
			size: stats.size,
			blksize: stats.blksize,
		};
	}
	lstat(path: string): x {
		console.log('lstat', path);

		try {
			const stats = fs.lstatSync(path);
			const result = {
				blocks: stats.blocks,
				atime: stats.atimeMs,
				mtime: stats.mtimeMs,
				ctime: stats.ctimeMs,
				dev: stats.dev,
				ino: stats.ino,
				mode: stats.mode,
				nlink: stats.nlink,
				uid: stats.uid,
				gid: stats.gid,
				rdev: stats.rdev,
				size: stats.size,
				blksize: stats.blksize,
			};

			return result;
		} catch (error) {
			throw error;
		}
	}
	mkdir(
		path: string,
		options?:
			| { recursive?: boolean | undefined; mode?: number | undefined }
			| undefined,
	): void {
		console.log('mkdir', path);
		fs.mkdirSync(path, options);
	}
	open(path: string, flags?: string | undefined, mode?: number | undefined): number {
		console.log('open', path);
		return fs.openSync(path, flags ?? 'rw', mode);
	}
	readdir(path: string): string[] {
		console.log('readdir', path);
		return fs.readdirSync(path);
	}
	read(
		fd: number,
		buffer: Uint8Array,
		offset: number,
		length: number,
		position: number,
	): number {
		console.log('>> read', fd);
		return fs.readSync(fd, buffer, offset, length, position);
	}
	rename(oldPath: string, newPath: string): void {
		fs.renameSync(oldPath, newPath);
	}
	rmdir(path: string): void {
		fs.rmdirSync(path);
	}
	truncate(path: string, len: number): void {
		fs.truncateSync(path, len);
	}
	unlink(path: string): void {
		fs.unlinkSync(path);
	}
	utimes(path: string, atime: number, mtime: number): void {
		fs.utimesSync(path, atime, mtime);
	}
	writeFile(
		path: string,
		data: string | Uint8Array,
		options?:
			| {
				encoding?: string | undefined;
				mode?: number | undefined;
				flag?: string | undefined;
			}
			| undefined,
	): void {
		console.log('WRITE FILE', path);
		const { encoding, ...restOptions } = options ?? {};
		fs.writeFileSync(path, data, {
			...restOptions,
			encoding: encoding as BufferEncoding,
		});
	}
	write(
		fd: number,
		buffer: Uint8Array,
		offset: number,
		length: number,
		position: number,
	): number {
		console.log('>> write');
		return fs.writeSync(fd, buffer, offset, length, position);
	}
}


function proxyMethods(obj: any, onCall: any) {
	return new Proxy(obj, {
		get(target, prop, receiver) {
			const value = Reflect.get(target, prop, receiver);
			if (typeof value !== "function") return value;
			return function (...args) {
				const original = value.bind(this);
				onCall(target, prop, args, original);
				return original(...args);
			};
		}
	});
}

test('debug', async () => {
	const sharedArrayBuffer = new ArrayBuffer(52_428_800);

	await configure({
		mounts: {
			'/': {
				backend: SingleBuffer,
				buffer: sharedArrayBuffer,
			},
		},
	});

	const pg = new PGlite({
		fs: proxyMethods(new MyFS(), (_target, prop, args) => {
			console.log("Call for", prop, args);
		})
	});
	await pg.waitReady;

	console.log(fs.readdirSync('/', { recursive: true }));
});

The output looks weird, it looks code calls lstat with empty path '' and never tries to create file, then promise waitReady rejects with message "unreachable":

stdout | src/core/storage/database/pglite/zenfs.test.ts > debug
Call for initialSyncFs []

stdout | src/core/storage/database/pglite/zenfs.test.ts > debug
Call for lstat [ '/PG_VERSION' ]
lstat /PG_VERSION
Call for lstat [ '/PG_VERSION' ]
lstat /PG_VERSION

stdout | src/core/storage/database/pglite/zenfs.test.ts > debug
Call for lstat [ '/PG_VERSION' ]
lstat /PG_VERSION
Call for lstat [ '' ]
lstat 
Call for lstat [ '/postgresql.conf' ]
lstat /postgresql.conf
Call for lstat [ '' ]
lstat 

 ❯ src/core/storage/database/pglite/zenfs.test.ts (1 test | 1 failed) 129ms
   × debug 129ms
     → unreachable

The request

It is not clear about current status of custom FS in pglite, so let's figure it out first. Does pglite provides any API to implement custom FS on user side?

If so, we need in docs and maybe trivial example how to implement custom FS.

I found issue #533 where @tdrz said

We are looking into making it easier to add custom filesystems but it will take a while...

It was almost year ago. I no need in API to make custom FS easier, I want to implement custom FS in any way, to address real use case.

With no custom FS pglite have no sense in my case, because the use case is privacy focused app that can't keep data in FS/IDB/etc with no encryption. That's why we use in-memory DB. But persistence and performance still does matter.

In case I can't implement custom FS, the only way is to dump data as tar archive, but this way show very poor performance for frequent syncs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions