Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,61 @@ jobs:
if: success() || failure()
run: odin check examples/complete -vet --strict-style && odin check examples/client -vet --strict-style
timeout-minutes: 1

build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
opt: [none, minimal, size, speed, aggressive]
pkg:
- .

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4
- uses: laytan/setup-odin@v2
with:
release: nightly

- name: Build ${{ matrix.pkg }}
shell: bash
run: |
if [ "${{ matrix.opt }}" = "none" ]; then
odin build ./${{ matrix.pkg }}/ -build-mode:lib -vet -strict-style -o:none -debug
else
odin build ./${{ matrix.pkg }}/ -build-mode:lib -vet -strict-style -o:${{ matrix.opt }}
fi

test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
opt: [none, minimal, size, speed, aggressive]
pkg:
- internal/mpsc

runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v4
- uses: laytan/setup-odin@v2
with:
release: nightly

- name: Test ${{ matrix.pkg }}
shell: bash
run: |
# On Windows, limit test threads to 1 if the package uses concurrent operations.
# Currently internal/mpsc tests are single-threaded; flag kept for future test additions.
THREAD_FLAGS=""
if [ "${{ runner.os }}" = "Windows" ]; then
THREAD_FLAGS="-define:ODIN_TEST_THREADS=1"
fi
if [ "${{ matrix.opt }}" = "none" ]; then
odin test ./${{ matrix.pkg }}/ -vet -strict-style -disallow-do -o:none -debug $THREAD_FLAGS -define:ODIN_TEST_FANCY=false
else
odin test ./${{ matrix.pkg }}/ -vet -strict-style -disallow-do -o:${{ matrix.opt }} $THREAD_FLAGS -define:ODIN_TEST_FANCY=false
fi
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ minimal
complete
readme
routing

# Local build scripts (not part of the submodule).
build_and_test_debug.sh
build_and_test.sh
53 changes: 53 additions & 0 deletions examples/async/doc.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
Async handler examples for odin-http.

Async handlers let the handler return immediately and get called again with the result.

The same proc handles both calls. res.work_data == nil on the first call:

my_handler :: proc(h: ^http.Handler, req: ^http.Request, res: ^http.Response) {
if res.work_data == nil {
// first call: allocate work struct, mark_async, start background work, return
} else {
// second call: work is done, send response, free work struct
defer { res.work_data = nil }
}
}

The first call runs inside nbio.tick() — return quickly, the event loop is blocked
until you do. The second call runs after tick() returns, when the server loop
calls the second parts of pending async handlers.

Allocate the work struct in the first call, store it via mark_async (saved in
res.work_data), read it in the second call, free before returning.

Examples
--------
ping_pong.odin no thread; body callback calls mark_async + resume inline
without_body_async.odin no body needed; spawns a thread in the first call
with_body_async.odin reads body first; body callback spawns the thread

Note: these use thread.create per request to keep the flow easy to follow.
In production, use a worker pool. Only the completion code calls http.resume(res).

API
---
mark_async(h, res, work)
Tells the server this request is going async.
Call it before starting background work.

cancel_async(res)
Call it to undo mark_async when background work fails to start.

resume(res)
Schedules the second handler call. Call it from the background thread when work
is done, exactly once. Don't touch res after this.

The background thread owns the work struct and may call resume once. From a
background thread:
- don't read or write res fields - res purpose just carry "async" info between calls
- don't call any http.* proc except resume
- don't allocate from context.temp_allocator (it's the per-connection arena, not thread-safe)

*/
package async_examples
46 changes: 46 additions & 0 deletions examples/async/ping_pong.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package async_examples

import http "../.."

// No background thread. Body callback calls mark_async + resume directly on the IO thread,
// inside nbio.tick(). The second handler call happens after tick() returns.

Ping_Pong_Work :: struct {
body: string,
}

ping_pong_handler :: proc(h: ^http.Handler, req: ^http.Request, res: ^http.Response) {
if res.work_data == nil {
// body callback only gets user_data (res), not h
res.async_handler = h
http.body(req, -1, res, ping_pong_callback)
return
}

work := (^Ping_Pong_Work)(res.work_data)
defer {res.work_data = nil}

if work.body == "ping" {
http.respond_plain(res, "pong")
} else {
http.respond(res, http.Status.Unprocessable_Content)
}
}

