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

Define a parallel queue for all file system operations #95

Merged
merged 15 commits into from
Jun 13, 2023

Conversation

a-sully
Copy link
Collaborator

@a-sully a-sully commented Jan 18, 2023

Fixes #74 by specifying a parallel queue for all file system operations, not just locking (see https://github.com/whatwg/fs/pull/9/files#r1109679577)

  • At least two implementers are interested (and none opposed):
    • Chromium
    • WebKit
  • Tests are written and can be reviewed and commented upon at:
    • N/A (unless there's a way to deterministically test race conditions that I don't know about)
  • Implementation bugs are filed:
    • Chromium: ...
    • Gecko: …
    • WebKit: …
  • MDN issue is filed: …

Preview | Diff

@a-sully
Copy link
Collaborator Author

a-sully commented Jan 18, 2023

cc @jesup @szewai

@a-sully a-sully requested a review from annevk January 18, 2023 01:15
@a-sully a-sully mentioned this pull request Jan 18, 2023
3 tasks
index.bs Outdated Show resolved Hide resolved
Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks better, but not quite there yet. I'll be out next week, but happy to look at this again in February.

index.bs Outdated
1. Increase |count| by one.
1. [=Queue a task=] to |replyTaskSource| to run |onLockTakenAlgorithm|
and return.
1. [=Reject=] |p| with a "{{NoModificationAllowedError}}" {{DOMException}}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to queue a task for this as well. But maybe it makes more sense that you don't pass the promise in to this algorithm and you have a single callback algorithm that you then pass success/failure to or some such?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense! The original algorithm returned true or false, but it now runs the passed-in algorithm with "success" and "failure" explicitly, which seems more clear. Let me know if there's a better way to do this!

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly lots of nits, but this looks to be heading in the right direction to me!

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
a-sully added a commit to a-sully/fs that referenced this pull request Feb 15, 2023
Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With some more task queueing I think this can land.

index.bs Outdated Show resolved Hide resolved
index.bs Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
@a-sully a-sully changed the title Define a parallel queue for lock operations Define a parallel queue for all file system operations Feb 24, 2023
@a-sully
Copy link
Collaborator Author

a-sully commented Feb 24, 2023

Updated to post all file system operations to a "file system queue", as we discussed offline. Lock take/release is once again sync (and must be called from that queue). Please feel free to nit-pick this as much as you please, since this pattern will be applied to all the other methods. Thanks!

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall setup here seems sound from a quick skim. The main problem is that "reject", "resolve", and "create a new X" are operations that need to happen on the main thread. And as such you need to do the "queue a storage task" dance for them.

Note that you don't necessarily need to replace each individually. You might be able to restructure things a bit so do all the in parallel work first and safe the results in variables and then queue a storage task that does the rejecting/resolving as per the variables.

Also, what kind of exception can "request access" throw, out of curiosity? Since we get that exception while in parallel, forwarding it probably doesn't entirely work. If we could normalize that to it not returning "granted" that would be better.

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
@a-sully
Copy link
Collaborator Author

a-sully commented Mar 2, 2023

The overall setup here seems sound from a quick skim. The main problem is that "reject", "resolve", and "create a new X" are operations that need to happen on the main thread. And as such you need to do the "queue a storage task" dance for them.

Oh I see... Question - does this work? If not, why not?

1. [=Queue a storage task=] with [=this=]'s [=relevant global object=] to
   [=enqueue the following steps=] to the [=file system queue=]:
   1. ...

Note that you don't necessarily need to replace each individually. You might be able to restructure things a bit so do all the in parallel work first and safe the results in variables and then queue a storage task that does the rejecting/resolving as per the variables.

Also, what kind of exception can "request access" throw, out of curiosity? Since we get that exception while in parallel, forwarding it probably doesn't entirely work. If we could normalize that to it not returning "granted" that would be better.

Unfortunately I think this is needed, since it can reject with a SecurityError. See https://wicg.github.io/file-system-access/#permissions

@annevk
Copy link
Member

annevk commented Mar 2, 2023

Question - does this work? If not, why not?

Because you still end up in parallel when doing "resolve", etc. You need some things to happen "in parallel" and then after those things you queue a task for things to happen on the main thread. Such as resolving a promise or creating a JS object.

