Stream: ideas

Topic: basic-webserver init()


view this post on Zulip Luke Boswell (Aug 06 2024 at 23:16):

I'd like to add a Task for basic-webserver that can run server initialisation setup.

Just wondering if anyone has any thoughts on this.

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:17):

So the API I'm think we would change to is basically

main : {
    init : Task {} [Exit I32 Str]_,
    handle : Request -> Task Response [],
}

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:20):

Potential use-cases I have in mind

  1. open sqlite connections, and create the prepared statements for later (re)use
  2. run any external setup build scripts like e.g. tailwindcss, rtl etc.
  3. check everything in the environment is in a correct state before starting up, possibly run some "tests"

view this post on Zulip Hannes (Aug 06 2024 at 23:20):

Do you mean basic web server?

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:21):

oops, yes -- fixed the thread name

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:21):

Essentially, a little basic-cli like script that can run before the server start handling requests

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:22):

Oh, for the sqlite setup we would need to return a Model

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:22):

So actually something like

main : {
    init : Task model [Exit I32 Str]_,
    handle : Request, model -> Task Response [],
}

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:24):

Here the model is read-only. We can support Tasks that can update the model in future, but for that we need to do a big upgrade to handle multithreaded state that plays nicely with roc which is difficult before we have effect interpreters and passed in allocators from what I understand.

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:25):

The platform will just box the Model to pass to the host, and the host just passes that in with each request

view this post on Zulip Brendan Hansknecht (Aug 06 2024 at 23:32):

Yeah, host should just need to set the box refcount to constant

view this post on Zulip Brendan Hansknecht (Aug 06 2024 at 23:32):

And I think it should work

view this post on Zulip Brendan Hansknecht (Aug 06 2024 at 23:33):

I'm not sure about it running RTL at startup, but no way to stop it.

view this post on Zulip Richard Feldman (Aug 06 2024 at 23:51):

yeah I like this! :thumbs_up:

view this post on Zulip Richard Feldman (Aug 06 2024 at 23:51):

assuming it can work using today's compiler :big_smile:

view this post on Zulip Luke Boswell (Aug 06 2024 at 23:52):

Brendan Hansknecht said:

I'm not sure about it running RTL at startup, but no way to stop it.

I was thinking of doing that using a Command, so not necessarily building this in or anything.

view this post on Zulip Brendan Hansknecht (Aug 07 2024 at 00:12):

Yeah I know

view this post on Zulip Brendan Hansknecht (Aug 07 2024 at 00:13):

Richard Feldman said:

assuming it can work using today's compiler :big_smile:

It can, but you have to wire some stuff manually and it's a bit jank

view this post on Zulip Brendan Hansknecht (Aug 07 2024 at 00:13):

We do it for wasm4

view this post on Zulip Luke Boswell (Aug 07 2024 at 09:52):

Ok, so WIP but I'm thinking of going with this API

platform "webserver"
    requires { Model } { server : {
        init : Task Model [Exit I32 Str]_,
        respond : Http.Request, Model -> InternalTask.Task Http.Response [ServerErr Str]_,
    } }
    exposes [
        Path,
        Dir,
        Env,
        File,
        FileMetadata,
        Http,
        Stderr,
        Stdout,
        Task,
        Tcp,
        Url,
        Utc,
        Sleep,
        Command,
        SQLite3,
    ]
    packages {}
    imports [
        Task.{ Task },
        Stderr.{ line },
    ]
    provides [forHost]

import InternalTask
import Http
import InternalHttp
import Task

ForHost : {
    init : Task (Box Model) I32,
    respond : InternalHttp.RequestToAndFromHost, Box Model -> InternalTask.Task InternalHttp.ResponseToHost [],
}

forHost : ForHost
forHost = { init, respond }

init : Task (Box Model) I32
init =
    Task.attempt server.init \res ->
        when res is
            Ok model -> Task.ok (Box.box model)
            Err (Exit code str) ->
                if Str.isEmpty str then
                    Task.err code
                else
                    line str
                    |> Task.onErr \_ -> Task.err code
                    |> Task.await \{} -> Task.err code

            Err err ->
                line
                    """
                    Program exited with error:
                        $(Inspect.toStr err)

                    Tip: If you do not want to exit on this error, use `Task.mapErr` to handle the error.
                    Docs for `Task.mapErr`: <https://www.roc-lang.org/packages/basic-cli/Task#mapErr>
                    """
                |> Task.onErr \_ -> Task.err 1
                |> Task.await \_ -> Task.err 1

