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

JS-friendly Deferred.t #10454

Merged
merged 20 commits into from
Mar 14, 2022
Merged

JS-friendly Deferred.t #10454

merged 20 commits into from
Mar 14, 2022

Conversation

mitschabaude
Copy link
Member

@mitschabaude mitschabaude commented Mar 10, 2022

This PR introduces a wrapper library called Promise which has a native and a JS implementation: The native version is just an alias for Async_kernel.Deferred, while the JS version is implemented using JS promises. The mli file common to both versions is designed to expose all the functionality we need from Async_kernel.Deferred.

This strategy bypasses the need for running a Scheduler in JS applications and exposes async operations in a way that is very natural to JS. It fixes the problem that currently, in JS, Deferreds never return.

Within kimchi_bindings and pickles, we use the new Promise.t instead of Deferred.t, so e.g. pickles' async provers can be used from JS. Within libraries not used in JS that depend on pickles, like transaction_snark, we just convert back to a Deferred.t, to keep the diff small.

TODO: So far, this strategy is only applied to the provers returned by pickles. It still has to be done for verify.

@mitschabaude mitschabaude added the ci-build-me Add this label to trigger a circle+buildkite build for this branch label Mar 10, 2022
@mitschabaude mitschabaude requested a review from a team as a code owner March 10, 2022 12:39
@mitschabaude mitschabaude changed the title [WIP] JS-friendly Deferred.t JS-friendly Deferred.t Mar 10, 2022
@mitschabaude
Copy link
Member Author

Not ready to review yet - there is still an issue with promise handling

Copy link
Member

@bkase bkase left a comment

Choose a reason for hiding this comment

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

Are we breaking any contracts of Deferred to have things automatically start executing without starting a scheduler? I think @nholland94 is familiar with those contracts.

Perhaps we want the JS implementation to be unit -> Js.Promise so that we can explicitly start it in the same way that Deferreds are started.

@mitschabaude
Copy link
Member Author

mitschabaude commented Mar 11, 2022

Are we breaking any contracts of Deferred to have things automatically start executing without starting a scheduler? I think @nholland94 is familiar with those contracts.

Perhaps we want the JS implementation to be unit -> Js.Promise so that we can explicitly start it in the same way that Deferreds are started.

@bkase, AFAIU, once a scheduler is up and running, Deferreds don't have to be started explicitly. They are just values stored in cells that are empty at the beginning and get "filled" eventually, and the scheduler has the task of running the operations that are associated with that value getting filled, i.e. call the listeners. In JavaScript, the built-in JS event loop plays that role of the scheduler. So in my view, in JS the contract is now equivalent to the native side when it starts a scheduler right at the beginning, which I think is fine. (We could potentially implement it in a way so there has to be an explicit global "start_scheduler" operation before Deferreds run.. I don't see the value of that, IMO that makes it harder to use)

@mitschabaude
Copy link
Member Author

This is ready for review now!
I fixed a remaining problem (Promise.bind wasn't implemented correctly). On the snarkyjs branch, which also includes these changes, I'm now able to obtain the pickles proof inside a promise in JS!

Copy link
Member

@bkase bkase 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 good to me pending the few questions I asked, and you're explanation makes sense, but would love someone familiar with the inner workings of Deferreds also on the review for this PR

src/lib/blockchain_snark/blockchain_snark_state.ml Outdated Show resolved Hide resolved
src/lib/promise/native_helpers/promise_native_helpers.ml Outdated Show resolved Hide resolved
src/lib/promise/js/promise.js Outdated Show resolved Hide resolved
src/lib/promise/js/promise.js Outdated Show resolved Hide resolved
src/lib/promise/js/promise.js Show resolved Hide resolved
@nholland94
Copy link
Member

Won't this change the concurrency control flow quite a bit? I fear this could lead to a lot of bugs. IIRC, promise chains in javascript run as much code synchronously as possible until an I/O operation is hit (as javascripts builtin threading model is to only switch tasks for I/O and event handlers). If this understanding of javascript promises is correct, then you are essentially swapping out our concurrency model for something that works very differently, which will lead to hard to debug race conditions that only occur in the javascript environment.

let to_deferred promise =
let module Ivar = Async_kernel.Ivar in
let ivar = Ivar.create () in
upon promise (fun x -> Ivar.fill ivar x) ;
Copy link
Member

Choose a reason for hiding this comment

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

I know I suggested this, but I don't think it'll handle exceptions properly, since upon will have swallowed the exception. We probably need an upon_exn, and to call this to_deferred_exn.

Copy link
Member Author

Choose a reason for hiding this comment

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

@mrmr1993 As currently implemented, when the promise rejects, this will throw an error, which cannot be caught in any way (because upon creates a new promise, which will reject when it's parent promise rejects, but the new promise doesn't get exposed so it can't be caught)

Copy link
Member Author

Choose a reason for hiding this comment

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

So this is not ideal obiously, but I'm wondering: How are exceptions in deferreds usually handled? I'd want to emulate that

Copy link
Member

Choose a reason for hiding this comment

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

They get caught by the scheduler, so the point at which we attach the promise to the scheduler feels like the right time to raise. The current implementation is really upon_exn (which I missed in review), and the real upon can be

function deferred_upon(deferred, func) {
  deferred.promise.then(function () {
    func(deferred.value);
  }).catch(function(){});
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I changed the upon_exn / upon thing but I'm hesitant to call this function to_deferred_exn, since to_deferred can't throw an exception in native ocaml and this JS version was actually just supposed to be a dummy function (we don't have a way of using Deferred in JS after all). So I'd say this function, with upon called upon_exn, is "good enough" for now

@mrmr1993
Copy link
Member

Won't this change the concurrency control flow quite a bit? I fear this could lead to a lot of bugs. IIRC, promise chains in javascript run as much code synchronously as possible until an I/O operation is hit (as javascripts builtin threading model is to only switch tasks for I/O and event handlers). If this understanding of javascript promises is correct, then you are essentially swapping out our concurrency model for something that works very differently, which will lead to hard to debug race conditions that only occur in the javascript environment.

@nholland94 I think this is important for us to be careful around in general, but I don't think we need to worry here. Concretely, the only Deferred.ts that we end up interacting with are created out of foreign functions (which will be implemented in javascript for SnarkyJS, designed to interact with the JS promise model), and so they will function as intended.

The other option would be to more-closely simulate the async scheduler, by changing all of the .then(foo) calls to

.then(function(input){
    return new Promise(function(resolve) {
        setTimeout(function() { resolve(foo(input)); }, 0);
    });
})

although this feels like overkill to me.

@mitschabaude
Copy link
Member Author

mitschabaude commented Mar 11, 2022

@mrmr1993 Yeah, that version would lift Promise execution out of the the current microtask queue, into the macrotask queue. But as I discussed with Nathan in Slack, the current version already doesn't execute them synchronously, but queues them as a microtask

@mrmr1993 mrmr1993 added ci-build-me Add this label to trigger a circle+buildkite build for this branch and removed ci-build-me Add this label to trigger a circle+buildkite build for this branch labels Mar 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ci-build-me Add this label to trigger a circle+buildkite build for this branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants