. "$BST_LIB" # BST-TIMEOUT: 180 # # redis 7.2.15 (meta-oe recipes-extended) -- the in-memory key/value # data-structure server, its RESP protocol and the redis-cli client. It is the # image's first key/value STORE, as opposed to memcached's cache, and the first # network service here to hold a whole protocol conversation in ONE process: # xmlrpc-c's abyss got there first but forks per connection, and civetweb and # memcached cannot start at all. # # TWO KERNEL GAPS SIT BETWEEN THIS BINARY AND A SERVING DAEMON, and the port # clears both with a patch each rather than leaving the suite red: # # * epoll is ENOSYS -- the whole family is absent from # arch/wasm32/kernel/syscall_table.c (brintos/linux#53). redis picks its # multiplexer from `#ifdef __linux__` with no probe, so it compiled # ae_epoll.c and died in aeCreateEventLoop() before opening a socket. # Patch 0001 narrows that inference and redis takes ae_select.c, its own # portable backend; select(2) IS wired here. The `multiplexing-api` case # below is what proves which backend the running server actually has -- it # reads INFO's own field rather than inferring it, so the day #53 lands and # the patch is dropped, that case is what notices. # # * a thread-mode clone is refused with EAGAIN # (brintos/linux-in-the-browser#85). bioInit() calls a failed # pthread_create Fatal, from InitServerLast() -- i.e. after initServer() # has already created the listening socket, which is what made it read as a # runtime fault rather than a startup refusal. Patch 0002 runs the three bio # workers' jobs -- fsync(2), close(2), free -- inline instead. That is a # latency cost, not a correctness one, and it is available to redis and not # to memcached because redis's command path is single-threaded BY DESIGN. # `no-thread-fatal` and `bio-fallback-logged` are the two halves of that: # one asserts the server did not die, the other that it said why. # # BOTH PATCHES WERE MEASURED NECESSARY, SEPARATELY, and this suite is what # measured them. Against a control built with neither, it reports 11/60 and # names the event loop; against one built with 0001 and without 0002, it reports # 11/60 and names the threads. The eleven that survive both are the packaging # and --version cases, which is the honest floor: they say the port BUILT, and # nothing more. # # WHAT THIS SUITE DOES NOT COVER, said plainly: replication, cluster mode, AOF # rewrite, Lua scripting via EVAL, and the modules API. The first four are # multi-process or multi-file subsystems well beyond the ticket's scope; the # fifth cannot work on a static-only image at all, since MODULE LOAD is # dlopen(3) and there is no shared object to load. # # `sleep N` does not sleep on this image (brintos/linux#48, # linux-in-the-browser#120), so every wait here spins against /proc/uptime. require redis-server redis/present VER=7.2.15 PORT=6479; HEX=194F # printf '%04X' 6479 -- /proc/net/tcp is uppercase hex D=/tmp/.bst-redis rm -rf "$D"; mkdir -p "$D"; cd "$D" || exit 0 up() { cut -d' ' -f1 /proc/uptime; } spin() { _i=0; while [ $_i -lt 2000 ]; do _i=$((_i + 1)); done; } # listening -- true iff /proc/net/tcp carries a LISTEN (state 0A) row # for that local port. Read from /proc so the case asserts the KERNEL's view and # cannot fail for iproute2's reasons. listening() { awk -v p=":$1" '$4 == "0A" && index($2, p) { hit = 1 } END { exit !hit }' \ /proc/net/tcp 2>/dev/null } # rcli -- one redis-cli invocation against our server, bounded. stdout # is RAW because it is not a tty: a bulk reply is its own bytes plus one \n, an # integer is its digits, so every case below reads a value rather than a render. rcli() { timeout 15 redis-cli -h 127.0.0.1 -p $PORT "$@" 2>&1; } # --- packaging --------------------------------------------------------------- inv="" for f in /usr/bin/redis-server /usr/bin/redis-cli /usr/bin/redis-benchmark \ /usr/bin/redis-check-aof /usr/bin/redis-check-rdb \ /usr/bin/redis-sentinel /etc/redis/redis.conf; do [ -e "$f" ] || inv="$inv $f" done check_eq redis/inventory "" "$inv" check_eq redis/resolves-to-port "/usr/bin/redis-server" "$(command -v redis-server)" # The bbappend drops upstream's init script and its systemd unit deliberately: a # data-structure server is started by whoever wants one, and a boot-time # listener on 6379 would also make this suite's bind assertion pass while its # own server failed to start. check_eq redis/no-init-script "" "$(ls /etc/init.d/redis-server 2>/dev/null)" check_eq redis/no-systemd-unit "" "$(ls /lib/systemd/system/redis.service 2>/dev/null)" # useradd.bbclass's `redis` system account, created by shadow-native through # pseudo at do_install and again by the package's preinst at rootfs assembly. check_out redis/useradd-ran "redis:" grep '^redis:' /etc/passwd # Nothing in these binaries is an unresolved-symbol trapping stub -- wasmld # emits one for a symbol it could not resolve, and it traps on first call. check_eq redis/no-trap-stubs "" \ "$(strings /usr/bin/redis-server /usr/bin/redis-cli 2>/dev/null | grep signature_mismatch)" # --- identity, which needs no socket ---------------------------------------- # The banner carries three facts worth asserting separately: the version this # image built, that the arm really is 32-bit, and that meta-oe's 0003 took -- # a jemalloc build would say malloc=jemalloc-x.y.z and would want a background # thread this kernel refuses. check_out redis/version "v=$VER" redis-server --version check_out redis/bits-32 "bits=32" redis-server --version check_out redis/malloc-libc "malloc=libc" redis-server --version check_out redis/cli-version "$VER" redis-cli --version # --- the daemon -------------------------------------------------------------- # Config written here rather than reusing /etc/redis/redis.conf: that file # daemonizes, persists into /var/lib/redis and appends an AOF, none of which # this suite wants. `save ""` keeps RDB snapshots under the cases' control; # maxclients stays well inside FD_SETSIZE, which is the bound select(2) has and # epoll does not (see patch 0001). cat > redis.conf < srv.log 2>&1 & SRV_PID=$! echo $SRV_PID > srv.pid _s=$(up); _s=${_s%.*} while :; do listening $HEX && break kill -0 "$SRV_PID" 2>/dev/null || break _n=$(up); _n=${_n%.*} [ $((_n - _s)) -ge 25 ] && break spin done # The server's EXIT STATUS, collected only when it failed to bind. It is the # evidence gate_of needs and the log cannot give: a syscall that traps rather # than returning ENOSYS kills the process silently, so the last thing in srv.log # is whatever it printed BEFORE the call -- see gate_of. SRV_RC="" listening $HEX || { wait "$SRV_PID" 2>/dev/null; SRV_RC=$?; } # srv_said -- the server's last line, with the two diagnostics it prints on # EVERY start filtered out. Both are survivable and both would otherwise be # mistaken for a gate: getrlimit(RLIMIT_NOFILE) is ENOSYS on this arm # (brintos/linux#20) and redis answers it with a warning and an assumed 1024, # and the memory-overcommit note is advice, not an error. Keying on either would # name a cause that is present on a HEALTHY run, which is the way this kind of # helper goes wrong -- measured: against a control built without patches 0001 # and 0002, the NOFILE warning was the last thing in the log and had nothing to # do with why the process was gone. srv_said() { grep -v 'Unable to obtain the current NOFILE limit' srv.log 2>/dev/null \ | grep -v 'overcommit_memory is set to 0' | tail -1 } # gate_of -- name the gap the server actually hit, # from what IT said and how it DIED, never from which gap we expect. The gaps # land independently and a suite that hardcodes today's answer reports the wrong # issue on the first day one of them changes. # # The exit status is the second argument because a message is not always # available: a syscall that is merely absent returns ENOSYS and redis says so, # but one that TRAPS kills the process where it stands and leaves only whatever # was printed before the call. Both gaps this port patches were MEASURED to be # the first kind -- epoll_create answers ENOSYS ("Failed creating the event # loop. Error message: 'Function not implemented'") and pthread_create answers # EAGAIN ("Fatal: Can't initialize Background Jobs") -- so the signal arm below # is for the gap nobody has met yet, and it names no issue, because guessing # which one would be exactly the mistake the filtering above exists to prevent. gate_of() { _line=$1; _rc=$2 case "$_line" in *"Can't initialize Background Jobs"*|*"Fatal"*"Background Jobs"*) echo "threads refused -- brintos/linux-in-the-browser#85, and patch 0002 is not in this build: $(_bst_1line "$_line")"; return ;; *"Address already in use"*|*"Could not create server TCP listening socket"*) echo "port $PORT was not free or could not be bound: $(_bst_1line "$_line")"; return ;; *"Function not implemented"*) echo "a syscall this arm does not wire answered ENOSYS -- $(_bst_1line "$_line")"; return ;; esac if [ -n "$_rc" ] && [ "$_rc" -gt 128 ]; then echo "the server was killed by signal $((_rc - 128)) with no error of its own -- a trapping syscall rather than one that answers ENOSYS; last line before it: '$(_bst_1line "$_line")'" elif [ -z "$_line" ]; then echo "server exited (rc=${_rc:-unknown}) or never bound, and printed nothing -- gate not identified" else echo "gate not identified -- rc=${_rc:-unknown}, server said: $(_bst_1line "$_line")" fi } GATE="" if listening $HEX; then bst_ok redis/daemon-listens else GATE=$(gate_of "$(srv_said)" "$SRV_RC") bst_notok redis/daemon-listens "$GATE" fi # rcase -- check_eq against the gate. rcase() { _c=$1; _want=$2; shift 2 if [ -n "$GATE" ]; then bst_notok "redis/$_c" "$GATE"; return; fi check_eq "redis/$_c" "$_want" "$(rcli "$@")" } # rvalue -- check_eq over an ARBITRARY command, for # the cases whose value comes out of a pipeline rather than out of one redis-cli # invocation. rcase's arguments are redis-cli's; these are the shell's. rvalue() { _c=$1; _want=$2; shift 2 if [ -n "$GATE" ]; then bst_notok "redis/$_c" "$GATE"; return; fi check_eq "redis/$_c" "$_want" "$("$@" 2>&1)" } # rgate -- a red carrying the gate, for the cases that are not a plain # command/expected pair. rgate() { bst_notok "redis/$1" "$GATE"; } # --- the patched gaps, asserted on the server's own words -------------------- # bioInit()'s message is the WARNING patch 0002 replaced its exit(1) with. Both # halves matter: had the patch not applied, the process would be gone and # daemon-listens would already be red; had it applied but the platform grown # threads, this line would be absent and the case would go red pointing at a # WIN. That is deliberate -- it is the marker that says patch 0002 can go. if [ -n "$GATE" ]; then rgate no-thread-fatal; else case "$(cat srv.log)" in *"Fatal: Can't initialize Background Jobs"*) bst_notok redis/no-thread-fatal "the unpatched fatal is still in this build" ;; *) bst_ok redis/no-thread-fatal ;; esac fi if [ -n "$GATE" ]; then rgate bio-fallback-logged; else check_out redis/bio-fallback-logged \ "running them synchronously in the main thread" cat srv.log fi if [ -n "$GATE" ]; then rgate ready-to-accept; else check_out redis/ready-to-accept "Ready to accept connections" cat srv.log fi # INFO's own field, not an inference: "select" is what patch 0001 bought, and # this case is what notices when brintos/linux#53 lands and it is dropped. rvalue multiplexing-api "multiplexing_api:select" \ sh -c "redis-cli -h 127.0.0.1 -p $PORT INFO server | tr -d '\r' | grep '^multiplexing_api:'" # --- the ticket's protocol scenario ----------------------------------------- rcase ping "PONG" PING rcase set-stored "OK" SET bst:k "hello-wasm32" rcase get "hello-wasm32" GET bst:k rcase strlen "12" STRLEN bst:k # INFO server reports the version just built, from the running process rather # than from --version. rvalue info-version "redis_version:$VER" \ sh -c "redis-cli -h 127.0.0.1 -p $PORT INFO server | tr -d '\r' | grep '^redis_version:'" # --- byte-exact round trip --------------------------------------------------- # The value carries CR, LF, NUL and four high bytes precisely so that anything # line-wise, NUL-terminating or locale-aware gets it wrong. It goes IN through # redis-cli -x (which reads the last argument from stdin, the only binary-safe # route: a NUL cannot travel in argv) and comes back out to a file, so the # comparison is over bytes and never over a shell variable, which would truncate # at the NUL. printf 'redis\015\012wasm32\000\376\377\303\277tail' > payload check_eq redis/fixture-bytes "22" "$(wc -c < payload | tr -d ' ')" if [ -n "$GATE" ]; then rgate set-binary; rgate strlen-binary; rgate get-byte-exact else check_out redis/set-binary "OK" sh -c \ "timeout 15 redis-cli -h 127.0.0.1 -p $PORT -x SET bst:bin < $D/payload" check_eq redis/strlen-binary "22" "$(rcli STRLEN bst:bin)" # raw mode appends exactly one \n to a bulk reply, so the expectation is the # payload plus that newline -- constructed from the payload itself rather than # re-typed, so the fixture and the expectation cannot drift apart. rcli GET bst:bin > got.bin 2>/dev/null { cat payload; printf '\n'; } > want.bin if cmp -s want.bin got.bin; then bst_ok redis/get-byte-exact else bst_notok redis/get-byte-exact \ "$(wc -c < want.bin | tr -d ' ') bytes out, $(wc -c < got.bin | tr -d ' ') back, and they differ"; fi fi # --- counters ---------------------------------------------------------------- rcase incr-1 "1" INCR bst:c rcase incr-2 "2" INCR bst:c rcase incrby "12" INCRBY bst:c 10 rcase decr "11" DECR bst:c rcase incr-type "string" TYPE bst:c # --- expiry ------------------------------------------------------------------ # TTL is asserted as a RANGE, not a value: it counts down in real time and a # case pinned to 100 is a case that goes red on a slow host. -1 (no expiry) and # -2 (no key) are the two answers that must not appear here. rcase expire-set "1" EXPIRE bst:k 100 if [ -n "$GATE" ]; then rgate ttl-remaining; else ttl=$(rcli TTL bst:k) case "$ttl" in ''|*[!0-9]*) bst_notok redis/ttl-remaining "TTL answered '$ttl', not a positive count" ;; *) if [ "$ttl" -gt 0 ] && [ "$ttl" -le 100 ]; then bst_ok redis/ttl-remaining else bst_notok redis/ttl-remaining "TTL $ttl outside (0,100]"; fi ;; esac fi rcase persist "1" PERSIST bst:k rcase ttl-cleared "-1" TTL bst:k # --- deletion ---------------------------------------------------------------- rcase del "1" DEL bst:k rcase exists-after "0" EXISTS bst:k # The nil reply itself, which raw mode renders as an empty line and so cannot be # told from an empty string. --no-raw forces the formatted render, where nil has # a spelling of its own. rcase get-after-delete "(nil)" --no-raw GET bst:k rcase ttl-missing "-2" TTL bst:k # --- data structures, which are the half of redis a cache does not have ------ rcase rpush "3" RPUSH bst:l a b c rcase lrange "a b c" LRANGE bst:l 0 -1 rcase lpop "a" LPOP bst:l rcase llen "2" LLEN bst:l rcase hset "2" HSET bst:h f1 v1 f2 v2 rcase hget "v1" HGET bst:h f1 rcase hlen "2" HLEN bst:h rcase sadd "3" SADD bst:s x y z rcase scard "3" SCARD bst:s rcase zadd "2" ZADD bst:z 1 one 2 two rcase zscore "2" ZSCORE bst:z two rcase type-list "list" TYPE bst:l rcase type-hash "hash" TYPE bst:h # --- persistence, which is where redis needs the syscalls a cache never does - # SAVE is SYNCHRONOUS -- it writes the RDB in the main process and needs no # fork. It is also the one command here that goes through the patched bio path, # since rdbSave() hands the file descriptor to bioCreateCloseJob(). rcase save "OK" SAVE if [ -n "$GATE" ]; then rgate rdb-written; rgate rdb-valid; else check redis/rdb-written test -s "$D/dump.rdb" # The image's own redis-check-rdb reads back what the server just wrote. check_out redis/rdb-valid "RDB looks OK" \ sh -c "timeout 30 redis-check-rdb $D/dump.rdb" fi # BGSAVE is the fork(2) path, and fork WORKS on this arm even though threads do # not -- which is the whole point of separating these two cases. Bounded by # rdb_bgsave_in_progress returning to 0. if [ -n "$GATE" ]; then rgate bgsave-forks; else rm -f "$D/dump.rdb" bg=$(rcli BGSAVE) case "$bg" in *"Background saving started"*) _s=$(up); _s=${_s%.*} while :; do inprog=$(redis-cli -h 127.0.0.1 -p $PORT INFO persistence 2>/dev/null \ | tr -d '\r' | awk -F: '/^rdb_bgsave_in_progress:/ {print $2}') [ "$inprog" = "0" ] && break _n=$(up); _n=${_n%.*} [ $((_n - _s)) -ge 30 ] && break spin done st=$(redis-cli -h 127.0.0.1 -p $PORT INFO persistence 2>/dev/null \ | tr -d '\r' | awk -F: '/^rdb_last_bgsave_status:/ {print $2}') if [ "$st" = "ok" ] && [ -s "$D/dump.rdb" ]; then bst_ok redis/bgsave-forks else bst_notok redis/bgsave-forks "rdb_last_bgsave_status='$st', dump.rdb $(wc -c < "$D/dump.rdb" 2>/dev/null || echo missing)"; fi ;; *) bst_notok redis/bgsave-forks "$(_bst_1line "$bg")" ;; esac fi # --- keyspace bookkeeping ---------------------------------------------------- # Six keys survive this session: bst:bin, bst:c, bst:l, bst:h, bst:s and bst:z. # bst:k was deleted above. The figure pins the fixture as much as the command. rcase dbsize "6" DBSIZE if [ -n "$GATE" ]; then rgate keyspace-hits; else hits=$(redis-cli -h 127.0.0.1 -p $PORT INFO stats 2>/dev/null \ | tr -d '\r' | awk -F: '/^keyspace_hits:/ {print $2}') case "$hits" in ''|*[!0-9]*) bst_notok redis/keyspace-hits "INFO stats gave '$hits'" ;; *) if [ "$hits" -gt 0 ]; then bst_ok redis/keyspace-hits else bst_notok redis/keyspace-hits "keyspace_hits is 0 after this session's reads"; fi ;; esac fi # --- stopping it ------------------------------------------------------------- # SHUTDOWN NOSAVE is the ticket's stop: redis-cli sets config.shutdown and # treats the lost connection as success, so it exits 0 having sent the command. # The daemon's own exit is asserted three ways -- the process is gone, it said # goodbye, and the port is released -- because a daemon that has to be killed is # a different daemon. if [ -n "$GATE" ]; then rgate shutdown-accepted; rgate shutdown-exits; rgate shutdown-says-bye rgate releases-port kill -9 "$(cat srv.pid)" 2>/dev/null else check redis/shutdown-accepted sh -c \ "timeout 15 redis-cli -h 127.0.0.1 -p $PORT SHUTDOWN NOSAVE" _s=$(up); _s=${_s%.*} while kill -0 "$(cat srv.pid)" 2>/dev/null; do _n=$(up); _n=${_n%.*} [ $((_n - _s)) -ge 20 ] && break spin done if kill -0 "$(cat srv.pid)" 2>/dev/null; then bst_notok redis/shutdown-exits "still running 20s after SHUTDOWN NOSAVE" kill -9 "$(cat srv.pid)" 2>/dev/null else bst_ok redis/shutdown-exits fi check_out redis/shutdown-says-bye "ready to exit, bye bye" cat srv.log if listening $HEX; then bst_notok redis/releases-port "port $PORT still LISTENing after the server exited" else bst_ok redis/releases-port fi fi cd /; rm -rf "$D"