Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
611862f
Add build-time async backend switch and port server (co)
dallison Jun 8, 2026
2f04fd6
Add asio Unix-domain socket facade and port client (additive)
dallison Jun 8, 2026
019d0a2
Compile server and client handler under the asio backend (additive)
dallison Jun 8, 2026
ce9564c
Add asio multi-threading, graceful shutdown, and vsock bridging
dallison Jun 9, 2026
7112cd6
test: make vsock loopback probe mirror the server's bridge bind
dallison Jun 9, 2026
e3115a0
cmake: link subspace_async into client and server libraries
dallison Jun 9, 2026
15c81a9
client: add async::Context Wait overloads for the asio backend
dallison Jun 9, 2026
1167b11
ci: cover the asio backend on Android and run the full asio suite
dallison Jun 9, 2026
f97d288
server: fix data races in the multi-threaded asio bridge path
dallison Jun 9, 2026
9850634
async: fix multi-threaded missed-wakeup hang in fd waits
dallison Jun 9, 2026
ebab348
server: fix bridged-address key mismatch in transmitter teardown
dallison Jun 9, 2026
fa33848
test: keep multi-threaded bridge stress test off split buffers
dallison Jun 9, 2026
b3853e9
ci: harden the android-asio emulator lane against flakiness
dallison Jun 9, 2026
fb5ee62
async: fix asio shutdown use-after-free in graceful cancellation
dallison Jun 9, 2026
1a0ccb9
server: don't block the io_context thread in RetirementCoroutine (asio)
dallison Jun 9, 2026
377fe68
test: give each bridge retirement/basic test its own channel
dallison Jun 9, 2026
af88a83
test: run the client/stress test server on 4 asio threads
dallison Jun 10, 2026
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
91 changes: 91 additions & 0 deletions .github/scripts/android-asio-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/bin/bash
set -euo pipefail

# Runs the Boost.Asio-backend subspace test binaries on the Android emulator.
# The binaries are expected to have been cross-compiled beforehand with
# `--config=android_x86_64 --//:coro_backend=asio` so that bazel-bin/ points at
# the asio-configured outputs. Only the backend-dependent targets are run here;
# the co-backend coverage lives in the regular `android` job.

# Wait for device to be fully available after emulator boot.
adb wait-for-device
adb root
sleep 5
adb wait-for-device
adb shell "while [[ -z \$(getprop sys.boot_completed) ]]; do sleep 1; done"

adb shell "mkdir -p /data/local/tmp"

# Collect shared libraries (dereference symlinks from bazel output).
rm -rf /tmp/android_libs
mkdir -p /tmp/android_libs
find bazel-bin/ -name "*.so" -path "*_solib*" -exec cp -L {} /tmp/android_libs/ \; 2>/dev/null || true
find bazel-bin/plugins/ -name "*.so" -exec cp -L {} /tmp/android_libs/ \; 2>/dev/null || true

