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

Re-work messaging interface. #372

Merged
merged 9 commits into from
Dec 26, 2019
Merged

Re-work messaging interface. #372

merged 9 commits into from
Dec 26, 2019

Conversation

twavv
Copy link
Member

@twavv twavv commented Dec 23, 2019

The goal of this PR is to rework WebIO's messaging system to be a little bit more simple and more extensible.

What this PR does/will do:

High Level (Architectural) Changes

  • Implement extensible message handling in Julia (using Val{...}-based dispatch)
  • Changes guarantees around command messages (we only guarantee that they are delivered to active connections and do nothing if there are no active connections, see below for reasoning)
  • Change request/response paradigm (see To Be Determined below)

Minor Changes

  • Make evaljs either a request or a command; this needs to be fleshed out, it could either be a kwarg like sync=true which is type-unstable (but then again so is pretty much everything here) or a new function.

To Be Determined

If we reduce the guarantees for sending commands, we need some other ways to do guaranteed delivery. One option is request/response (which we currently do, but is not well defined wrt multiple connections).

One option (that would be pretty close to how we do today) would be something like this.

s = Scope(dom=...)
display(s)

# Async is only necessary for Jupyter/IJulia
@async begin
    wait(s)
    # Do stuff knowing that something has connected
end

We could also expose some hooks to do request/response with individual connections (#353 was an attempt at this). If we do request/response with scopes, the response would have to be a set of responses (corresponding to responses for all of the active connections for a scope).

Glossary

This might not be our final terminology, but my current mental model is this.

  • A message is anything that's sent between Julia and the frontend. It must be one of three types (command, request, or response).
  • A command is a type of message that is fire-and-forget (analogous to an event in a lot of systems). There is no acknowledgment that a command is received and we make no guarantees that any connection receives it (see below for caveats).
  • A request is a type of message that expects a response.
  • A response is a type of message that is the response to a request.
  • A scope is a collection of observables and assets that acts as an "anchor" for the frontend JavaScript (the ancestor scope is available in any JS handler using _webIOScope, which we might give a better name).
  • A scope is said to mount when it is added to the dom. This is when the onimport/onmount handlers run.
  • A mountpoint is a boundary in the DOM where WebIO managed "stuff" begins (e.g., in a Jupyter notebook, the mointpoint is inside of a cell output and every cell output has exactly one mointpoint). This is useful since scopes can mount/unmount many times while a mountpoint only does so once. (This maybe should be renamed since it's similar to but distinct from a scope mounting).
  • An observable is essentially a time-varying value that supports operations such as update and map.
  • An asset is something that the frontend loads (such as a CSS file or JS library).

Why IJulia Makes Things Hard

Fundamentally, WebIO uses a one-to-many communication model (one Julia process and zero or more frontends). We basically make the guarantee that every message sent by the frontend(s) is received by Julia unless the connection is severed (in which case, you're just plain out of luck). Things get interesting in the other direction, especially when you have zero frontends. This complicates the kinds of guarantees (the contract, if you will) we make about messaging.

The canonical examples of commands are setup_scope (only sent from frontend to Julia when a scope mounts and it sets up observable updates) and update_observable. We don't need to worry about setup_scope because it is frontend-to-Julia which has strong guarantees. I'm worried about update_observable's behavior though.

What happens when you send update_observable for a scope that has no active connections? The way that we currently do it (pre-next) is to use an outbox that makes sure that every message is sent to at least one connection. This leads to issues (#330, #343).

@shashi proposed synchronous updates by default (#367) which I'm... mostly... in favor of. The issue with that approach is that it's incompatible with at-least-one-connection delivery when used with IJulia. The reason for this is that we don't handle the setup_scope message until after the request that outputs the scope is done. We get deadlock. This means that essentially, we have the following chain of events.

scope = Scope()
obs = Observable(scope, "obs", "foo")
scope(...) # Put stuff inside of it
display(scope)

obs[] = "bar"
# ^^^ This results in an update_observable command sent to scope
# Since we don't have any connections, and we (hypothetically) don't
# have an outbox, we have to block here. Meanwhile, since we displayed
# the scope, it has mounted and sent a setup_scope message... BUT!
# we can't handle it because IJulia won't start handling that message
# until our current code execution finishes.

# DEADLOCK!

@codecov
Copy link

codecov bot commented Dec 23, 2019

Codecov Report

Merging #372 into next will increase coverage by 8.28%.
The diff coverage is 49.51%.

Impacted file tree graph

@@            Coverage Diff            @@
##             next    #372      +/-   ##
=========================================
+ Coverage   47.42%   55.7%   +8.28%     
=========================================
  Files          17      18       +1     
  Lines         563     578      +15     
=========================================
+ Hits          267     322      +55     
+ Misses        296     256      -40
Impacted Files Coverage Δ
src/WebIO.jl 77.77% <ø> (ø) ⬆️
src/rpc.jl 100% <100%> (+100%) ⬆️
src/scope.jl 54.94% <23.07%> (+0.18%) ⬆️
src/messaging.jl 37.09% <48.83%> (+34.31%) ⬆️
src/connectionpool.jl 51.28% <51.28%> (ø)
src/evaljs.jl 80% <80%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d983987...51ff91a. Read the comment docs.

@twavv
Copy link
Member Author

twavv commented Dec 24, 2019

FORGETMENOT:
The way that requests are passed back to the frontend changed and no longer matches whats in TS. That needs to be updated before merging.

@twavv twavv merged commit ccc74da into next Dec 26, 2019
@twavv twavv deleted the next-pull-on-mount branch December 26, 2019 23:13
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.

1 participant