Unfortunately I think this is needed, since it can reject with a SecurityError.

Hmm, I guess we should redesign this somewhat whereby "request access" returns some kind of internal value and we then later turn that into the appropriate exception.

@a-sully
Copy link
Collaborator Author

a-sully commented Apr 4, 2023

Question - does this work? If not, why not?

Because you still end up in parallel when doing "resolve", etc. You need some things to happen "in parallel" and then after those things you queue a task for things to happen on the main thread. Such as resolving a promise or creating a JS object.

Ah yes, looking at this with fresh eyes I see the problem. I've updated a few methods - if that pattern looks good, I'll copy it everywhere else

Unfortunately I think this is needed, since it can reject with a SecurityError.

Hmm, I guess we should redesign this somewhat whereby "request access" returns some kind of internal value and we then later turn that into the appropriate exception.

This is a bit tricky because the WICG spec considers file system access a "powerful feature", and powerful features have permission request algorithms that are allowed to throw. I've added an explicit warning that this spec doesn't comply with that pattern, which seems like it should be good enough?

Anyways, I've updated the access checks to return either a PermissionState or an error code that can be turned into a DomException (though I wonder if returning (as opposed to throwing) a DomException directly is an allowed pattern?)

If this all seems reasonable, I'll file an issue to update the WICG spec (and update all the instances in this spec which assume these algorithms can throw)

index.bs Outdated
Comment on lines 78 to 84
<p class=warning> Dependent specifications may consider this API a
[=powerful feature=]. However, unlike other [=powerful features=] whose
[=permission request algorithm=] may throw, [=/file system entry=]'s
[=file system entry/query access=] and [=file system entry/request access=]
algorithms must run [=in parallel=] on the [=file system queue=] and are
therefore not allowed to throw. Instead, the caller is expected to [=/reject=]
as appropriate should these algorithms return an [=exception/error name=].
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really should go in WICG spec, but since we're doing something a bit funky it might be useful to have here, too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they run in parallel the caller will also be in parallel. They wouldn't be able to immediately reject something.

But also I'm not a big fan of this design. Perhaps these should return a struct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they run in parallel the caller will also be in parallel. They wouldn't be able to immediately reject something.

Updated to say the caller is expected to [=queue a storage task=] to [=/reject=], as appropriate

... though while going through the whole spec I realized that there is one situation - the asynchronous iterator initialization steps - where there is no promise to reject. See the other comment

But also I'm not a big fan of this design. Perhaps these should return a struct?

Done. AFAICT a struct item cannot be "optional" (only algorithm parameters?) so "error name" is just empty most of the time

index.bs Outdated Show resolved Hide resolved
@a-sully
Copy link
Collaborator Author

a-sully commented May 10, 2023

if that pattern looks good, I'll copy it everywhere else

friendly ping @annevk. This infra PR is holding up the PRs specifying move() and remove()

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patience. This is looking rather good. Except the access checks, those could do with some more design work.

index.bs Outdated
Comment on lines 78 to 84
<p class=warning> Dependent specifications may consider this API a
[=powerful feature=]. However, unlike other [=powerful features=] whose
[=permission request algorithm=] may throw, [=/file system entry=]'s
[=file system entry/query access=] and [=file system entry/request access=]
algorithms must run [=in parallel=] on the [=file system queue=] and are
therefore not allowed to throw. Instead, the caller is expected to [=/reject=]
as appropriate should these algorithms return an [=exception/error name=].
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they run in parallel the caller will also be in parallel. They wouldn't be able to immediately reject something.

But also I'm not a big fan of this design. Perhaps these should return a struct?

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
Copy link
Collaborator Author

@a-sully a-sully left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As promised, I've updated the whole spec to follow the pattern. Note that much of the diff is just formatting (e.g. adding an indentation)

I've manually double-checked that all promise rejections and resolutions now happen from the "storage task source" and all access checks are done in-parallel from the file system queue (though it's still possible I've missed something...)