respond : InternalHttp.RequestToAndFromHost, Box Model -> InternalTask.Task InternalHttp.ResponseToHost []
respond = \request, boxedModel ->
    when server.respond (InternalHttp.fromHostRequest request) (Box.unbox boxedModel) |> Task.result! is
        Ok response -> Task.ok response
        Err (ServerErr msg) ->
            # prints the error message to stderr
            line! msg

            # returns a http server error response
            InternalTask.ok {
                status: 500,
                headers: [],
                body: [],
            }

        Err err ->
            line!
                """
                Server error:
                    $(Inspect.toStr err)

                Tip: If you do not want to see this error, use `Task.mapErr` to handle the error.
                Docs for `Task.mapErr`: <https://www.roc-lang.org/packages/basic-webserver/Task#mapErr>
                """

            InternalTask.ok {
                status: 500,
                headers: [],
                body: [],
            }

view this post on Zulip Luke Boswell (Aug 07 2024 at 09:53):

Taking some inspiration from this discussion https://roc.zulipchat.com/#narrow/stream/383402-API-Design/topic/Default.20error.20handling.20in.20basic-webserver/near/436375969

view this post on Zulip Luke Boswell (Aug 07 2024 at 10:03):

Here's how hello-web looks -- I prefer it without all the type annotations... but leaving here for completness

app [Model, server] { pf: platform "../platform/main.roc" }

import pf.Stdout
import pf.Task exposing [Task]
import pf.Http exposing [Request, Response]
import pf.Utc

Model : {}

server = { init, respond }

init : Task Model [Exit I32 Str]_
init = Task.ok {}

respond : Request, Model -> Task Response [ServerErr Str]_
respond = \req, _ ->

    # Log request datetime, method and url
    datetime = Utc.now! |> Utc.toIso8601Str

    Stdout.line! "$(datetime) $(Http.methodToStr req.method) $(req.url)"

    Task.ok { status: 200, headers: [], body: Str.toUtf8 "<b>Hello, world!</b>\n" }

view this post on Zulip Luke Boswell (Aug 08 2024 at 09:54):

Ok, I've made a PR https://github.com/roc-lang/basic-webserver/pull/64

I've only upgraded the stubbed app and hello-web examples.

I am not super confident in how I've done things. Sometimes it runs perfectly fine, and then sometimes I make a random change, and all of a sudden it starts crashing -- but I'm not sure, maybe I've got it right now.

If you want to check this out, just jump on that branch then.

$ nix develop
$ roc build.toc
$ roc examples/hello-web.roc

@Brendan Hansknecht could you please have a look at my implementation, particularly how I'm threading the captures and Model through to each request.

Next step is to update the examples to new API and testing. But this should be pretty straightforward as long as the impl is correct.

view this post on Zulip Luke Boswell (Aug 08 2024 at 10:18):

I had trouble getting it working with a singleton global. I just remembered why I was trying to do that. I dont want the roc init task running multiple times because it's effectful. So I'll need to find a way to run that once, and then give each thread a copy of the Model and captures.

view this post on Zulip Brendan Hansknecht (Aug 09 2024 at 00:49):

Probably tomorrow I can take some time and try to fix up the implementation.

view this post on Zulip Luke Boswell (Aug 09 2024 at 00:51):

I've included a simple_bench binary to help throw requests at the server and find issues.

view this post on Zulip Luke Boswell (Aug 09 2024 at 00:52):

It's working so well, I know my implementation as a serious problem somewhere :sweat_smile:

view this post on Zulip Brendan Hansknecht (Aug 09 2024 at 16:54):

Just a note, you can also throw request with a lot of tools. wrk is a simple one: wrk -t 4 -c 64 -d 20 http://127.0.0.1:8000 is what I have been doing for simple benchmarking.

view this post on Zulip Brendan Hansknecht (Aug 09 2024 at 16:55):

Also, probably need to pull up a linux machine and look at this in valgrind. Though lifetimes should be correct now, something is still breaking memory.

view this post on Zulip Brendan Hansknecht (Aug 09 2024 at 18:02):

Oh, I found it. Simple double free.

view this post on Zulip Brendan Hansknecht (Aug 09 2024 at 18:06):

Ok, I think I have pushed fixes such that it is ready to just port all the examples over

view this post on Zulip Brendan Hansknecht (Aug 10 2024 at 06:37):

I pushed a bunch more updates, may not be 100% yet, but I think essentially everything is working and updated.

view this post on Zulip Brendan Hansknecht (Aug 10 2024 at 16:39):

I think https://github.com/roc-lang/basic-webserver/pull/64 is fully ready for review and merge.

view this post on Zulip Anton (Aug 10 2024 at 16:43):

Awesome :heart: I'll check it out


Last updated: Jun 16 2026 at 16:19 UTC