dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.08k stars 1.56k forks source link

[breaking change] Remove dart:cli waitFor experiment #52121

Closed mraleph closed 6 months ago

mraleph commented 1 year ago

Change Intent

waitFor contradicts Dart's event-loop model in a way that no other old or new feature does: it allows async and synchronous code to interleave. The feature has been marked experimental since the beginning and effectively has only two users: sass and dcli package. It is not available in Flutter or on the Web because dart:cli library is not exposed there.

See discussion on https://github.com/dart-lang/sdk/issues/39390 for context.

Justification

We would like to reduce VM's technical debt by removing this experimental functionality, which we believe was added hastily and without due thought to the consequences.

We also have some indications (based on some internal code which has been migrated from waitFor) that availability of waitFor encourages ineffecient and convoluted coding patterns.

If you compare waitFor with Finalizer (which was added in 2.17) you will observe that we choose to make Finalizer less powerful by specifying that finalizers are only invoked at the turn of the event loop for the sake of maintaining semantic purity around interleaving of synchronous code.

Impact

Current users (which basically amounts to dcli and sass) will have to migrate off waitFor which will require them to rewrite their code.

Mitigation

Based on our analysis of dcli and sass multiple migration strategies are available:

Note that it is an explicit non-goal to provide a completely equivalent replacement for waitFor, because we believe the feature itself is incompatible with how Dart's async is designed and should be removed.

Synchronous communication over dart:ffi

This code demonstrates how dart:ffi can be used to establish an entirely synchronous communication channel between the main (dispatcher) isolate and a worker isolate. This type of communication channel should cover sass needs.

To make migration simpler we will provide a portable synchronization package which implements mutexes and conditional variables, though we leave implementation of cross-isolate communication channels to the developers.

Isolate.resolvePackageUri

Some minor users of waitFor use it to synchronously unwrap Future returned from Isolate.resolvePackageUri API. In reality the underlying implementation of Isolate.resolvePackageUri is entirely synchronous.

While there is migration path for this code using dart:ffi and isolates we can simplify things by directly exposing Isolate.resolvePackageUriSync(...) and deprecating async version of this API.

Timeline

The removal will follow this timeline:

tsavo-at-pieces commented 5 months ago

Okay solid - interesting situation here - are you able to share a git link to the problematic code? I'm wrapping up named locks tonight and can take a look at this as well (no promises but definitely happy to take a crack at it locally)!

tsavo-at-pieces commented 5 months ago

I'm assuming the you're calling something like mailbox.put within the isolate before calling mailbox.take?

bsutton commented 5 months ago

The code in question is here:

https://github.com/onepub-dev/dcli/blob/master/dcli/lib/src/process/process/process_in_isolate.dart

After spawning isolate the call to _connectSendPort makes the call to take.

I'm locally playing with some alternate patterns, but if you have any ideas I would be pleased to here them.

bsutton commented 5 months ago

I'm assuming the you're calling something like mailbox.put within the isolate before calling mailbox.take?

This is the crux of the problem.

Because we can't 'await' the spawn we can't know if the mailbox has been primed via a call to put. Any sync call (from the primary isolate) to determine if the mailbox is primed, stops the spawn completing.

Catch 22.

On Wed, Apr 3, 2024 at 8:20 AM Tsavo Knott @.***> wrote:

I'm assuming the you're calling something like mailbox.put within the isolate before calling mailbox.take?

— Reply to this email directly, view it on GitHub https://github.com/dart-lang/sdk/issues/52121#issuecomment-2033118791, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG32OHFPT2QHCKIAZTX7ADY3MOIHAVCNFSM6AAAAAAXGRJJN6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMZTGEYTQNZZGE . You are receiving this because you were mentioned.Message ID: @.***>

tsavo-at-pieces commented 5 months ago

