lua: asynchronous invoke, and the descriptor a request answered with - #26
Open
ddimension wants to merge 2 commits into
Open
lua: asynchronous invoke, and the descriptor a request answered with#26ddimension wants to merge 2 commits into
ddimension wants to merge 2 commits into
Conversation
conn:call() runs ubus_invoke(), which spins its own event loop until the answer arrives. In a daemon that already runs uloop this stalls everything else it is doing — timers, sockets, other callbacks — for as long as the peer takes. conn:call_async(object, method, params, callback [, timeout]) hands the request to the uloop the connection is already attached to and returns immediately. The callback runs exactly once with (result_or_nil, status). lua/test_async.lua shows the difference by counting timer ticks across both, and demonstrates the deadline. On an ipq807x access point: blocking call: 0 timer ticks while it ran deferred call: 107 timer ticks while it ran, status 0 timed out as expected, status 7 (UBUS_STATUS_TIMEOUT is 7) What this is for: the same process answers RADIUS for hostapd there, and with macaddr_acl=2 the access point stays silent until that answer is in. Every blocking ubus call was a window in which no station could associate. Three things libubus leaves to the caller and this has to get right: - There is no deadline on an asynchronous request. A peer that never answers keeps it on the pending list forever, so each call carries its own uloop timeout and reports UBUS_STATUS_TIMEOUT. It defaults to the connection's timeout, the same one the synchronous path uses. - ubus_abort_request() does not run complete_cb, it only unlinks the request. Exactly one of the two paths therefore frees the state, guarded by a flag. - The callback goes through lua_pcall, not lua_call: it runs from a uloop callback, and an error thrown out of there would longjmp past libubus' own bookkeeping. Signed-off-by: André Valentin <avalentin@marcant.net>
A ubus method may answer with a file descriptor instead of with data.
'log read' with stream:true is the case this exists for: logd writes the
log down a pipe rather than returning it, which is why
`ubus call log read '{"stream":true}'` prints nothing — the CLI does not
render one. libubus has carried the mechanism all along: libubus.h
ubus_fd_handler_t, req->fd_cb, and ubus_process_req_msg() closes the
descriptor itself when no handler claims it.
call_async() now claims it and passes it to the callback as a third value.
Third value rather than a second callback because Lua discards extra
arguments: every existing cb(result, status) keeps working unchanged, and
one that wants the descriptor takes cb(result, status, fd).
A descriptor on its own is of no use to Lua, and not for want of taste.
io.* wants a FILE*; posix, nixio and lfs are on none of the OpenWrt
devices this was written for, and luasocket, which is there, cannot wrap
a descriptor either. Handing out a number nobody can read from or close
would be a feature in name only, so three small functions come with it:
ubus.read_fd(fd [, bytes]) the bytes, or nil plus 'eof' or 'again'.
Never blocks: the descriptor is put into
non-blocking mode on first use, which is
what a reader driven by uloop wants — a
spurious wakeup then costs an EAGAIN rather
than a stalled process.
ubus.close_fd(fd) whoever is handed one has to close it.
ubus.blob_decode(buffer) one blob attribute out of a byte string,
with the number of bytes it consumed.
blob_decode is needed because the stream is not text. logd writes blob
attributes, each length prefixed and already carrying its fields: on an
access point a record is 116 bytes holding msg, id, priority, source and
time. That is better than the rendered syslog line, which would have to
be taken apart again with a regular expression, and id is a sequence
number, so a reader can tell when it has missed something. Framing
belongs in C because the format does; buffering stays in Lua, where a
growing string is the whole of it.
uloop needs no change: get_sock_fd() in libubox already accepts a bare
number, so uloop.fd_add(fd, cb, ULOOP_READ) works as it stands.
Measured end to end on OpenWrt 25.12: call_async('log', 'read',
{stream = true, lines = 0}) delivers fd 7, uloop reports it readable,
read_fd returns the bytes, blob_decode turns them into
{id = 10544, msg = 'fdtest: blob test eins', priority = 28, source = 1,
time = 1787514663721}, and close_fd returns true.
Signed-off-by: André Valentin <avalentin@marcant.net>
ddimension
pushed a commit
to ddimension/openwrt-repo
that referenced
this pull request
Aug 23, 2026
… to use it
A ubus method may answer with a file descriptor instead of with data. 'log
read' with stream:true is the one this exists for, and it is why
`ubus call log read '{"stream":true}'` prints nothing — the CLI does not render
one. libubus has carried the mechanism all along (ubus_fd_handler_t,
req->fd_cb) and closes the descriptor itself when nothing claims it.
call_async() now claims it and passes it as a third value, so every existing
cb(result, status) keeps working and one that wants it takes a third parameter.
The three functions alongside are not extras. On the access points this feed
builds for there is no posix, no nixio and no lfs — checked on the fleet — and
luasocket, which is there, cannot wrap a descriptor either; io.* wants a FILE*.
A fd handed to Lua and nothing else would be a number nobody could read from or
close. So: read_fd, close_fd, and blob_decode, the last because the stream is
not text — logd writes length prefixed blob attributes carrying msg, id,
priority, source and time.
Measured end to end on ap-av-attic: fd delivered, uloop reports it readable,
read_fd returns the bytes, blob_decode turns them into a table, close_fd
returns true. Also sent upstream, on top of the call_async patch, as the second
commit of openwrt/ubus#26.
Signed-off-by: André Valentin <avalentin@marcant.net>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Lua binding offers
conn:call()and nothing else. It runsubus_invoke(),which spins its own event loop until the answer arrives, so in a program that
already runs uloop everything else stops for the duration — timers, sockets,
other callbacks.
conn:call_async(object, method, params, callback [, timeout])hands therequest to the uloop the connection is already attached to and returns
immediately. The callback runs exactly once with
(result_or_nil, status).Why it matters here
The process this was written for runs an on-AP RADIUS server for hostapd out of
the same uloop. With
macaddr_acl=2hostapd stays silent until that answer isin, so every blocking ubus call was a window in which no station could
associate.
Demonstration
lua/test_async.luacounts timer ticks across a blocking and a deferred calland then shows the deadline. On an ipq807x access point:
Three things libubus leaves to the caller
ubus_complete_request_async()takes no timeout — only thesynchronous
ubus_complete_request()does — so a peer that never answerskeeps the request on the pending list forever. Each call therefore carries
its own
uloop_timeoutand reportsUBUS_STATUS_TIMEOUT. It defaults to theconnection's timeout, the same one the synchronous path uses.
ubus_abort_request()does not runcomplete_cb, it only unlinks therequest. Exactly one of the two paths frees the state, guarded by a flag.
lua_pcall, notlua_call. The callback runs from a uloop callback, andan error thrown out of there would longjmp past libubus' own bookkeeping.
The existing callbacks in this file use
lua_call; I did not think that areason to add another one.
Testing
CMakeLists.txt(
-Wall -Werror -Wextra -Wformat -Werror=format-security -Werror=format-nonliteral -Werror=implicit-function-declaration -Os -g3 -Wmissing-declarations -Wno-unused-parameter -std=gnu99): no new warnings,and the unmodified file is equally clean under the same flags.
aarch64_cortex-a53and for 32 bitarm_cortex-a7throughthe OpenWrt 25.12 SDK, and against master.
both ad-hoc calls and batches of about thirty.
Nothing the existing binding does changes;
call_asyncis added besidecall.Second commit: the descriptor a request answered with
A ubus method may answer with a file descriptor instead of with data.
ubus call log read '{"stream":true}'prints nothing for exactly that reason —logd writes the log down a pipe, and the CLI does not render one. libubus has
carried the mechanism all along (
ubus_fd_handler_t,req->fd_cb), andubus_process_req_msg()closes the descriptor itself when nothing claims it.call_async()now claims it and passes it as a third value, so everyexisting
cb(result, status)keeps working and one that wants the descriptortakes
cb(result, status, fd).A descriptor alone is of no use to Lua, and not for want of taste:
io.*wantsa
FILE*, and on the OpenWrt devices this was written for there is noposix,no
nixioand nolfs—luasocketis there and cannot wrap one either. Sothree small functions come with it:
ubus.read_fd(fd [, bytes])nilplus'eof'or'again'. Never blocks — the descriptor is put into non-blocking mode on first use, which is what a reader driven by uloop wantsubus.close_fd(fd)ubus.blob_decode(buffer)blob_decodeis needed because the stream is not text. logd writes blobattributes, each length prefixed and already carrying its fields — on an access
point a record is 116 bytes holding
msg,id,priority,sourceandtime. Framing belongs in C because the format does; buffering stays in Lua,where a growing string is the whole of it.
uloopneeds no change:get_sock_fd()in libubox already takes a bare number.Measured end to end on OpenWrt 25.12: fd 7 delivered, uloop reports it readable,
read_fdreturns the bytes,blob_decodeyields{id = 10544, msg = "fdtest: blob test eins", priority = 28, source = 1, time = 1787514663721},close_fdreturns true.