mirror of https://github.com/telemt/telemt.git
Merge c8ba307780 into 0c3c9009a9
This commit is contained in:
commit
ddfe1f6462
|
|
@ -0,0 +1,80 @@
|
|||
name: Build telemt for OpenBSD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: openbsd-build-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: OpenBSD ${{ matrix.arch }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
artifact: telemt-openbsd-x86_64
|
||||
rustflags: -C opt-level=3
|
||||
- arch: aarch64
|
||||
artifact: telemt-openbsd-aarch64
|
||||
rustflags: -C opt-level=3
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
clean: true
|
||||
submodules: false
|
||||
|
||||
- name: Compile in OpenBSD VM
|
||||
uses: vmactions/openbsd-vm@3fafb45f2e2e696249c583835939323fe1c3448c # v1
|
||||
with:
|
||||
release: "7.8"
|
||||
arch: ${{ matrix.arch }}
|
||||
usesh: true
|
||||
sync: sshfs
|
||||
envs: RUSTFLAGS CARGO_TERM_COLOR
|
||||
prepare: |
|
||||
pkg_add rust
|
||||
run: |
|
||||
set -e
|
||||
required_major=1
|
||||
required_minor=85
|
||||
rustc_version="$(rustc --version | awk '{print $2}')"
|
||||
rustc_major="${rustc_version%%.*}"
|
||||
rustc_rest="${rustc_version#*.}"
|
||||
rustc_minor="${rustc_rest%%.*}"
|
||||
if [ "$rustc_major" -lt "$required_major" ] || { [ "$rustc_major" -eq "$required_major" ] && [ "$rustc_minor" -lt "$required_minor" ]; }; then
|
||||
echo "Installed rustc version ($rustc_version) is older than the required minimum ${required_major}.${required_minor}.x for this project (e.g., edition = \"2024\")."
|
||||
echo "Please update the OpenBSD rust package or adjust the workflow to use a newer toolchain."
|
||||
exit 1
|
||||
fi
|
||||
cargo build --release --locked --verbose
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
|
||||
- name: Verify artifact exists
|
||||
run: test -f target/release/telemt
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: target/release/telemt
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
|
@ -206,6 +206,67 @@ jobs:
|
|||
dist/${{ matrix.asset }}.tar.gz
|
||||
dist/${{ matrix.asset }}.sha256
|
||||
|
||||
# ==========================
|
||||
# OpenBSD
|
||||
# ==========================
|
||||
build-openbsd:
|
||||
name: OpenBSD ${{ matrix.arch }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
asset: telemt-x86_64-openbsd
|
||||
rustflags: -C opt-level=3
|
||||
- arch: aarch64
|
||||
asset: telemt-aarch64-openbsd
|
||||
rustflags: -C opt-level=3
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build in OpenBSD VM
|
||||
uses: vmactions/openbsd-vm@v1
|
||||
with:
|
||||
release: "7.8"
|
||||
arch: ${{ matrix.arch }}
|
||||
usesh: true
|
||||
sync: sshfs
|
||||
envs: RUSTFLAGS CARGO_TERM_COLOR
|
||||
prepare: |
|
||||
pkg_add rust
|
||||
run: |
|
||||
set -e
|
||||
RUSTC_VERSION=$(rustc --version | awk '{print $2}')
|
||||
RUSTC_MAJOR=$(echo "$RUSTC_VERSION" | cut -d. -f1)
|
||||
RUSTC_MINOR=$(echo "$RUSTC_VERSION" | cut -d. -f2)
|
||||
REQUIRED_MAJOR=1
|
||||
REQUIRED_MINOR=85
|
||||
if [ "$RUSTC_MAJOR" -lt "$REQUIRED_MAJOR" ] || { [ "$RUSTC_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$RUSTC_MINOR" -lt "$REQUIRED_MINOR" ]; }; then
|
||||
echo "rustc ${REQUIRED_MAJOR}.${REQUIRED_MINOR}.0 or newer is required for this project (found ${RUSTC_VERSION})."
|
||||
exit 1
|
||||
fi
|
||||
cargo build --release --locked --verbose
|
||||
env:
|
||||
RUSTFLAGS: ${{ matrix.rustflags }}
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cp target/release/${{ env.BINARY_NAME }} dist/${{ env.BINARY_NAME }}-${{ matrix.arch }}-unknown-openbsd
|
||||
cd dist
|
||||
tar -czf ${{ matrix.asset }}.tar.gz ${{ env.BINARY_NAME }}-${{ matrix.arch }}-unknown-openbsd
|
||||
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.asset }}
|
||||
path: |
|
||||
dist/${{ matrix.asset }}.tar.gz
|
||||
dist/${{ matrix.asset }}.sha256
|
||||
|
||||
# ==========================
|
||||
# Docker
|
||||
# ==========================
|
||||
|
|
@ -261,7 +322,7 @@ jobs:
|
|||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-gnu, build-musl]
|
||||
needs: [build-gnu, build-musl, build-openbsd]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Compile (no tests)
|
||||
run: cargo check --workspace --all-features --lib --bins --verbose
|
||||
run: cargo check --workspace --all-features --bins --verbose
|
||||
|
||||
- name: Run tests (single pass)
|
||||
run: cargo test --workspace --all-features --verbose
|
||||
|
|
|
|||
|
|
@ -293,6 +293,8 @@ const QUOTA_CONTENTION_RETRY_MAX_INTERVAL: Duration = Duration::from_millis(64);
|
|||
#[cfg(test)]
|
||||
static QUOTA_RETRY_SLEEP_ALLOCS: AtomicU64 = AtomicU64::new(0);
|
||||
#[cfg(test)]
|
||||
static QUOTA_RETRY_SLEEP_ALLOCS_BY_USER: OnceLock<DashMap<String, u64>> = OnceLock::new();
|
||||
#[cfg(test)]
|
||||
static QUOTA_USER_LOCK_EVICTOR_SPAWN_COUNT: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -300,11 +302,23 @@ pub(crate) fn reset_quota_retry_sleep_allocs_for_tests() {
|
|||
QUOTA_RETRY_SLEEP_ALLOCS.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn reset_quota_retry_sleep_allocs_for_user_for_tests(user: &str) {
|
||||
let map = QUOTA_RETRY_SLEEP_ALLOCS_BY_USER.get_or_init(DashMap::new);
|
||||
map.remove(user);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn quota_retry_sleep_allocs_for_tests() -> u64 {
|
||||
QUOTA_RETRY_SLEEP_ALLOCS.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn quota_retry_sleep_allocs_for_user_for_tests(user: &str) -> u64 {
|
||||
let map = QUOTA_RETRY_SLEEP_ALLOCS_BY_USER.get_or_init(DashMap::new);
|
||||
map.get(user).map(|v| *v.value()).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn quota_contention_retry_delay(retry_attempt: u8) -> Duration {
|
||||
let shift = u32::from(retry_attempt.min(5));
|
||||
|
|
@ -329,12 +343,22 @@ fn poll_quota_retry_sleep(
|
|||
sleep_slot: &mut Option<Pin<Box<Sleep>>>,
|
||||
wake_scheduled: &mut bool,
|
||||
retry_attempt: &mut u8,
|
||||
user: &str,
|
||||
cx: &mut Context<'_>,
|
||||
) {
|
||||
#[cfg(not(test))]
|
||||
let _ = user;
|
||||
|
||||
if !*wake_scheduled {
|
||||
*wake_scheduled = true;
|
||||
#[cfg(test)]
|
||||
{
|
||||
QUOTA_RETRY_SLEEP_ALLOCS.fetch_add(1, Ordering::Relaxed);
|
||||
let map = QUOTA_RETRY_SLEEP_ALLOCS_BY_USER.get_or_init(DashMap::new);
|
||||
map.entry(user.to_string())
|
||||
.and_modify(|count| *count = count.saturating_add(1))
|
||||
.or_insert(1);
|
||||
}
|
||||
*sleep_slot = Some(Box::pin(tokio::time::sleep(quota_contention_retry_delay(
|
||||
*retry_attempt,
|
||||
))));
|
||||
|
|
@ -465,6 +489,7 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
|||
&mut this.quota_read_retry_sleep,
|
||||
&mut this.quota_read_wake_scheduled,
|
||||
&mut this.quota_read_retry_attempt,
|
||||
&this.user,
|
||||
cx,
|
||||
);
|
||||
return Poll::Pending;
|
||||
|
|
@ -482,6 +507,7 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
|||
&mut this.quota_read_retry_sleep,
|
||||
&mut this.quota_read_wake_scheduled,
|
||||
&mut this.quota_read_retry_attempt,
|
||||
&this.user,
|
||||
cx,
|
||||
);
|
||||
return Poll::Pending;
|
||||
|
|
@ -570,6 +596,7 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
|||
&mut this.quota_write_retry_sleep,
|
||||
&mut this.quota_write_wake_scheduled,
|
||||
&mut this.quota_write_retry_attempt,
|
||||
&this.user,
|
||||
cx,
|
||||
);
|
||||
return Poll::Pending;
|
||||
|
|
@ -587,6 +614,7 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
|||
&mut this.quota_write_retry_sleep,
|
||||
&mut this.quota_write_wake_scheduled,
|
||||
&mut this.quota_write_retry_attempt,
|
||||
&this.user,
|
||||
cx,
|
||||
);
|
||||
return Poll::Pending;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ async fn tdd_single_pending_timer_does_not_allocate_on_each_repoll() {
|
|||
.try_lock()
|
||||
.expect("test must hold local lock to force retry scheduling");
|
||||
|
||||
reset_quota_retry_sleep_allocs_for_tests();
|
||||
reset_quota_retry_sleep_allocs_for_user_for_tests(&user);
|
||||
|
||||
let mut io = StatsIo::new(
|
||||
tokio::io::sink(),
|
||||
|
|
@ -65,12 +65,12 @@ async fn tdd_single_pending_timer_does_not_allocate_on_each_repoll() {
|
|||
|
||||
let first = Pin::new(&mut io).poll_write(&mut cx, &[0xA1]);
|
||||
assert!(first.is_pending());
|
||||
let allocs_after_first = quota_retry_sleep_allocs_for_tests();
|
||||
let allocs_after_first = quota_retry_sleep_allocs_for_user_for_tests(&io.user);
|
||||
let ptr_after_first = sleep_slot_ptr(&io.quota_write_retry_sleep);
|
||||
|
||||
let second = Pin::new(&mut io).poll_write(&mut cx, &[0xA2]);
|
||||
assert!(second.is_pending());
|
||||
let allocs_after_second = quota_retry_sleep_allocs_for_tests();
|
||||
let allocs_after_second = quota_retry_sleep_allocs_for_user_for_tests(&io.user);
|
||||
let ptr_after_second = sleep_slot_ptr(&io.quota_write_retry_sleep);
|
||||
|
||||
assert_eq!(allocs_after_first, 1, "first pending poll must allocate one timer");
|
||||
|
|
@ -96,7 +96,7 @@ async fn tdd_retry_cycle_allocates_once_per_fired_timer_cycle_not_per_poll() {
|
|||
.try_lock()
|
||||
.expect("test must hold local lock to keep write path pending");
|
||||
|
||||
reset_quota_retry_sleep_allocs_for_tests();
|
||||
reset_quota_retry_sleep_allocs_for_user_for_tests(&user);
|
||||
|
||||
let mut io = StatsIo::new(
|
||||
tokio::io::sink(),
|
||||
|
|
@ -126,7 +126,7 @@ async fn tdd_retry_cycle_allocates_once_per_fired_timer_cycle_not_per_poll() {
|
|||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
|
||||
let allocs = quota_retry_sleep_allocs_for_tests();
|
||||
let allocs = quota_retry_sleep_allocs_for_user_for_tests(&io.user);
|
||||
assert!(allocs >= 2, "multiple fired cycles should allocate multiple timers");
|
||||
assert!(
|
||||
allocs < polls,
|
||||
|
|
@ -146,7 +146,7 @@ async fn adversarial_backoff_latency_envelope_stays_bounded_under_contention() {
|
|||
.try_lock()
|
||||
.expect("test must hold local lock for sustained contention");
|
||||
|
||||
reset_quota_retry_sleep_allocs_for_tests();
|
||||
reset_quota_retry_sleep_allocs_for_user_for_tests(&user);
|
||||
|
||||
let mut io = StatsIo::new(
|
||||
tokio::io::sink(),
|
||||
|
|
@ -191,7 +191,7 @@ async fn adversarial_backoff_latency_envelope_stays_bounded_under_contention() {
|
|||
"retry wake gap must remain bounded in test profile; observed max gap={max_gap:?}"
|
||||
);
|
||||
assert!(
|
||||
quota_retry_sleep_allocs_for_tests() <= 16,
|
||||
quota_retry_sleep_allocs_for_user_for_tests(&io.user) <= 16,
|
||||
"allocation cycles must remain bounded during a short contention window"
|
||||
);
|
||||
|
||||
|
|
@ -247,3 +247,69 @@ async fn micro_benchmark_release_to_completion_latency_stays_bounded() {
|
|||
"contention release->completion p95 must stay bounded; p95_ms={p95_ms}, samples={samples_ms:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn adversarial_per_user_retry_allocation_counter_isolation_under_parallel_contention() {
|
||||
let _guard = quota_test_guard();
|
||||
|
||||
let user_a = format!("retry-alloc-isolation-a-{}", std::process::id());
|
||||
let user_b = format!("retry-alloc-isolation-b-{}", std::process::id());
|
||||
|
||||
let lock_a = quota_user_lock(&user_a);
|
||||
let lock_b = quota_user_lock(&user_b);
|
||||
let held_guard_a = lock_a
|
||||
.try_lock()
|
||||
.expect("test must hold lock A to force pending scheduling");
|
||||
let held_guard_b = lock_b
|
||||
.try_lock()
|
||||
.expect("test must hold lock B to force pending scheduling");
|
||||
|
||||
reset_quota_retry_sleep_allocs_for_tests();
|
||||
reset_quota_retry_sleep_allocs_for_user_for_tests(&user_a);
|
||||
reset_quota_retry_sleep_allocs_for_user_for_tests(&user_b);
|
||||
|
||||
let mut io_a = StatsIo::new(
|
||||
tokio::io::sink(),
|
||||
Arc::new(SharedCounters::new()),
|
||||
Arc::new(Stats::new()),
|
||||
user_a.clone(),
|
||||
Some(2048),
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
Instant::now(),
|
||||
);
|
||||
let mut io_b = StatsIo::new(
|
||||
tokio::io::sink(),
|
||||
Arc::new(SharedCounters::new()),
|
||||
Arc::new(Stats::new()),
|
||||
user_b.clone(),
|
||||
Some(2048),
|
||||
Arc::new(AtomicBool::new(false)),
|
||||
Instant::now(),
|
||||
);
|
||||
|
||||
let (_wake_counter_a, mut cx_a) = build_context();
|
||||
let (_wake_counter_b, mut cx_b) = build_context();
|
||||
|
||||
let first_a = Pin::new(&mut io_a).poll_write(&mut cx_a, &[0xE1]);
|
||||
let first_b = Pin::new(&mut io_b).poll_write(&mut cx_b, &[0xE2]);
|
||||
assert!(first_a.is_pending());
|
||||
assert!(first_b.is_pending());
|
||||
|
||||
assert_eq!(
|
||||
quota_retry_sleep_allocs_for_user_for_tests(&user_a),
|
||||
1,
|
||||
"user A scoped counter must reflect only user A allocations"
|
||||
);
|
||||
assert_eq!(
|
||||
quota_retry_sleep_allocs_for_user_for_tests(&user_b),
|
||||
1,
|
||||
"user B scoped counter must reflect only user B allocations"
|
||||
);
|
||||
assert!(
|
||||
quota_retry_sleep_allocs_for_tests() >= 2,
|
||||
"global counter remains aggregate and should include both users"
|
||||
);
|
||||
|
||||
drop(held_guard_a);
|
||||
drop(held_guard_b);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue