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

Bonito connection #212

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
46 changes: 46 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,52 @@ end
serve()
```

In case you need the client side component, Bonito needs an asset server. A convenient option here is to use `NoServer`, which tells Bonito to include all assets inline in the html.

```julia
force_asset_server!(NoServer())
```

In case you need to server side component, Bonito will use its own `WebSocketConnection` by default. In this case Bonito will start another HTTP server on another port and serve. While this may work fine locally, in production you will typically want to have the websocket endpoint served by Oxygen so that it will be easily handled by any infastructure you have configured such as reverse proxies, load balancers, and firewall. For this case, you can use `OxygenWebSocketConnection`.

*Note that the following part of the Bonito instructions is currently experimental and the API may change outside normal semver guarantees.*

In order to make use the following functions defined in the Bonito extension, you must load `Bonito` before first loading `Oxygen`.

```julia
using Bonito
using Oxygen
'''

There are two parts to integrating Bonito with Oxygen:

1. Registering a websocket handler to pass requests to Bonito's ahndler
3. Forcing Bonito to use this connection object `OxygenWebSocketConnection` object which points to the correct Oxygen context and URL

To do both you can simply use the following command:

```julia
function __init__()
# This must be done in __init__ so that functions from the Oxygen Bonito extension are available
Oxygen.setup_bonito_connection(CONTEXT[]; setup_all=true)
end
```

In case you want to change the url of the Bonito websocket route, you can do so via the `route_base` keyword argument, which defaults to `"/bonito_websocket/"`.

You may like to add some custom behaviour to the route such as authentication. In this case, you will need to setup the route yourself like so:

```julia
function __init__()
const route_base = "/foobar/"
oxygen_bonito = Oxygen.setup_bonito_connection(CONTEXT[]; setup_register_connection=true, route_base=route_base)
route_pattern = route_base * "{session_id}"
@websocket route_pattern function mybonitohandler(websocket::HTTP.WebSocket, session_id::String)
# Add custom behaviour here
oxygen_bonito.handler(websocket, session_id)
end
end
```

## Templating

Expand Down
1 change: 1 addition & 0 deletions src/context.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ end
docs :: Documenation = Documenation()
cron :: CronContext = CronContext()
tasks :: TasksContext = TasksContext()
ext :: Dict{Symbol, Any} = Dict{Symbol, Any}()
end

Base.isopen(service::Service) = !isnothing(service.server[]) && isopen(service.server[])
Expand Down
6 changes: 5 additions & 1 deletion src/extensions/load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ function __init__()
# Plotting Extensions #
################################################################
@require CairoMakie="13f3f980-e62b-5c42-98c6-ff1f3baf88f0" include("plotting/cairomakie.jl")
@require Bonito="824d6782-a2ef-11e9-3a09-e5662e0c26f8" include("plotting/bonito.jl")
@require Bonito="824d6782-a2ef-11e9-3a09-e5662e0c26f8" begin
@info "Loading Bonito plotting extension..."
include("plotting/bonito.jl")
include("plotting/bonito_connection.jl")
end
@require WGLMakie="276b4fcb-3e11-5398-bf8b-a0c2d153d008" begin
@require Bonito="824d6782-a2ef-11e9-3a09-e5662e0c26f8" begin
include("plotting/wglmakie.jl")
Expand Down
10 changes: 7 additions & 3 deletions src/extensions/plotting/bonito.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import .Bonito: Page, App

export html


const BONITO_OFFLINE::Ref{Bool} = Ref(true)


"""
Converts a Figure object to the designated MIME type and wraps it inside an HTTP response.
"""
function response(content::App, mime_type::MIME, status::Int, headers::Vector)
function response(content::App, mime_type::MIME, status::Int, headers::Vector, offline=BONITO_OFFLINE[])
# Force inlining all data & js dependencies
Page(exportable=true, offline=true)
Page(exportable=true, offline=offline)

# Convert & load the figure into an IOBuffer
io = IOBuffer()
Expand All @@ -29,4 +33,4 @@ end

Convert a Bonito.App to HTML and wrap it inside an HTTP response.
"""
html(app::App, status=200, headers=[]) :: HTTP.Response = response(app, HTML, status, headers)
html(app::App, status=200, headers=[], offline=BONITO_OFFLINE[]) :: HTTP.Response = response(app, HTML, status, headers, offline)
162 changes: 162 additions & 0 deletions src/extensions/plotting/bonito_connection.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using HTTP.WebSockets: WebSocket, WebSocketError
using HTTP.WebSockets: receive, isclosed
using HTTP.WebSockets

import .Bonito: setup_connection
using .Bonito: FrontendConnection, WebSocketHandler, Session, setup_websocket_connection_js
using .Bonito: run_connection_loop, register_connection!
using .Bonito: CleanupPolicy, DefaultCleanupPolicy, allow_soft_close, soft_close, should_cleanup

export OxygenWebSocketConnection, mk_bonito_websocket_handler, setup_bonito_connection

mutable struct OxygenWebSocketConnection <: FrontendConnection
context::Context
endpoint::String
session::Union{Nothing,Session}
handler::WebSocketHandler
end

function OxygenWebSocketConnection(context::Context, endpoint::String)
return OxygenWebSocketConnection(context, endpoint, nothing, WebSocketHandler())
end

Base.isopen(ws::OxygenWebSocketConnection) = isopen(ws.handler)
Base.write(ws::OxygenWebSocketConnection, binary) = write(ws.handler, binary)
Base.close(ws::OxygenWebSocketConnection) = close(ws.handler)

mutable struct BonitoConnectionContext
cleanup_policy::CleanupPolicy
open_connections::Dict{String, OxygenWebSocketConnection}
cleanup_task::Union{Task, Nothing}
lock::ReentrantLock
end

BonitoConnectionContext(policy=DefaultCleanupPolicy()) = BonitoConnectionContext(policy, Dict{String, Session{OxygenWebSocketConnection}}(), nothing, ReentrantLock())

"""
setup_bonito_connection(
context::Context;
setup_all=false,
setup_route=setup_all,
setup_register_connection=setup_all,
route_base="/bonito_websocket/"
)

This is the high level function to setup bonito connection. It will register the route and create a connection if needed.

In the simplest use case you can simply pass `setup_bonito_connection(CONTEXT[]; setup_all=true)` and it will set up everything for you. Please see the guide for advanced usage.
"""
function setup_bonito_connection(
context::Context;
setup_all=false,
setup_route=setup_all,
setup_register_connection=setup_all,
route_base="/bonito-websocket/"
)
BONITO_OFFLINE[] = false
context.ext[:bonito_connection] = BonitoConnectionContext()
handler = mk_bonito_websocket_handler(context)
if setup_route
Oxygen.Core.register(context, WEBSOCKET, route_base * "{session_id}", handler)
end
function mk_connection()
return OxygenWebSocketConnection(context, route_base)
end
if setup_register_connection
register_connection!(mk_connection, OxygenWebSocketConnection)
end
return (;
mk_connection,
handler
)
end


function mk_bonito_websocket_handler(context::Context)
if !(:bonito_connection in keys(context.ext))
error("bonito_connection not setup in context (did you call setup_bonito_connection(...)?)")
end
bonito_context = context.ext[:bonito_connection]
function bonito_websocket_handler(websocket::HTTP.WebSocket, session_id::String)
local connection
lock(bonito_context.lock) do
connection = bonito_context.open_connections[session_id]
end
session = connection.session
handler = connection.handler

@debug("opening ws connection for session: $(session.id)")
try
run_connection_loop(session, handler, websocket)
finally
if allow_soft_close(bonito_context.cleanup_policy)
@debug("Soft closing: $(session.id)")
soft_close(session)
else
@debug("Closing: $(session.id)")
# might as well close it immediately
close(session)
lock(bonito_context.lock) do
delete!(bonito_context.open_connections, session.id)
end
end
end
end
return bonito_websocket_handler
end

function cleanup_bonito_context(bonito_context)
remove = Set{OxygenWebSocketConnection}()
lock(bonito_context.lock) do
for (session_id, connection) in bonito_context.open_connections
if should_cleanup(bonito_context.cleanup_policy, connection.session)
push!(remove, connection)
end
end
for connection in remove
if !isnothing(connection.session)
session = connection.session
delete!(bonito_context.open_connections, session.id)
close(session)
end
end
end
end

function cleanup_loop(bonito_context)
while true
try
sleep(1)
cleanup_bonito_context(bonito_context)
catch e
if !(e isa EOFError)
@warn "error while cleaning up server" exception=(e, Base.catch_backtrace())
end
end
end
end

function setup_connection(session::Session, connection::OxygenWebSocketConnection)
connection.session = session
context = connection.context
if !(:bonito_connection in keys(context.ext))
error("bonito_connection not setup in context (did you call setup_bonito_connection(...)?)")
end
bonito_context = context.ext[:bonito_connection]
if context.service.external_url[] === nothing
error("external_url not set in context (did you call start the server yet?)")
end
external_url_base = context.service.external_url[]
lock(bonito_context.lock) do
if bonito_context.cleanup_task === nothing
bonito_context.cleanup_task = Threads.@spawn cleanup_loop(bonito_context)
end
bonito_context.open_connections[session.id] = connection
end
external_url = external_url_base * session.connection.endpoint
return setup_websocket_connection_js(external_url, session)
end

function setup_connection(session::Session{OxygenWebSocketConnection})
return setup_connection(session, session.connection)
end
6 changes: 3 additions & 3 deletions src/extensions/plotting/wglmakie.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export html
"""
Converts a Figure object to the designated MIME type and wraps it inside an HTTP response.
"""
function response(content::FigureLike, mime_type::MIME, status::Int, headers::Vector)
function response(content::FigureLike, mime_type::MIME, status::Int, headers::Vector, offline=BONITO_OFFLINE[])
# Force inlining all data & js dependencies
Page(exportable=true, offline=true)
Page(exportable=true, offline=offline)

# Convert & load the figure into an IOBuffer
io = IOBuffer()
Expand All @@ -30,5 +30,5 @@ end

Convert a Makie figure to HTML and wrap it inside an HTTP response.
"""
html(fig::FigureLike, status=200, headers=[]) :: HTTP.Response = response(fig, HTML, status, headers)
html(fig::FigureLike, status=200, headers=[], offline=BONITO_OFFLINE[]) :: HTTP.Response = response(fig, HTML, status, headers, offline)

Loading