Skip to content

Finish landing ioxide.timer: pack it, and give it a Playground sample - #216

Merged
MDA2AV merged 2 commits into
mainfrom
ci/pack-ioxide-timer
Aug 30, 2026
Merged

Finish landing ioxide.timer: pack it, and give it a Playground sample#216
MDA2AV merged 2 commits into
mainfrom
ci/pack-ioxide-timer

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 30, 2026

Copy link
Copy Markdown
Owner

ioxide.timer landed in #213 but did not land completely: CI never packed it, and it has no Playground sample. Both here.

ci: pack ioxide.timer

The pack steps in the release workflow are one per project and hand-written, so adding a project to ioxide.slnx builds it but does not ship it. That is what happened to 0.7.211 — the SubmitTimeout in core went out, the client over it did not. Checked the rest against the tree while there; every project carrying a PackageId now has a pack step.

No version change is needed: ioxide.timer 0.7.211 was never published, so the next release picks it up at the version its siblings already carry.

Playground/Clients/Timer

Every other package has a sample that is a complete server, a row in bench/samples.tsv, and a pane on the site generated from that same file. A package without one is a package a reader has to learn from its doc comments.

GET /<ms> answers after that many milliseconds:

dotnet run -c Release --project Playground/Clients/Timer
curl http://127.0.0.1:8080/50     # -> 50ms, after 50ms

It holds one RingTimer for the connection and re-arms it per request. That is the shape a caller is meant to copy, and it carries the one constraint worth knowing: a timer holds a single RingOpSource, so it carries one wait at a time — something waiting on several deadlines at once wants a timer each.

It also shows the thing a caller would otherwise get wrong. An expired timeout completes with -ETIME, which is io_uring reporting success, so the check is RingTimer.Expired(result) and not result >= 0. The sample writes errno N into the body when it is anything else, so a mistake there would be visible rather than silent.

The useTaskDelay knob runs the same server with Task.Delay in place of the ring — same response, only the wait changes — so the off-reactor hop it costs is measurable rather than asserted, the way Tcp/Hop is runnable next to Tcp/Raw. The delay comes from the request, so it is clamped; an unbounded one would let a client hold a connection all day.

Registered as h1 on :8080 with PLAYGROUND_DELAY_MS=0, which prices the wait itself against Tcp/Raw rather than measuring a sleep. bench/any.sh --list reports it runnable.

Also here

README.md's client package table was missing ioxide.timer entirely. Added.

The site's example panes are regenerated from the samples, so scripts/gen-docs-panes.py gains the timer entry and docs/index.html gains the tab, the pane and its three CSS rules. Only the new pane changed — the rest were already in sync, and both generators report "already up to date" on a second run.

Verified

Against the running sample: / waits the default 25ms, /50 waits 50ms, /200 waits 200ms, a non-numeric path falls back, the ceiling holds at 60s, and three keep-alive requests on one connection each wait their own 40ms rather than sharing a deadline. The PLAYGROUND_TASK_DELAY=1 arm serves identically. Full solution builds clean, 0 warnings.

MDA2AV added 2 commits August 28, 2026 15:25
The pack steps are one per project and hand-written, so a new package is not
picked up by adding it to the solution - ioxide.slnx builds it and the release
then shipped everything except it. Which is what happened to 0.7.211: the
SubmitTimeout in core went out, the client over it did not.

Checked the rest against the tree while here; every project carrying a
PackageId now has a pack step.
ioxide.timer shipped without one. Every other package has a Playground sample
that is a complete server, is a row in bench/samples.tsv, and is a pane on the
site generated from that same file - so a package without one is a package a
reader has to learn from its doc comments.

Clients/Timer answers GET /<ms> after that many milliseconds. It holds ONE
RingTimer for the connection and re-arms it per request, which is the shape a
caller is meant to copy and the one constraint worth knowing: a timer carries a
single wait at a time, so something waiting on several deadlines at once wants a
timer each. It also shows what a caller would otherwise get wrong - an expired
timeout completes with -ETIME, so the check is RingTimer.Expired and not
result >= 0.

The useTaskDelay knob runs the same server with Task.Delay in place of the ring,
so the hop that costs is measurable rather than asserted, the way Tcp/Hop is
runnable next to Tcp/Raw. The delay comes from the request, so it is clamped;
an unbounded one would let a client hold a connection all day.

Registered as h1 on :8080 with PLAYGROUND_DELAY_MS=0, which prices the wait
itself against Tcp/Raw rather than measuring a sleep.

Also here: README's client package table was missing ioxide.timer entirely,
and the site's example panes are regenerated - only the new one changed, the
rest were already in sync.

Verified against the running sample: / waits the default 25ms, /50 waits 50ms,
a non-numeric path falls back, the ceiling holds at 60s, and three keep-alive
requests on one connection each wait their own 40ms.
@MDA2AV
MDA2AV merged commit dc995e3 into main Aug 30, 2026
1 check passed
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