Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrading @matrixai/async-init, @matrixai/async-locks, @matrixai/db, @matrixai/errors, @matrixai/workers and integrating @matrixai/resources #63

Merged
merged 18 commits into from
May 9, 2022

Conversation

CMCDragonkai
Copy link
Member

@CMCDragonkai CMCDragonkai commented Apr 5, 2022

Description

Several core libraries have been updated and js-encryptedfs needs to start using them.

  1. @matrixai/async-init - no major changes
  2. @matrixai/async-locks - the INodeManager uses a collection of Mutex for mutual exclusion of inodes. This means operations to individual inodes may be blocked. We can switch this with Lock, and in the future optimise with RWLockWriter or RWLockReader
  3. @matrixai/db - the DB now supports proper DB transactions, we should switch the INodeManager API to fully support it
  4. @matrixai/errors - we make all of our errors extend AbstractError<T> and also provide static descriptions to all of them, as well as use the cause chain
  5. @matrixai/workers - no major changes here
  6. @matrixai/resources - since the @matrixai/db no longer does any locking, the acquisition of the DBTransaction and Lock has to be done together with withF or withG

Issues Fixed

Tasks

  • 1. Upgrade all exceptions to use AbstractError and use cause chain and static descriptions
  • 2. Integrate @matrixai/db into INodeManager, remove sublevel objects and replace with full keypaths.
  • 3. Integrate @matrixai/async-locks to use the Lock class instead of Mutex from async-mutex
  • 4. Use @matrixai/resources withF and withG to replace some of our transact API.
  • 5. See if FileDescriptor.write and FileDescriptor.read should be using a single transaction context for the entire operation: Upgrading @matrixai/async-init, @matrixai/async-locks, @matrixai/db, @matrixai/errors, @matrixai/workers and integrating @matrixai/resources #63 (comment)
  • 6. Removed canary check in favour of Integrate Canary Check during DB start js-db#7
  • 7. Fix tests for transaction usage in tests/inodes
  • 8. Clean up left over commentary from migration
  • [ ] 9. Consider optimising EncryptedFS.ftruncate so that it does not need to iterate over the entire file blocks before truncating, this targets Identify and Eliminate Unscalable Operations with Lazy GC #53 - do this in Identify and Eliminate Unscalable Operations with Lazy GC #53
  • 10. Review fileGetBlocks and see if it correctly implemented - infinite loop solved by incrementing the block count inside the while loop, and added new test to INodeManager.file.test.ts read sparse blocks from a file to cover this
  • 11. tests/utils.ts#expectError has been changed to take the exception class as well, and then the errno object to be checked
  • 12. Fixed get after delete consistency in DB and therefore in EFS, in particular ftruncate
  • 13. FD operations are mutually exclusive, this is enforced with each FileDescriptor having its own lock. This prevents some stream concurrency errors
  • 14. Reviewing all concurrency tests for correctness - some tests are too brittle, they should expect non-deterministic concurrent behaviour
    • INode creation is currently racy, it needs to be made mutually exclusive for the same name on the same directory
    • The navigateFrom seems racy with multiple transaction contexts being used
    • The _open seems racy with the final FD creation with respect to inode creation
    • The mkdir seems to have strange behavour when the target already exists, need to compare behaviour with real filesystem mkdir
  • 15. Updated jest, linting, typedoc dependencies

Final checklist

  • Domain specific tests
  • Full tests
  • Updated inline-comment documentation
  • Lint fixed
  • Squash and rebased
  • Sanity check the final build

@CMCDragonkai CMCDragonkai changed the title WIP: Upgrading @matrixai/async-init, @matrixai/async-locks, @matrixai/db, @matrixai/errors, @matrixai/workers WIP: Upgrading @matrixai/async-init, @matrixai/async-locks, @matrixai/db, @matrixai/errors, @matrixai/workers and integrating @matrixai/resources Apr 5, 2022
@CMCDragonkai
Copy link
Member Author

This is a big upgrade... it would result in v3.5.0, external API doesn't really change, it's mostly internal things. Lots of patterns produced now.

@CMCDragonkai CMCDragonkai self-assigned this Apr 6, 2022
@CMCDragonkai
Copy link
Member Author

The INodeManager.gcAll originally used this.db.transact to lock up the DB every time it did any deletion operations. This should no longer be necessary with transactional iteration. No locks are even needed here since it's only called by INodeManager.start and INodeManager.stop.

@CMCDragonkai
Copy link
Member Author

In integrating the @matrixai/db. I've found the NonEmptyArray type to be quite annoying. It requires alot of casting.

If we remove the requirement, and that would mean [] has to mean something. It could mean the key '' and thus be equivalent to [''].

That could simplify the code and remove all the as unknown as KeyPath.

@CMCDragonkai
Copy link
Member Author

Only issue is that some places the keyPath is spread into other key paths like ['data', ...keyPath]. Prior to this happening, the an empty keypath must be turned into [''] to ensure that a keypath always has at least 1 entry.

@CMCDragonkai
Copy link
Member Author

CMCDragonkai commented Apr 6, 2022

The @matrixai/db has been updated to 3.2.2 and now KeyPath [] becomes ['']. No more as unknown as KeyPath type casting required.

@CMCDragonkai
Copy link
Member Author

When I removed the INodeManager.transact method I noticed quite an increase in verbosity. I realised the with* convenience wrappers like in RWLockWriter.withReadF() and RWLockWriter.withReadG() are actually quite nice here, and they can take over the transact method.

However I wanted to improve the API a little bit. Since we now know how to do NonEmptyArray, we can also create variadic argument types that specify the last type as a callback.