// runs on the IO thread inside nbio.tick(); temp_allocator is already the connection arena
ping_pong_callback :: proc(user_data: rawptr, body: http.Body, err: http.Body_Error) {
res := (^http.Response)(user_data)
if err != nil {
http.respond(res, http.body_error_status(err))
return
}

work := new(Ping_Pong_Work, context.temp_allocator)
work.body = string(body)

// mark_async before resume — same rule as the threaded patterns
http.mark_async(res.async_handler, res, work)

// schedules the second handler call — it runs after tick() returns
http.resume(res)
}
81 changes: 81 additions & 0 deletions examples/async/with_body_async.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package async_examples

import http "../.."
import "core:mem"
import "core:thread"
import "core:time"

Body_Context :: struct {
alloc: mem.Allocator,
}

Body_Work :: struct {
alloc: mem.Allocator,
thread: ^thread.Thread,
body: string,
result: string,
}

body_handler :: proc(h: ^http.Handler, req: ^http.Request, res: ^http.Response) {
if res.work_data == nil {
// body callback only gets user_data (res), not h
res.async_handler = h
http.body(req, -1, res, body_callback)
return
}

work := (^Body_Work)(res.work_data)
defer {
thread.join(work.thread) // the thread is already done — it called resume before we got here
thread.destroy(work.thread)
free(work, work.alloc)
res.work_data = nil
}

http.respond_plain(res, work.result)
}

// runs on the IO thread inside nbio.tick(), after the full body is received
body_callback :: proc(user_data: rawptr, body: http.Body, err: http.Body_Error) {
res := (^http.Response)(user_data)
ctx := (^Body_Context)(res.async_handler.user_data)

if err != nil {
http.respond(res, http.body_error_status(err))
return
}

work := new(Body_Work, ctx.alloc)
work.alloc = ctx.alloc
work.body = string(body)

// mark_async before thread.start, same as the direct pattern
http.mark_async(res.async_handler, res, work)

t := thread.create(body_background_proc)
if t == nil {
// both required: cancel_async tells the server, respond tells the client
http.cancel_async(res)
free(work, ctx.alloc)
http.respond(res, http.Status.Internal_Server_Error)
return
}
t.data = res
work.thread = t
thread.start(t)
}

body_background_proc :: proc(t: ^thread.Thread) {
res := (^http.Response)(t.data)
work := (^Body_Work)(res.work_data)

// context.temp_allocator is the connection's arena — not ours to use from a background thread
old_temp := context.temp_allocator
defer {context.temp_allocator = old_temp}

time.sleep(10 * time.Millisecond)

// write result before calling resume — don't touch res after that
work.result = work.body
http.resume(res)
}
66 changes: 66 additions & 0 deletions examples/async/without_body_async.odin
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package async_examples

import http "../.."
import "core:mem"
import "core:thread"
import "core:time"

Without_Body_Context :: struct {
alloc: mem.Allocator,
}

Without_Body_Work :: struct {
alloc: mem.Allocator,
thread: ^thread.Thread,
result: string,
}

without_body_handler :: proc(h: ^http.Handler, req: ^http.Request, res: ^http.Response) {
ctx := (^Without_Body_Context)(h.user_data)

if res.work_data == nil {
work := new(Without_Body_Work, ctx.alloc)
work.alloc = ctx.alloc

// mark_async before thread.start — resume must not schedule the second call before mark_async runs
http.mark_async(h, res, work)

t := thread.create(without_body_background_proc)
if t == nil {
// both required: cancel_async tells the server, respond tells the client
http.cancel_async(res)
free(work, ctx.alloc)
http.respond(res, http.Status.Internal_Server_Error)
return
}
t.data = res
work.thread = t
thread.start(t)
return
}

work := (^Without_Body_Work)(res.work_data)
defer {
thread.join(work.thread) // the thread is already done — it called resume before we got here
thread.destroy(work.thread)
free(work, work.alloc)
res.work_data = nil
}

http.respond_plain(res, work.result)
}

without_body_background_proc :: proc(t: ^thread.Thread) {
res := (^http.Response)(t.data)
work := (^Without_Body_Work)(res.work_data)

// context.temp_allocator is the connection's arena — not ours to use from a background thread
old_temp := context.temp_allocator
defer {context.temp_allocator = old_temp}

time.sleep(10 * time.Millisecond)

// write result before calling resume — don't touch res after that
work.result = "hello from background"
http.resume(res)
}
Loading