index.bs Outdated Show resolved Hide resolved
index.bs Outdated
Comment on lines 78 to 84
<p class=warning> Dependent specifications may consider this API a
[=powerful feature=]. However, unlike other [=powerful features=] whose
[=permission request algorithm=] may throw, [=/file system entry=]'s
[=file system entry/query access=] and [=file system entry/request access=]
algorithms must run [=in parallel=] on the [=file system queue=] and are
therefore not allowed to throw. Instead, the caller is expected to [=/reject=]
as appropriate should these algorithms return an [=exception/error name=].
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they run in parallel the caller will also be in parallel. They wouldn't be able to immediately reject something.

Updated to say the caller is expected to [=queue a storage task=] to [=/reject=], as appropriate

... though while going through the whole spec I realized that there is one situation - the asynchronous iterator initialization steps - where there is no promise to reject. See the other comment

But also I'm not a big fan of this design. Perhaps these should return a struct?

Done. AFAICT a struct item cannot be "optional" (only algorithm parameters?) so "error name" is just empty most of the time

index.bs Outdated
1. Set |accessErrorName| to |accessResult|'s
[=file system access result/error name=] if it is not
« the empty string »; otherwise "{{NotAllowedError}}".
1. [=Throw=] an |accessErrorName| {{DOMException}}.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've specified that query access requires going in parallel on the file system queue, but if we get an exception there's no associated promise to reject...

... @mkruisselbrink do you know if this is this even necessary? Or is it enough to check query access in just the "get the next iteration result" algorithm below?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to do anything based on these steps. Per https://webidl.spec.whatwg.org/#idl-async-iterable we can omit it I think.

cc @domenic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tentatively omitted it for now, pending input from others

index.bs Outdated
Comment on lines 767 to 771
1. [=/Reject=] |promise| with an |accessErrorName| {{DOMException}}
and return |promise|.

1. If |directory| is `null`, [=/reject=] |result| with a
"{{NotFoundError}}" {{DOMException}} and abort these steps.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is the only "behavioral" change in this PR. Presumably the "NotAllowedError" should take precedence over the "NotAllowedError", just like everywhere else

@@ -135,7 +176,7 @@ A <dfn export id=directory>directory entry</dfn> additionally consists of a [=/s
<dfn for="directory entry">children</dfn>, which are themselves [=/file system entries=].
Each member is either a [=/file entry=] or a [=/directory entry=].

A [=/file system entry=] |entry| should be [=list/contained=] in the [=children=] of at most one
A [=/file system entry=] |entry| should be [=list/contained=] in the [=directory entry/children=] of at most one
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: this fixes a spec build failure

index.bs Outdated Show resolved Hide resolved
index.bs Outdated
Comment on lines 88 to 91
:: A [=string=] which must be either « the empty string » or an
[=exception/error name=] listed in the [=error names table=].
If [=file system access result/permission state=] is
"{{PermissionState/granted}}" this must be « the empty string »
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it so that when permission is denied it has to be an error name so we don't have to do defaulting below?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. This simplifies things quite a lot

Side note: is "iff" okay to use in specs? Happy to spell it out if not

index.bs Outdated
1. Set |accessErrorName| to |accessResult|'s
[=file system access result/error name=] if it is not
« the empty string »; otherwise "{{NotAllowedError}}".
1. [=Throw=] an |accessErrorName| {{DOMException}}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem to do anything based on these steps. Per https://webidl.spec.whatwg.org/#idl-async-iterable we can omit it I think.

cc @domenic

@a-sully
Copy link
Collaborator Author

a-sully commented Jun 7, 2023

@annevk friendly ping (this is holding up a number of other changes). If this looks good, feel free to merge

Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found a couple remaining nits, generally looks good. Feel free to merge after addressing them. Though I should have time tomorrow if you'd rather I do it.

index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
index.bs Outdated Show resolved Hide resolved
Copy link
Member

@annevk annevk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to approve this to make that possible. Doh.

@a-sully
Copy link
Collaborator Author

a-sully commented Jun 13, 2023

Thanks for the review! I'll merge this now so we can start making progress on the various PRs that are blocked on it. If you have more nits, I'm happy to address them in a follow-up

Just an FYI that I also updated the permission checks to use the new [=DOMException/name=] and "names table" added in whatwg/webidl#1211 (which broke the refs we were adding)

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.

Consider specifying a parallel queue for operations which acquire locks
2 participants