So now we have:

  public async withTransactionF<T>(
    ...args: [
      ...inos: INodeIndex[],
      f: (tran: DBTransaction) => Promise<T>
    ]
  ) {
    const f = args.pop() as (tran: DBTransaction) => Promise<T>;
    return withF(
      [
        this.db.transaction(),
        this.getLocks(...args as Array<INodeIndex>)
      ],
      ([tran]) => f(tran)
    );
  }

  public withTransactionG<T, TReturn, TNext>(
    ...args: [
      ...inos: INodeIndex[],
      g: (tran: DBTransaction) => AsyncGenerator<T, TReturn, TNext>,
    ]
  ): AsyncGenerator<T, TReturn, TNext> {
    const g = args.pop() as (tran: DBTransaction) => AsyncGenerator<T, TReturn, TNext>;
    return withG(
      [
        this.db.transaction(),
        this.getLocks(...args as Array<INodeIndex>)
      ],
      ([tran]) => g(tran)
    );
  }

This actually conveniently replaces the transact methods. And you get to state up front what inodes you need to lock if any.

This is nicer than having the inodes that you need to lock at the very end.

Furthermore, notice that I "specialise" the resources to just the transaction, because the user won't have any use for the lock objects.

The user can always still compose much more complex resource contexts by using the getLocks and putting together the ResourceAcquire.

@CMCDragonkai
Copy link
Member Author

Also moving to selective imports so that tests can be run by themselves.

import { permissions } from '@';

To:

import * as permissions from '@/permissions';

@CMCDragonkai
Copy link
Member Author

I tried getting a vscode regex to do a big find and replace. Couldn't work due to matches braces requirements.

@CMCDragonkai
Copy link
Member Author

There are no functions that are exposing a generator interface atm. Even streams are using the FD abstraction, and they don't actually hold lock/transaction during their lifetime. This is because every write is a single operation. So for now there's no use of INodeManager.withTransactionG except internally inside INodeManager.

@CMCDragonkai
Copy link
Member Author

I've found that the FileDescriptor methods of write and read aren't using one entire transaction for their operations. It seems like could be encapsulated in one transaction... should test this later.

@CMCDragonkai
Copy link
Member Author

The EncryptedFS has been using alot of:

        fd ? [fd.ino] : [],

And also for target. They should be using fd != null or target != null. The ino could be 0 in its type, although this is prevented by starting the resource counter at 1. I'm changing over.

@CMCDragonkai
Copy link
Member Author

Everything is migrated over, however I got a few test failures in EncryptedFS.

@CMCDragonkai
Copy link
Member Author

The canary check logic should be moved into DB during await DB.create() and DB.start. It doesn't make sense for it to be in the EFS. Plus now that DB has "hidden" sublevels, this can be done in the root level.

@CMCDragonkai
Copy link
Member Author

This is now done in js-db at 3.2.3. And the canary check has been removed. The ErrorEncryptedFSKey remains though and wraps around the ErrorDBKey when thrown.

@CMCDragonkai
Copy link
Member Author

Current tests state, all passes except 2:

[nix-shell:~/Projects/js-encryptedfs]$ npm test

> encryptedfs@3.4.3 test /home/cmcdragonkai/Projects/js-encryptedfs
> jest