if ls /tmp/android_libs/*.so 1>/dev/null 2>&1; then
adb shell "rm -rf /data/local/tmp/android_libs"
adb push /tmp/android_libs /data/local/tmp/android_libs
fi

LIB="LD_LIBRARY_PATH=/data/local/tmp/android_libs"

# Push test binaries (asio backend).
adb push bazel-bin/server/subspace_server /data/local/tmp/subspace_server
adb push bazel-bin/client/client_test /data/local/tmp/client_test
adb push bazel-bin/client/bridge_test /data/local/tmp/bridge_test
adb push bazel-bin/common/split_buffer_test /data/local/tmp/split_buffer_test
adb push bazel-bin/asio_rpc/test/rpc_test /data/local/tmp/asio_rpc_test
adb push bazel-bin/asio_rpc/server/server_test /data/local/tmp/asio_rpc_server_test
adb push bazel-bin/asio_rpc/client/client_test /data/local/tmp/asio_rpc_client_test

# Push plugin .so files to the relative path the tests load them from. Remove
# the target directory first so a stale directory cannot shadow the file.
rm -rf /tmp/android_plugins
mkdir -p /tmp/android_plugins
cp -L bazel-bin/plugins/nop_plugin.so /tmp/android_plugins/
cp -L bazel-bin/plugins/split_buffer_free_test_plugin.so /tmp/android_plugins/
adb shell "rm -rf /data/local/tmp/plugins"
adb push /tmp/android_plugins /data/local/tmp/plugins

# Make binaries executable.
adb shell "chmod +x /data/local/tmp/*_test /data/local/tmp/subspace_server"

# Run a test binary on the emulator, retrying a few times to absorb transient
# flakiness. The asio backend multiplexes work across real threads and is far
# more sensitive to scheduling latency than the cooperative co backend, and the
# GitHub-hosted x86_64 emulator (2 cores, swiftshader) is slow enough that
# timing-sensitive tests occasionally miss a deadline. The same tests pass
# reliably on the real Linux/macOS asio runners; the retry keeps the emulator
# smoke lane stable without masking a genuine, reproducible regression.
run_with_retry() {
local desc="$1"
local cmd="$2"
local attempts=3
local n
for n in $(seq 1 "$attempts"); do
echo "=== ${desc} (attempt ${n}/${attempts}) ==="
if adb shell "cd /data/local/tmp && ${LIB} ${cmd}"; then
return 0
fi
echo "--- ${desc} attempt ${n} failed ---"
sleep 3
done
echo "FAILED: ${desc} after ${attempts} attempts"
return 1
}

run_with_retry "split_buffer_test (asio)" "./split_buffer_test"
run_with_retry "client_test (asio)" "./client_test"

# Exclude the multi-threaded bridge stress test on the emulator: it spawns 4
# asio io-context threads x 8 channels x 1000 messages, which is both
# ill-matched to a 2-core emulator and the heaviest source of timing flakiness
# here. Its multi-core thread-safety coverage runs on the real Linux/macOS
# asio runners (the asio-backend jobs), which exercise the full bridge_test.
run_with_retry "bridge_test (asio)" "./bridge_test --gtest_filter=-BridgeStressTest.*"

run_with_retry "asio_rpc_test" "./asio_rpc_test"
run_with_retry "asio_rpc_server_test" "./asio_rpc_server_test"
run_with_retry "asio_rpc_client_test" "./asio_rpc_client_test"

echo "=== All Android asio tests passed ==="
106 changes: 106 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,48 @@ jobs:
name: bazel-test-logs-${{ matrix.os }}-${{ matrix.mode }}
path: bazel-testlogs

asio-backend:
name: asio-backend (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
bazel_flags:
- os: macos-latest
bazel_flags: --config=macos_arm64

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Install Bazel
uses: bazel-contrib/setup-bazel@0.18.0
with:
bazelisk-cache: true
disk-cache: ${{ github.workflow }}-${{ matrix.os }}-asio
repository-cache: true

# Test the full backend-dependent suite with the Boost.Asio backend
# selected. async_test exercises the asio WaitReadable/WaitEither/Sleep
# and the StreamSocket/UDPSocket facade; client_test and bridge_test run
# the real asio client/server port (including discovery, bridging and the
# multi-threaded data plane) end to end.
- name: Build and test asio backend
run: |
bazel test \
//common/async:async_test \
//common:split_buffer_test \
//client:client_test \
//client:bridge_test \
//server:server_test \
--//:coro_backend=asio \
--verbose_failures \
--test_output=errors \
${{ matrix.bazel_flags }}

diagnostics:
name: diagnostics (${{ matrix.os }}, ${{ matrix.mode }})
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -382,3 +424,67 @@ jobs:
with:
name: android-test-logs
path: bazel-testlogs

android-asio:
name: android-asio (x86_64)
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Install Bazel
uses: bazel-contrib/setup-bazel@0.18.0
with:
bazelisk-cache: true
disk-cache: ${{ github.workflow }}-android-asio
repository-cache: true

- name: Set up Android NDK
run: |
ANDROID_SDK_ROOT="${ANDROID_HOME:-/usr/local/lib/android/sdk}"
# Find the installed NDK version
NDK_VERSION=$(ls "$ANDROID_SDK_ROOT/ndk" | sort -V | tail -1)
echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/$NDK_VERSION" >> "$GITHUB_ENV"

# Cross-compile only the backend-dependent targets with the Boost.Asio
# backend selected; the co backend is covered by the `android` job.
- name: Cross-compile for Android x86_64 (asio backend)
run: |
bazel build \
//server:subspace_server \
//client:client_test \
//client:bridge_test \
//common:split_buffer_test \
//plugins:nop_plugin.so \
//plugins:split_buffer_free_test_plugin.so \
//asio_rpc/test:rpc_test \
//asio_rpc/server:server_test \
//asio_rpc/client:client_test \
--verbose_failures \
--config=android_x86_64 \
--//:coro_backend=asio

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Run asio tests on Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
target: google_apis
arch: x86_64
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
emulator-boot-timeout: 900
script: bash .github/scripts/android-asio-test.sh

- name: Upload Android asio test logs
uses: actions/upload-artifact@v7
if: failure()
with:
name: android-asio-test-logs
path: bazel-testlogs
26 changes: 26 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
package(default_visibility = ["//visibility:public"])

load("@bazel_skylib//rules:common_settings.bzl", "string_flag")

# Build-time selection of the coroutine/networking backend for the server (and
# the client paths it relies on). See common/async/context.h. `co` is the
# default; `asio` compiles the server against Boost.Asio.
#
# bazelisk build --//:coro_backend=asio //server
string_flag(
name = "coro_backend",
build_setting_default = "co",
values = [
"co",
"asio",
],
)

config_setting(
name = "coro_backend_co",
flag_values = {":coro_backend": "co"},
)

config_setting(
name = "coro_backend_asio",
flag_values = {":coro_backend": "asio"},
)

config_setting(
name = "macos_arm64",
constraint_values = [
Expand Down
30 changes: 30 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ FetchContent_MakeAvailable(protobuf)
# Protobuf provides targets like protobuf::libprotobuf and protobuf::protoc
# These targets can be used directly without find_package

# --- Option: coroutine/networking backend for the server ---
# Mirrors the Bazel //:coro_backend flag. "co" (default) builds against the co
# coroutine library; "asio" builds the server against Boost.Asio. The value is
# translated into the SUBSPACE_CORO_BACKEND macro (see common/async/context.h)
# and applied globally so every translation unit that includes the async
# headers sees a consistent backend.
set(SUBSPACE_CORO_BACKEND "co" CACHE STRING "Server coroutine backend: co or asio")
set_property(CACHE SUBSPACE_CORO_BACKEND PROPERTY STRINGS "co" "asio")

if(SUBSPACE_CORO_BACKEND STREQUAL "asio")
add_compile_definitions(SUBSPACE_CORO_BACKEND=2)
message(STATUS "Subspace coroutine backend: asio (Boost.Asio)")

# Boost.Asio is header-only but pulls in several Boost libraries. Fetch a
# Boost release only when the asio backend is requested so the default co
# build does not pay for it.
FetchContent_Declare(
Boost
GIT_REPOSITORY https://github.com/boostorg/boost.git
GIT_TAG boost-1.87.0
CMAKE_ARGS
-DCMAKE_OSX_ARCHITECTURES="${CMAKE_OSX_ARCHITECTURES}"
)
set(BOOST_INCLUDE_LIBRARIES asio coroutine context)
FetchContent_MakeAvailable(Boost)
else()
# co backend leaves SUBSPACE_CORO_BACKEND unset; context.h defaults to _CO.
message(STATUS "Subspace coroutine backend: co")
endif()

# --- Option: Python bindings ---
option(SUBSPACE_PYTHON "Build Python bindings (requires Python3 and pybind11)" OFF)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ See the file docs/subspace.pdf for full documentation. Additional documentation
- [C Client API](docs/c-client.md)
- [Client Architecture](docs/client-architecture.md)
- [Server Architecture](docs/server-architecture.md)
- [Asio Backend and vsock Bridging](docs/asio-backend.md)
- [Rust Client](docs/rust-client.md)
- [Shadow Process (Crash Recovery)](docs/shadow-process.md)
- [Running Subspace on Android](docs/android.md)
Expand Down
30 changes: 29 additions & 1 deletion client/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ cc_library(
include_prefix = "subspace/client",
deps = [
"//common:subspace_common",
"//common/async:async",
"@abseil-cpp//absl/container:flat_hash_map",
"@abseil-cpp//absl/container:flat_hash_set",
"@abseil-cpp//absl/status",
Expand All @@ -42,11 +43,18 @@ cc_library(
hdrs = ["test_fixture.h"],
deps = [
":subspace_client",
"//common/async:async",
"//server",
"@abseil-cpp//absl/status:status_matchers",
"@coroutines//:co",
"@googletest//:gtest",
],
] + select({
"//:coro_backend_asio": [
"@boost.asio//:boost.asio",
"@boost.coroutine//:boost.coroutine",
],
"//conditions:default": [],
}),
)

cc_test(
Expand Down Expand Up @@ -158,6 +166,26 @@ cc_test(
],
)

cc_test(
name = "asio_client_test",
size = "small",
timeout = "moderate",
srcs = ["asio_client_test.cc"],
copts = [
"-Wno-missing-field-initializers",
"-Wno-unused-parameter",
],
deps = [
":subspace_client",
"//common/async:async",
"//server",
"@abseil-cpp//absl/status",
"@abseil-cpp//absl/status:statusor",
"@coroutines//:co",
"@googletest//:gtest_main",
],
)

cc_test(
name = "bridge_test",
srcs = ["bridge_test.cc"],
Expand Down
1 change: 1 addition & 0 deletions client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ set_source_files_properties(arm_crc32.S PROPERTIES
# Link against other internal libraries (e.g., common) or external dependencies if needed:
target_link_libraries(subspace_client PUBLIC
subspace_common
subspace_async
subspace_proto
absl::flags
absl::flat_hash_map
Expand Down
Loading
Loading