@bsutton okay solid - I believe named locks can actually help here with something like the following usage.

  1. Create a NamedLock instance on the Main Thread and immediately lock it
  2. Pass in the name of the NamedLock to the Isolate through the _startIsolate(settings, channel); call
  3. Create a NamedLock instance within the Isolate Thread from the passed name
  4. The Isolate then tries to acquire the NamedLock with a blocking NamedLock.acquire() call
  5. Back on the main thread, right after the _startIsolate(settings, channel); call, we call proceed to call NamedLock.unlock() from the main thread allowing the isolate to successfully complete its NamedLock.acquire() call, lock the NamedLock within the Isolate and allow it to proceed and call mailbox.put()
  6. Again on the main thread, we call NamedLock.acquire() ahead of our mailbox.take() can introduce a small sleep if needed but this main thread NamedLock.acquire() will block mailbox.take() until the isolate calls NamedLock.unlock()
  7. Lastly on the Isolate side right after it calls mailbox.put() it proceeds to call NamedLock.unlock() or NamedLock.dispose() unblocking the main threads blocking NamedLock.acquire() preceding the call to mailbox.take()
bsutton commented 5 months ago

6 is problematic. As we can't know if the spawned isolate ever took the lock. So we would have to sleep (which slows launch down) and we still have no guarantees.

But the real problem is that the sleep is sync which will stop the isolate spawning.

I could be wrong but I don't think the isolate gets spawned until some Async code executes. If this is correct the there is no way around the problem.

I think we need some input from the dart Devs.

On Wed, 3 Apr 2024, 9:29 am Tsavo Knott, @.***> wrote:

@bsutton https://github.com/bsutton okay solid - I believe named locks can actually help here with something like the following usage.

  1. Create a NamedLock instance on the Main Thread and immediately lock it
  2. Pass in the name of the NamedLock to the Isolate through the _startIsolate(settings, channel); call
  3. Create a NamedLock instance within the Isolate Thread from the passed name
  4. The Isolate then tries to acquire the NamedLock with a blocking NamedLock.acquire() call
  5. Back on the main thread, right after the _startIsolate(settings, channel); call, we call proceed to call NamedLock.unlock() from the main thread allowing the isolate to successfully complete its NamedLock.acquire() call, lock the NamedLock within the Isolate and allow it to proceed and call mailbox.put()
  6. Again on the main thread, we call NamedLock.acquire() ahead of our mailbox.take() can introduce a small sleep if needed but this main thread NamedLock.acquire() will block mailbox.take() until the isolate calls NamedLock.unlock()
  7. Lastly on the Isolate side right after it calls mailbox.put() it proceeds to call NamedLock.unlock() or NamedLock.dispose() unblocking the main threads blocking NamedLock.acquire() preceding the call to mailbox.take()

— Reply to this email directly, view it on GitHub https://github.com/dart-lang/sdk/issues/52121#issuecomment-2033208826, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAG32OC7UVVQAUEHL4SYO4LY3MWM3AVCNFSM6AAAAAAXGRJJN6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAMZTGIYDQOBSGY . You are receiving this because you were mentioned.Message ID: @.***>

tsavo-at-pieces commented 5 months ago

Absolutely - looking forward to input from Dart Devs as well!

Last thing to note is that the NamedLocks implementation I've been working on has a native shared memory map/counter to count the number of threads/processes that have a NamedLocks instance with an unresolved NamedLocks.acquire() call. If the number of expected & unique threads calling NamedLock.acquire() is deterministic then perhaps we can leverage this to know?

bsutton commented 5 months ago

I've raised this issue to create spawnSync as I think it is the only workable solution:

https://github.com/dart-lang/sdk/issues/55356

tsavo-at-pieces commented 5 months ago

Awesome - good looks 🫡 Looking forward to hearing more from the Dart team!

We'll get there - it's always a bit frustrating in the moment but gonna be solid 🤝

appreciate you @bsutton!

julemand101 commented 5 months ago

Deleted my comment after further research since I can see you want to interact with the spawned process in a sync matter with stdin and stdout.