Determining test suites to run...
GLOBAL SETUP
 PASS  tests/workers/efsWorker.test.ts
  EFS worker
    ✓ encryption and decryption (1082 ms)
    ✓ encryption and decryption within 1 call (99 ms)

 PASS  tests/utils.test.ts
  utils
    ✓ random bytes generation (43 ms)
    ✓ key is randomly generated (4 ms)
    ✓ key generation from password is non-deterministic with random salt (766 ms)
    ✓ key generation is deterministic with a given salt (1360 ms)
    ✓ encryption and decryption (14 ms)
    ✓ block offset is position % block size (3 ms)
    ✓ number of blocks to be written
    ✓ block index start (1 ms)
    ✓ block index end (3 ms)
    ✓ block cursor (1 ms)
    ✓ buffer segmentation (6 ms)
    maybeCallback
      as a promise
        ✓ Should function
        ✓ Should throw error (18 ms)
      as a callback
        ✓ Should function (1 ms)
        ✓ Should throw error

 PASS  tests/inodes/INodeManager.test.ts (8.642 s)
  INodeManager
    ✓ inode manager is persistent across restarts (400 ms)
    ✓ transactions are locked via inodes (34 ms)
    ✓ inodes can be scheduled for deletion when there are references to them (167 ms)

 PASS  tests/fd/FileDescriptorManager.test.ts (8.91 s)
  File Descriptor Manager
    ✓ create a file descriptor manager (36 ms)
    ✓ create a file descriptor (18 ms)
    ✓ retreive a file descriptor (7 ms)
    ✓ delete a file descriptor (16 ms)
    ✓ duplicate a file descriptor (9 ms)
    ✓ read/write to fd when inode deleted from directory (210 ms)

 PASS  tests/inodes/INodeManager.symlink.test.ts (9.017 s)
  INodeManager Symlink
    ✓ create and delete symlink (274 ms)

 PASS  tests/inodes/INodeManager.file.test.ts (9.025 s)
  INodeManager File
    ✓ create a file (131 ms)
    ✓ create a file with supplied data (97 ms)
    ✓ write and read data from a file (62 ms)
    ✓ read a single block from a file (64 ms)
    ✓ write a single block from a file (41 ms)
    ✓ handle accessing blocks that the db does not have (45 ms)

 PASS  tests/inodes/INodeManager.dir.test.ts (9.339 s)
  INodeManager Directory
    ✓ create root directory (256 ms)
    ✓ create subdirectory (142 ms)
    ✓ create subdirectories (239 ms)
    ✓ delete subdirectory (106 ms)
    ✓ rename directory entry (153 ms)
    ✓ iterate directory entries (129 ms)

 PASS  tests/EncryptedFS.test.ts (9.573 s)
  EncryptedFS
    ✓ efs readiness (211 ms)
    ✓ efs is persistent across restarts (224 ms)
    ✓ creating fresh efs (192 ms)
    ✓ efs exposes constants (38 ms)
    ✓ validate key (49 ms)

 PASS  tests/fd/FileDescriptor.test.ts (9.692 s)
  File Descriptor
    ✓ create a file descriptor (65 ms)
    ✓ can set flags (20 ms)
    ✓ can set position (126 ms)
    ✓ read all the data on the file iNode (62 ms)
    ✓ read with the file descriptor at a certain position (78 ms)
    ✓ read when the return buffer length is less than the data length (66 ms)
    ✓ write to an empty file iNode (79 ms)
    ✓ overwrite a single block of a file iNode (67 ms)
    ✓ overwrite at an offset to a file iNode (75 ms)
    ✓ write past the end of a file iNode (72 ms)
    ✓ append data to the file iNode (155 ms)

 PASS  tests/EncryptedFS.streams.test.ts (10.779 s)
  EncryptedFS Streams
    readstream
      ✓ using 'for await' (335 ms)
      ✓ using 'event readable' (185 ms)
      ✓ using 'event data' (128 ms)
      ✓ respects start and end options (124 ms)
      ✓ respects the high watermark (131 ms)
      ✓ respects the start option (115 ms)
      ✓ end option is ignored without the start option (121 ms)
      ✓ can use a file descriptor (113 ms)
      ✓ with start option overrides the file descriptor position (111 ms)
      ✓ can handle errors asynchronously (36 ms)
      ✓ can compose with pipes (111 ms)
    writestream
      ✓ can compose with pipes (139 ms)
      ✓ can create and truncate files (140 ms)
      ✓ can be written into (110 ms)
      ✓ allow ignoring of the drain event, temporarily ignoring resource usage control (112 ms)
      ✓ can use the drain event to manage resource control (191 ms)
      ✓ can handle errors asynchronously (33 ms)

 PASS  tests/EncryptedFS.nav.test.ts (12.528 s)
  EncryptedFS Navigation
    ✓ EFS using callback style functions (419 ms)
    ✓ should be able to restore state (141 ms)
    ✓ should be able to navigate before root (232 ms)
    ✓ trailing slash refers to the directory instead of a file (138 ms)
    ✓ trailing slash works for non-existent directories when intending to create them (82 ms)
    ✓ trailing `/.` for mkdir should result in errors (77 ms)
    ✓ navigating invalid paths (442 ms)
    ✓ various failure situations (253 ms)
    ✓ cwd returns the absolute fully resolved path (142 ms)
    ✓ cwd still works if the current directory is deleted (106 ms)
    ✓ deleted current directory can still use . and .. for traversal (128 ms)
    ✓ can still chdir when both current and parent directories are deleted (159 ms)
    ✓ cannot chdir into a directory without execute permissions (66 ms)
    ✓ should be able to access inodes inside chroot (297 ms)
    ✓ should not be able to access inodes outside chroot (129 ms)
    ✓ should not be able to access inodes outside chroot using symlink (134 ms)
    ✓ prevents users from changing current directory above the chroot (104 ms)
    ✓ can sustain a current directory inside a chroot (70 ms)
    ✓ can chroot, and then chroot again (100 ms)
    ✓ chroot returns a running efs instance (60 ms)
    ✓ chroot start & stop does not affect other efs instances (115 ms)
    ✓ root efs instance stops all chrooted instances (121 ms)
    ✓ destroying chroot is a noop (63 ms)

 PASS  tests/EncryptedFS.perms.test.ts (14.801 s)
  EncryptedFS Permissions
    ✓ chown changes uid and gid (302 ms)
    ✓ chmod with 0 wipes out all permissions (150 ms)
    ✓ mkdir and chmod affects the mode (137 ms)
    ✓ umask is correctly applied (213 ms)
    ✓ non-root users can only chown uid if they own the file and they are chowning to themselves (213 ms)
    ✓ chmod only works if you are the owner of the file (97 ms)
    ✓ permissions are checked in stages of user, group then other (334 ms)
    ✓ permissions are checked in stages of user, group then other (using chown) (380 ms)
    ✓ --x-w-r-- permission staging (159 ms)
    ✓ file permissions --- (189 ms)
    ✓ file permissions r-- (193 ms)
    ✓ file permissions rw- (196 ms)
    ✓ file permissions rwx (187 ms)
    ✓ file permissions r-x (168 ms)
    ✓ file permissions -w- (176 ms)
    ✓ file permissions -wx (168 ms)
    ✓ file permissions --x (159 ms)
    ✓ directory permissions --- (203 ms)
    ✓ directory permissions r-- (242 ms)
    ✓ directory permissions rw- (221 ms)
    ✓ directory permissions rwx (224 ms)
    ✓ directory permissions r-x (347 ms)
    ✓ directory permissions -w- (146 ms)
    ✓ directory permissions -wx (254 ms)
    ✓ directory permissions --x (242 ms)
    ✓ permissions dont affect already opened fd (204 ms)
    ✓ chownr changes uid and gid recursively (284 ms)
    ✓ chown can change groups without any problem because we do not have a user group hierarchy (81 ms)
    ✓ --x-w-r-- do not provide read write and execute to the user due to permission staging (204 ms)

 PASS  tests/EncryptedFS.links.test.ts (15.837 s)
  EncryptedFS Links
    ✓ Symlink stat makes sense (276 ms)
    symlink
      ✓ creates symbolic links (508 ms)
      ✓ paths can contain multiple slashes (211 ms)
      ✓ can resolve 1 symlink loop (67 ms)
      ✓ can resolve 2 symlink loops (143 ms)
      ✓ can be expanded by realpath (239 ms)
      ✓ cannot be traversed by rmdir (100 ms)
      ✓ is able to be added and traversed transitively (338 ms)
      ✓ is able to traverse relative symlinks (156 ms)
      ✓ returns EACCES when a component of the 2nd name path prefix denies search permission (200 ms)
      ✓ returns EACCES if the parent directory of the file to be created denies write permission (221 ms)
      ✓ returns ELOOP if too many symbolic links were encountered in translating the name2 path name (147 ms)
      ✓ returns EEXIST if the 2nd name argument already exists as a regular (68 ms)
      ✓ returns EEXIST if the 2nd name argument already exists as a dir (51 ms)
      ✓ returns EEXIST if the 2nd name argument already exists as a block (61 ms)
      ✓ returns EEXIST if the 2nd name argument already exists as a symlink (54 ms)
    unlink
      ✓ can remove a link to a regular (81 ms)
      ✓ can remove a link to a block (55 ms)
      ✓ successful updates ctime of a regular (122 ms)
      ✓ successful updates ctime of a block (122 ms)
      ✓ unsuccessful does not update ctime of a regular (111 ms)
      ✓ unsuccessful does not update ctime of a block (111 ms)
      ✓ does not traverse symlinks (198 ms)
      ✓ returns ENOTDIR if a component of the path prefix is not a directory (113 ms)
      ✓ returns ENOENT if the named file does not exist (78 ms)
      ✓ returns EACCES when search permission is denied for a component of the path prefix (122 ms)
      ✓ returns EACCES when write permission is denied on the directory containing the link to be removed (119 ms)
      ✓ returns ELOOP if too many symbolic links were encountered in translating the pathname (115 ms)
      ✓ returns EISDIR if the named file is a directory (65 ms)
      ✓ will not immeadiately free a file (172 ms)
    link
      ✓ creates hardlinks to regular (214 ms)
      ✓ creates hardlinks to block (243 ms)
      ✓ successful updates ctime of regular (118 ms)
      ✓ successful updates ctime of block (108 ms)
      ✓ unsuccessful does not update ctime of regular (123 ms)
      ✓ unsuccessful does not update ctime of block (121 ms)
      ✓ should not create hardlinks to directories (50 ms)
      ✓ can create multiple hardlinks to the same file (151 ms)
      ✓ returns ENOTDIR if a component of either path prefix is a regular (147 ms)
      ✓ returns ENOTDIR if a component of either path prefix is a dir (22 ms)
      ✓ returns ENOTDIR if a component of either path prefix is a block (120 ms)
      ✓ returns ENOTDIR if a component of either path prefix is a symlink (18 ms)
      ✓ returns EACCES when a component of either path prefix denies search permission (212 ms)
      ✓ returns EACCES when the requested link requires writing in a directory with a mode that denies write permission (199 ms)
      ✓ returns ELOOP if too many symbolic links were encountered in translating one of the pathnames (151 ms)
      ✓ returns ENOENT if the source file does not exist (103 ms)
      ✓ returns EEXIST if the destination regular does exist (110 ms)
      ✓ returns EEXIST if the destination dir does exist (94 ms)
      ✓ returns EEXIST if the destination block does exist (76 ms)
      ✓ returns EEXIST if the destination symlink does exist (79 ms)
      ✓ returns EPERM if the source file is a directory (86 ms)

 FAIL  tests/EncryptedFS.files.test.ts (16.472 s)
  EncryptedFS Files
    ✓ File stat makes sense (331 ms)
    ✓ Uint8Array data support (242 ms)
    ✓ URL path support (240 ms)
    lseek
      ✓ can seek different parts of a file (134 ms)
      ✓ can seek beyond the file length and create a zeroed "sparse" file (143 ms)
    fallocate
      ✓ can extend the file length (94 ms)
      ✓ does not touch existing data (96 ms)
      ✓ will only change ctime (120 ms)
    truncate
      ✓ will change mtime and ctime (181 ms)
      ✕ truncates the fd position (204 ms)
    read
      ✓ can be called using different styles (126 ms)
      ✓ file can be called using different styles (132 ms)
      ✓ file moves with fd position (132 ms)
      ✓ moves with the fd position (104 ms)
      ✓ does not change fd position according to position parameter (313 ms)
    write
      ✓ can be called using different styles (104 ms)
      ✓ file can be called using different styles (259 ms)
      ✓ moves with the fd position (113 ms)
      ✓ can make 100 files (1760 ms)
      ✓ does not change fd position according to position parameter (97 ms)
      ✓ respects the mode (138 ms)
      ✓ file writes from the beginning, and does not move the fd position (91 ms)
      ✓ with O_APPEND always set their fd position to the end (159 ms)
      ✓ can copy files (152 ms)
      ✓ using append moves with the fd position (115 ms)
    open
      ✓ opens a file if O_CREAT is specified and the file doesn't exist (148 ms)
      ✓ updates parent directory ctime/mtime if file didn't exist (134 ms)
      ✓ doesn't update parent directory ctime/mtime if file existed (142 ms)
      ✓ returns ENOTDIR if a component of the path prefix is a regular (64 ms)
      ✓ returns ENOTDIR if a component of the path prefix is a block (60 ms)
      ✓ returns ENOENT if a component of the path name that must exist does not exist or O_CREAT is not set and the named file does not exist (88 ms)
      ✓ returns EACCES when search permission is denied for a component of the path prefix (145 ms)
      ✓ returns EACCES when the required permissions are denied for a regular file (476 ms)
      ✓ returns EACCES when the required permissions are denied for adirectory (290 ms)
      ✓ returns EACCES when O_TRUNC is specified and write permission is denied (210 ms)
      ✓ returns ELOOP if too many symbolic links were encountered in translating the pathname (84 ms)
      ✓ returns EISDIR when trying to open a directory for writing (71 ms)
      ✓ returns ELOOP when O_NOFOLLOW was specified and the target is a symbolic link (60 ms)
      ✓ returns EEXIST when O_CREAT and O_EXCL were specified and the regular exists (90 ms)
      ✓ returns EEXIST when O_CREAT and O_EXCL were specified and the dir exists (72 ms)
      ✓ returns EEXIST when O_CREAT and O_EXCL were specified and the block exists (78 ms)
      ✓ returns EEXIST when O_CREAT and O_EXCL were specified and the symlink exists (83 ms)

  ● EncryptedFS Files › truncate › truncates the fd position

    expect(received).toEqual(expected) // deep equality

    - Expected  - 2
    + Received  + 2

      Object {
        "data": Array [
          100,
    -     98,
    -     99,
    +     101,
    +     102,
        ],
        "type": "Buffer",
      }

      183 |       await efs.ftruncate(fd, 4);
      184 |       await efs.read(fd, buf, 0, buf.length);
    > 185 |       expect(buf).toEqual(Buffer.from('dbc'));
          |                   ^
      186 |       await efs.close(fd);
      187 |     });
      188 |   });

      at Object.<anonymous> (tests/EncryptedFS.files.test.ts:185:19)

 PASS  tests/EncryptedFS.dirs.test.ts (17.557 s)
  EncryptedFS Directories
    ✓ Directory stat makes sense (319 ms)
    ✓ Empty root directory at startup (102 ms)
    file descriptors
      ✓ can change stats, permissions and flush data (215 ms)
      ✓ cannot perform read or write operations (186 ms)
      ✓ inode nlink becomes 0 after deletion of the directory (151 ms)
    rmdir
      ✓ should be able to remove directories (390 ms)
      ✓ cannot delete current directory using . (88 ms)
      ✓ cannot delete parent directory using .. even when current directory is deleted (174 ms)
      ✓ cannot create inodes within a deleted current directory (197 ms)
      ✓ returns ENOENT if the named directory does not exist (04) (95 ms)
      ✓ returns ELOOP if too many symbolic links were encountered in translating the pathname (175 ms)
      ✓ returns ENOTEMPTY if the named directory contains files other than '.' and '..' in it for regular (144 ms)
      ✓ returns ENOTEMPTY if the named directory contains files other than '.' and '..' in it for dir (106 ms)
      ✓ returns ENOTEMPTY if the named directory contains files other than '.' and '..' in it for block (115 ms)
      ✓ returns ENOTEMPTY if the named directory contains files other than '.' and '..' in it for symlink (109 ms)
      ✓ returns EACCES when search permission is denied for a component of the path prefix (213 ms)
      ✓ returns EACCES when write permission is denied on the directory containing the link to be removed (187 ms)
      ✓ recursively deletes the directory if it contains regular (135 ms)
      ✓ recursively deletes the directory if it contains dir (122 ms)
      ✓ recursively deletes the directory if it contains block (128 ms)
      ✓ recursively deletes the directory if it contains symlink (161 ms)
      ✓ recursively deletes a deep directory (684 ms)
    mkdir & mkdtemp
      ✓ is able to make directories (328 ms)
      ✓ can create temporary directories (114 ms)
      ✓ should not make the root directory (24 ms)
      ✓ trailing '/.' should not result in any errors (201 ms)
      ✓ returns EACCES when write permission is denied on the parent directory of the directory to be created (183 ms)
      ✓ returns EEXIST if the named regular exists (93 ms)
      ✓ returns EEXIST if the named dir exists (92 ms)
      ✓ returns EEXIST if the named block exists (77 ms)
      ✓ returns EEXIST if the named symlink exists (88 ms)
    rename
      ✓ can rename a directory (80 ms)
      ✓ cannot rename the current or parent directory to a subdirectory (117 ms)
      ✓ cannot rename where the old path is a strict prefix of the new path (166 ms)
      ✓ changes name but inode remains the same for regular (150 ms)
      ✓ changes name but inode remains the same for block (140 ms)
      ✓ changes name for dir (81 ms)
      ✓ changes name for regular file (135 ms)
      ✓ unsuccessful of regular does not update ctime (92 ms)
      ✓ unsuccessful of dir does not update ctime (86 ms)
      ✓ unsuccessful of block does not update ctime (76 ms)
      ✓ unsuccessful of symlink does not update ctime (76 ms)
      ✓ returns ENOENT if a component of the 'from' path does not exist, or a path prefix of 'to' does not exist (92 ms)
      ✓ returns EACCES when a component of either path prefix denies search permission (224 ms)
      ✓ returns EACCES when the requested link requires writing in a directory with a mode that denies write permission (220 ms)
      ✓ returns ELOOP if too many symbolic links were encountered in translating one of the pathnames (155 ms)
      ✓ returns ENOTDIR if a component of either path prefix is a regular (142 ms)
      ✓ returns ENOTDIR if a component of either path prefix is a block (117 ms)
      ✓ returns ENOTDIR when the 'from' argument is a directory, but 'to' is a regular (80 ms)
      ✓ returns ENOTDIR when the 'from' argument is a directory, but 'to' is a block (88 ms)
      ✓ returns ENOTDIR when the 'from' argument is a directory, but 'to' is a symlink (84 ms)
      ✓ returns EISDIR when the 'to' argument is a directory, but 'from' is a regular (86 ms)
      ✓ returns EISDIR when the 'to' argument is a directory, but 'from' is a block (76 ms)
      ✓ returns EISDIR when the 'to' argument is a directory, but 'from' is a symlink (86 ms)
      ✓ returns EINVAL when the 'from' argument is a parent directory of 'to' (93 ms)
      ✓ returns ENOTEMPTY if the 'to' argument is a directory and contains regular (107 ms)
      ✓ returns ENOTEMPTY if the 'to' argument is a directory and contains dir (102 ms)
      ✓ returns ENOTEMPTY if the 'to' argument is a directory and contains block (96 ms)
      ✓ returns ENOTEMPTY if the 'to' argument is a directory and contains symlink (97 ms)
      ✓ changes file ctime for regular (103 ms)
      ✓ changes file ctime for dir (84 ms)
      ✓ changes file ctime for block (77 ms)
      ✓ changes file ctime for symlink (80 ms)
      ✓ succeeds when destination regular is multiply linked (142 ms)
      ✓ succeeds when destination block is multiply linked (115 ms)

 FAIL  tests/EncryptedFS.concurrent.test.ts (24.681 s)
  EncryptedFS Concurrency
    ✓ Renaming a directory at the same time with two different calls (355 ms)
    ✓ Reading a directory while adding/removing entries in the directory (246 ms)
    ✓ Reading a directory while removing the directory (277 ms)
    ✓ Reading a directory while renaming entries (278 ms)
    ✓ File metadata changes while reading/writing a file. (109 ms)
    ✓ Dir metadata changes while reading/writing a file. (177 ms)
    ✓ Read stream and write stream to same file (1158 ms)
    ✓ One write stream and one fd writing to the same file (88 ms)
    ✓ One read stream and one fd writing to the same file (176 ms)
    ✓ One write stream and one fd reading to the same file (78 ms)
    ✓ One read stream and one fd reading to the same file (168 ms)
    ✓ Two write streams to the same file (2097 ms)
    ✓ Writing a file and deleting the file at the same time using writeFile (68 ms)
    ✓ opening a file and deleting the file at the same time (71 ms)
    ✓ Writing a file and deleting the file at the same time for fd (148 ms)
    ✓ Writing a file and deleting the file at the same time for stream (125 ms)
    ✓ Appending to a file that is being written to for fd  (1130 ms)
    ✕ Appending to a file that is being written for stream (86 ms)
    ✓ Copying a file that is being written to for fd (192 ms)
    ✓ Copying a file that is being written to for stream (284 ms)
    ✓ removing a dir while renaming it. (120 ms)
    concurrent file writes
      ✓ 10 short writes with efs.writeFile. (508 ms)
      ✓ 10 long writes with efs.writeFile. (2155 ms)
      ✓ 10 short writes with efs.write. (179 ms)
      ✓ 10 long writes with efs.write. (4299 ms)
    Allocating/truncating a file while writing (stream or fd)
      ✓ Allocating while writing to fd (93 ms)
      ✓ Truncating while writing to fd (157 ms)
      ✓ Allocating while writing to stream (100 ms)
      ✓ Truncating while writing to stream (115 ms)
    Changing fd location in a file (lseek) while writing/reading (and updating) fd pos
      ✓ Seeking while writing to file. (62 ms)
      ✓ Seeking while reading a file. (64 ms)
      ✓ Seeking while updating fd pos. (53 ms)
    checking if nlinks gets clobbered.
      ✓ when creating and removing the file. (215 ms)
      ✓ when creating and removing links. (251 ms)

  ● EncryptedFS Concurrency › Appending to a file that is being written for stream

    expect(received).toContain(expected) // indexOf

    Expected substring: "A"
    Received string:    "BBBBBBBBBB"

      820 | 
      821 |     const fileContents = (await efs.readFile('file')).toString();
    > 822 |     expect(fileContents).toContain('A');
          |                          ^
      823 |     expect(fileContents).toContain('B');
      824 |     expect(fileContents).toContain('AB');
      825 | 

      at Object.<anonymous> (tests/EncryptedFS.concurrent.test.ts:822:26)

