Skip to content

Implements wait_for()#190

Open
shikokuchuo wants to merge 9 commits into
rstudio:mainfrom
shikokuchuo:wait
Open

Implements wait_for()#190
shikokuchuo wants to merge 9 commits into
rstudio:mainfrom
shikokuchuo:wait

Conversation

@shikokuchuo

@shikokuchuo shikokuchuo commented Oct 17, 2025

Copy link
Copy Markdown
Member

Closes #142, closes #48.

Implements wait_for() — a general-purpose function that synchronizes a promise by blocking until it resolves or rejects, then returns the value or raises the original error condition.

Interrupt behaviour

Chromote's wait_for() uses a synchronize() function with an interrupt domain that, on Ctrl+C:

  1. Catches the interrupt and sets an interrupted flag
  2. Wraps all promise callbacks to check this flag and short-circuit — preventing new work from being scheduled during cleanup
  3. Drains the event loop so cleanup/finally handlers can run
  4. Re-raises the interrupt via tools::pskill()

This is necessary because chromote communicates with an external Chrome process over WebSocket — pending CDP commands must be cancelled and the connection cleaned up gracefully. The interrupt domain (~100 lines) is implemented in chromote because it requires application-specific cleanup logic (cancelling CDP commands, closing WebSocket connections). A general-purpose wait_for() cannot know what cancellation means for an arbitrary promise.

Our wait_for() lets the interrupt propagate naturally. This is the correct approach for a general-purpose utility because most promises don't have a meaningful cancel operation — there is no external actor to notify.

  • Tests
  • News item

cc @schloerke @gadenbuie @cpsievert

@shikokuchuo

shikokuchuo commented Oct 19, 2025

Copy link
Copy Markdown
Member Author

After putting together this PR, I came across the version of wait_for() @lionel- uses internally for tests in coro. This gives me confidence in (i) the naming of the function and (ii) its behaviour on error.

@gadenbuie

Copy link
Copy Markdown
Member

This looks like a great start @shikokuchuo! Have you reviewed the chromote implementation?

Currently this PR resembles various implementations we've used in package testing, which is a much more controlled environment. For a public, robust and final solution in promises we should be very convinced about the features we do or do not need from the chromote implementation.

@shikokuchuo

Copy link
Copy Markdown
Member Author

Thanks @gadenbuie I hadn't actually reviewed chromote as I was under the impression that it wasn't an R-level implementation, let alone one based on promises! Luckily it seems we all converge on the same approach.

Chromote additionally has a lot of code to handle interrupts and my gut reaction is that this isn't necessary in the general case, but this should become apparent when I actually write the tests!

@gadenbuie

Copy link
Copy Markdown
Member

Chromote additionally has a lot of code to handle interrupts and my gut reaction is that this isn't necessary in the general case, but this should become apparent when I actually write the tests!

I think this gets at what I meant by the difference between the implementations we've used in testing and a user-facing API. My inclination is that in user-facing code we'd want, or might even need, to support interrupts and other features that users would need and expect in an interactive session.

@schloerke

Copy link
Copy Markdown
Contributor

Yes. Both @gadenbuie and I would like to have the chromote version with interrupt support.

Thank you, @shikokuchuo !

@shikokuchuo

Copy link
Copy Markdown
Member Author

Just so we're on the same page, the wait_for() currently in this PR does support interrupts. At the underlying level, later::run_now(Inf) is user-interruptible without any side effects. Semantically, this is usually what we want - we're free to wait, stop waiting, or carry on waiting at any time.

The version in chromote seems to be to support the interrupt cancelling the underlying promise itself, and is specifically used together with later_with_interrupt() which is a non-exported function internal to chromote.

@gadenbuie

Copy link
Copy Markdown
Member

Just so we're on the same page, the wait_for() currently in this PR does support interrupts.

Thanks for clarifying, that's great to hear.

The version in chromote seems to be to support the interrupt cancelling the underlying promise itself ...

...my gut reaction is that this isn't necessary in the general case, but this should become apparent when I actually write the tests!

I'm happy to wait to see what you find out when you write the tests, but it'd be helpful in the end if this PR description can include a summary of the chromote code and an explanation of why we don't need to follow the same path here. It'd also be useful to explain whether we can replace chromote's version with promises' solution without breaking backcompat.

@shikokuchuo shikokuchuo marked this pull request as draft October 31, 2025 19:32
@shikokuchuo shikokuchuo marked this pull request as ready for review April 2, 2026 13:51
@shikokuchuo

Copy link
Copy Markdown
Member Author

I'm reviving this PR as I'm not seeing anything obviously wrong with it now (having tweaked it to throw the original condition on rejected promise). Perhaps someone else will see it...

Comment thread R/utils.R
}
private <- attr(promise, "promise_impl")$.__enclos_env__$private
while (private$state == "pending") {
later::run_now(Inf, loop = loop)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This can only consume a single loop at a time, right?

Comment thread R/utils.R
Comment on lines +270 to +272
if (!is.promise(promise)) {
stop("wait_for() requires a promise object")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What about the following:

f <- \() promise_resolve(42)
g <- \() 42
h <- \() ifelse(runif() < 0.5, f(), g())
h() |> wait_for()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just burned myself with mirai and this recently actually: mirais need to be wrapped with as.promise first. So maybe not a good idea to make this magically work for all types.

Is as.promise designed to be idempotent in general?

Comment thread R/utils.R
later::run_now(Inf, loop = loop)
}
if (private$state == "rejected") {
stop(private$value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Call stack?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide a method to make a promise synchronous Provide a way to block and wait for a promise (for testing)

4 participants