Test Suites: 2 failed, 14 passed, 16 total
Tests:       2 failed, 314 passed, 316 total
Snapshots:   0 total
Time:        25.165 s, estimated 26 s
Ran all test suites.
GLOBAL TEARDOWN
npm ERR! Test failed.  See above for more details.

First:


  ● EncryptedFS Files › truncate › truncates the fd position

    expect(received).toEqual(expected) // deep equality

    - Expected  - 2
    + Received  + 2

      Object {
        "data": Array [
          100,
    -     98,
    -     99,
    +     101,
    +     102,
        ],
        "type": "Buffer",
      }

      183 |       await efs.ftruncate(fd, 4);
      184 |       await efs.read(fd, buf, 0, buf.length);
    > 185 |       expect(buf).toEqual(Buffer.from('dbc'));
          |                   ^
      186 |       await efs.close(fd);
      187 |     });
      188 |   });

      at Object.<anonymous> (tests/EncryptedFS.files.test.ts:185:19)
  ● EncryptedFS Concurrency › Appending to a file that is being written for stream

    expect(received).toContain(expected) // indexOf

    Expected substring: "A"
    Received string:    "BBBBBBBBBB"

      820 | 
      821 |     const fileContents = (await efs.readFile('file')).toString();
    > 822 |     expect(fileContents).toContain('A');
          |                          ^
      823 |     expect(fileContents).toContain('B');
      824 |     expect(fileContents).toContain('AB');
      825 | 

      at Object.<anonymous> (tests/EncryptedFS.concurrent.test.ts:822:26)

Both appear to involve concurrent writing/reading to file blocks which is the trickiest part, and that which is impacted by #63 (comment)

@CMCDragonkai
Copy link
Member Author

Replacing INodeManager's lock collection with LockBox from @matrixai/async-locks 2.2.0.

@CMCDragonkai
Copy link
Member Author

No problems integrating LockBox.

In doing so I discovered that there's no need to remove entries from the lock collection since the lock entries are now removed when there is nothing holding them.

This does mean that it has to construct the lock object each time someone is trying to lock it. Which can be a bit inefficient.

But at the same time, not having to keep it around reduces programmer burden to figure out when to clear or delete a lock entry.

In the future, the LockBox can additionally be cached if necessary.

@CMCDragonkai
Copy link
Member Author

For the first test failure involving truncates the fd position.

It appears the efs.ftruncate(fd, 4); isn't doing anything.

The data in teh /fdtest should be abcdef, and when truncating it should become abcd. The pointer should be at d, and therefore the end result should be dbc.

I confirmed this with a normal fs. However this is now broken. Need to trace through ftruncate.

@CMCDragonkai
Copy link
Member Author

The ftruncate leads to the INodeManager.fileWriteBlock. At the very end, it says it's writing 4 bytes. However the resulting block is still abcdef and not abcd.

Need to compare with what is in master.

@CMCDragonkai
Copy link
Member Author

In master, it doesn't go into that case at all. It instead goes to fileWriteBlock:

    if (!block) {
      await tran.put(dataDomain, key, data, true);
      bytesWritten = data.length;
    } else {

The reason is because:

    let block = await this.fileGetBlock(tran, ino, idx);

Is undefined. It is looking up ino = 2 and idx = 0.

This is quite strange. Because in master, the block doesn't exist, and thus fileGetBlock is undefined.

However in the current PR, it does exist.

Now should it exist or not? If you already have a file that has abcdef. If you open up a r+ descriptor, that doesn't clear the file.

Therefore it seems to make sense that the block would still exist, and the master is wrong to not have the buffer.

@CMCDragonkai
Copy link
Member Author

Reduced this problem to the simplest reproduction:

import fs from 'fs/promises';

async function main () {

  await fs.writeFile('./tmp/fdtest', 'abcdef');
  const fd = await fs.open('./tmp/fdtest', 'r+');
  await fd.truncate(4);
  await fd.close();
  // File is abcd
  console.log(await fs.readFile('./tmp/fdtest', { encoding: 'utf-8' }));

}

main();

vs:

import os from 'os';
import fs from 'fs';
import pathNode from 'path';
import Logger, { StreamHandler, LogLevel } from '@matrixai/logger';
import { EncryptedFS, utils } from './src';

async function main () {
  const logger = new Logger('EncryptedFS Files', LogLevel.WARN, [
    new StreamHandler(),
  ]);
  const dataDir = await fs.promises.mkdtemp(
    pathNode.join(os.tmpdir(), 'encryptedfs-test-'),
  );
  const dbPath = `${dataDir}/db`;
  const dbKey: Buffer = utils.generateKeySync(256);
  const efs = await EncryptedFS.createEncryptedFS({
    dbKey,
    dbPath,
    umask: 0o022,
    logger,
  });

  await efs.writeFile('/fdtest', 'abcdef');

  // File is abcdef
  console.log(await efs.readFile('/fdtest', { encoding: 'utf-8'}));

  const fd = await efs.open('/fdtest', 'r+');

  // File is abcdef
  console.log(await efs.readFile('/fdtest', { encoding: 'utf-8'}));

  await efs.ftruncate(fd, 4);
  await efs.close(fd);

  // File should be abcd, but is instead abcdef
  console.log(await efs.readFile('/fdtest', { encoding: 'utf-8'}));

}

main();

In the current branch, the resulting file isn't truncated. It's still abcdef.

In the master branch it does it correctly to abcd just like the normal fs.

@CMCDragonkai
Copy link
Member Author

CMCDragonkai commented Apr 18, 2022

The main difference is in INodeManager.fileWriteBlock.

When looking up the block during truncation, in master, we have undefined, which means it ends up writing the abcd.

However in the current branch, it does find the block which means it doesn't write a new block being just abcd, but instead it would "write-over" the abcdef, and thus end up with the same result.

The question is why does master see no block while this current branch does see the block.

@CMCDragonkai
Copy link
Member Author

The fileGetBlocks has been changed from master using createReadStream, to now using the tran.iterator in this current branch.

This could mean that it is correctly acquiring the block in the current branch, because it is still abcdef.

I'm not sure if the fileGetBlocks is correct anyway since it has a while loop that as long as the buffer index is not equal to the block count, it yields empty blocks. But this while loop will never become true/false...?

@CMCDragonkai
Copy link
Member Author

I noticed that ftruncate has to call await this.iNodeMgr.fileClearData(fd.ino, tran); prior to calling fileSetBlocks.

This would mean that it is intended that the block is undefined if fileClearData is meant to clear all the blocks in that sublevel.

However it appears that even though it is clearing the right key: [ 'INodeManager', 'data', '2', <Buffer 00> ], the block is still being found afterwards. This might be a bug in our branch then.

@tegefaulkes
Copy link
Contributor

In the case of rename we have a source and a target path. I'm not sure what to lock for the target path. The target inode will not exist so we can't lock that. Should I lock the parent of the target path? The Dir Inode in this case?

@CMCDragonkai
Copy link
Member Author

CMCDragonkai commented May 2, 2022

The rename might be tricky. You might need to lock all of these things:

  1. The directory containing source and the directory containing target. This could be the same directory. (Make sure not to deadlock!)
  2. If the target is new inode index, it also need to be locked.
  3. If the target is an existing inode, then you must lock that as well.
  4. The source as well.

I have debated with myself on whether to lock the directory then target, or target than directory. I'm not sure what the implications are atm. So you'll see through testing.

@tegefaulkes
Copy link
Contributor

tegefaulkes commented May 3, 2022

task reference

checklist:

  • 1. All methods that use transactions need to check if target exists after it has locked. if the target doesn't exist after locking then the method needs to throw an ENOENT error. Use chroot as an example. Ignore this for _open, mkdir,symlink and mknod. List all methods affected, by extension make a checklist of all methods affected to tick off.
  • 2. rmdir tests should be added for recursive option for the following conditions.
    • When the parent directory lacks the write permission
    • When the parent directory lacks the search permission
    • When the target directory lacks write permission and is empty
    • When the target directory lacks write permission and is not empty
    • When the target directory lacks search permissions and is empty
    • When the target directory lacks search permissions and is not empty
    • When the target directory lacks write permissions, but a subdirectory under the target has write permissions and has files underneath them. The correct behaviour is to match however node's filesystem works. I suggest creating a prototype script that uses fs/promises and running the same test over the real filesystem, observing the side-effect and ensuring that our implementation does the same thing. In my recent changes to rmdir I believe this should be case, but new tests should be written. This test is covered by the 1st two. with lacking write permissions you can remove a directory that is deep enough. With lacking search permissions you can't.
  • 3. concurrency tests need to be updated. They need to be made non-deterministic since we can't determine the results, specifically order. Need to test expected outcome 1 OR outcome 2. since both are valid depending on order completed.
    • fixing up commented out test
    • concurrent mknod
    • concurrent symlink
    • concurrent mixed writes, mkdir, symlink, mknod
    • concurrent mixed readFile and writeFile
    • concurrent mixed read and write on multiple file descriptors
    • concurrent mixed read and write on the same file descriptor
    • concurrent mixed open and deletion
    • concurrent mixed stream read and write
    • ftruncate test according to ./test-fail.ts Correct behaviour should be based off node FS behaviour. Use a prototype script to test how node FS behaves.
  • 4. INode allocation test, we need to test that the inode allocation is working since I updated this. Basically start the filesystem, write things to it, directories, files, take note of all the inodes allocated (delete some files and directories. Shutdown filesystem, recreate the filesystem (via EncryptedFS.start and EncryptedFS.createEncryptedFS) and ensure that the inode allocation reads from the FS. If the iNodeCounter is correct, then the next inode created should be the lowest deallocated inode index. Use the getAll to test this.
  • 5. Remove all the test-* files (scaffolding).
  • 6. Ensure all comments are without . full stop, and don't capitalise the test description or message (this can be done incrementally)
  • 7. Fix up any usage of let variable causing us to loose type information.
list of methods using transactions
  • chroot
  • chdir
  • access
  • chmod
  • chown
  • copyFile - I think this is fixed?
  • fallocate
  • fchmod
  • fchown
  • fstat
  • ftruncate
  • futimes
  • lchmod
  • lchown
  • link - come back to this
  • lseek - come back
  • lstat
  • read
  • readdir
  • readlink
  • rename - come back to this x2
  • stat
  • utimes
  • unlink
  • rmdir - seems fine?
  • navigateFrom - doesn't really need an ENOENT error?
  • symlink
  • mkdir
  • ~ _open~
  • mknod

@tegefaulkes
Copy link
Contributor

There seems to be a bug when concurrently streaming writes to a file and truncating said file. Normally I'd expect that the size of a file when using stat should be equal or greater than the contents of the file. I've found a situation where after truncating and writing the file I end up with the stat size of 27 but the contents has a length of 45.

This is likely a problem with the EncryptedFS.createWriteStream implementation. I'll look into it after finishing task 3.

@tegefaulkes
Copy link
Contributor

I've found another possible bug. When using lseek to move the cursor 20 bytes and then writing 4 bytes using write I'd expect the resulting file to have stat size 24 and the contents to have length 24 with the first 20 bytes filled with 0.

But I'm seeing a stat size of 24 and the contents of the file just the 4 bytes written by write. This is another thing I need to look into.

@tegefaulkes
Copy link
Contributor

tegefaulkes commented May 5, 2022

General ETA for what is left. refering to the above reference. task 3. is hard to pin down. It's revealing small bugs as I progress so the overall time depends on things that are not fully known yet. But first blush i'm expecting up to 1 days to finish the tests and possibly 2 more to fix any bugs. For task 4. should be half a day if that at most. 5. and 6. are trivial and should take no time at all.

So overall;

  • task 3 - 3-ish days
  • task 4 - 0.5 days
  • tasks 5 and 6 - 1 hour.

Should be done within 4 days.

@tegefaulkes tegefaulkes force-pushed the dbanderrors branch 2 times, most recently from b5b5969 to 235f858 Compare May 9, 2022 07:50
@tegefaulkes
Copy link
Contributor

Cleaned up the commits

CMCDragonkai and others added 14 commits May 9, 2022 18:23
We check after the navigation step aswell as after acquiring the lock. This Means we will still throw an `ENOENT` if something happened to the target before acquiring the lock.

This needs to squash into `wip: checking target exists after lock`
Write was not zero filling bytes when seeking past the end of a file. This was only a problem in one specific case. That was when you created a new file, seeked ahead and then wrote to the file. Existing test only checked when the file already existed.
…arameters

When you copied a file and got the stat the resulting blocks and size parameters would be 0. Just needed to set the parameters after copying.
@CMCDragonkai
Copy link
Member Author

The expectReason works against the promise settled result, while expectError works against promises. Would have preferred to call them something like expectErrorPromise and expectErrorResult, but we can proceed.

@CMCDragonkai
Copy link
Member Author

Note that js-errors is updated, but after this PR is merged, we want to update all the other packages again because they will be using the updated js-errors as well and we want all of our exceptions to be using the new toJSON and fromJSON serialisation/deserialisation.

@CMCDragonkai CMCDragonkai changed the title WIP: Upgrading @matrixai/async-init, @matrixai/async-locks, @matrixai/db, @matrixai/errors, @matrixai/workers and integrating @matrixai/resources Upgrading @matrixai/async-init, @matrixai/async-locks, @matrixai/db, @matrixai/errors, @matrixai/workers and integrating @matrixai/resources May 9, 2022
@CMCDragonkai CMCDragonkai merged commit c803148 into master May 9, 2022
@CMCDragonkai CMCDragonkai deleted the dbanderrors branch May 9, 2022 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants