chore(test): enforce complete local test barrier
Deny workspace warnings and lint suppressions, consolidate Rust and Python coverage under cargo xtask test with 60-second per-test limits, and remove stale flaky, stateful, Docker, and orphaned test artifacts.
This commit is contained in:
parent
2027e61d84
commit
1c8d6ab912
53 changed files with 683 additions and 3060 deletions
|
|
@ -3,4 +3,4 @@ rustc-workspace-wrapper = "tools/actor-control-flow-lint/rustc-wrapper.py"
|
||||||
|
|
||||||
[alias]
|
[alias]
|
||||||
xtask = "run --package xtask --"
|
xtask = "run --package xtask --"
|
||||||
myelin-chat = "run --package xtask -- myelin-chat"
|
lint = "clippy --workspace --all-targets --all-features"
|
||||||
|
|
|
||||||
2
.config/nextest.toml
Normal file
2
.config/nextest.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[profile.default]
|
||||||
|
slow-timeout = { period = "60s", terminate-after = 1 }
|
||||||
119
.github/workflows/myelin-properties.yml
vendored
119
.github/workflows/myelin-properties.yml
vendored
|
|
@ -1,119 +0,0 @@
|
||||||
name: Myelin properties
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
schedule:
|
|
||||||
- cron: "17 3 * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
component-properties:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install pinned Rust toolchain
|
|
||||||
run: rustup toolchain install nightly-2026-02-07 --profile minimal --component rustfmt --component rustc-dev --component rust-src --component llvm-tools-preview
|
|
||||||
- name: Check formatting
|
|
||||||
run: cargo fmt --all -- --check
|
|
||||||
- name: Check default workspace members
|
|
||||||
run: cargo check
|
|
||||||
- name: Check compiler-policy contracts
|
|
||||||
run: cargo test -p actor-control-flow-lint-tests --test contracts compiler_policy_contracts -- --exact
|
|
||||||
- name: Engine scheduling properties
|
|
||||||
run: |
|
|
||||||
cargo test -p swactor-engine --test engine_unit generated_actor_timers_and_completion_are_bounded -- --exact
|
|
||||||
cargo test -p swactor-engine --test engine_unit lifecycle_invariant_detects_injected_duplicate_completion -- --exact
|
|
||||||
cargo test -p swactor-engine --test engine_unit lifecycle_invariant_detects_injected_uncancelled_periodic_timer -- --exact
|
|
||||||
- name: Process adapter properties
|
|
||||||
run: |
|
|
||||||
cargo test -p swactor-process --lib operations::properties::generated_stream_observations_close_once_and_stay_closed -- --exact
|
|
||||||
cargo test -p swactor-process --lib operations::properties::generated_lifecycle_actions_make_stop_idempotent_and_exit_terminal -- --exact
|
|
||||||
cargo test -p swactor-process --lib operations::properties::generated_stop_notifications_are_delivered_at_most_once -- --exact
|
|
||||||
cargo test -p swactor-process --lib operations::properties::generated_stdin_commands_and_eof_notify_once -- --exact
|
|
||||||
cargo test -p swactor-process --lib operations::properties::property_invariants_reject_controlled_defects -- --exact
|
|
||||||
cargo test -p swactor-process --lib operations::properties::trivial_real_child_exit_has_a_hard_timeout -- --exact
|
|
||||||
- name: Myelin component properties (exclude job and reconciler tests)
|
|
||||||
run: |
|
|
||||||
cargo test -p myelin --lib orchestration::manual_control::tests::aggressive_random_event_stream_preserves_control_invariants -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::manual_control::tests::rental_free_end_to_end_sequences_converge -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::manual_control::tests::manual_actor_generated_public_actions_and_callbacks_are_bounded -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::manual_control::tests::fixed_helper_cardinality_invariant_detects_controlled_extra_spawn -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::manual_control::tests::callback_panic_reports_typed_failure_without_poisoning_work_actor -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::manual_control::tests::callback_panic_invariant_detects_controlled_unguarded_panic -- --exact
|
|
||||||
cargo test -p myelin --lib provisioning::tests::mock_vastai_handle_state_survives_random_create_and_stop_sequences -- --exact
|
|
||||||
cargo test -p myelin --lib provisioning::tests::docker_generated_attempt_lifecycles_are_idempotent_and_bounded -- --exact
|
|
||||||
cargo test -p myelin --lib provisioning::tests::docker_duplicate_resource_detector_rejects_controlled_fault -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::offer_status_classes_are_offers_or_typed_rejections -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::malformed_offer_bodies_are_typed_rejections -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::wrong_or_missing_offer_fields_are_typed_rejections -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::duplicate_offer_records_remain_explicit_values -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::provider_monitor_preserves_contract_identity_and_cardinality -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::provider_monitor_terminal_polling_stops_after_one_typed_outcome -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::provider_monitor_poll_stop_orderings_cease_polling -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::duplicate_terminal_detector_rejects_controlled_fault -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::ssh_bootstrap_output_lines_preserve_stream_and_protocol -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::ssh_bootstrap_malformed_protocol_is_data_not_poison -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::ssh_bootstrap_eof_orderings_stop_relay_and_actor -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::ssh_bootstrap_child_failures_have_typed_attempt_outcomes -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::ssh_bootstrap_timeout_is_typed_and_stops_polling -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::provider_adapters::vastai::tests::ssh_bootstrap_stop_orderings_emit_one_terminal_and_stop_all_actors -- --exact
|
|
||||||
cargo test -p myelin --lib node::worker_node_runtime::control_flow_properties::runtime_actors_generated_transitions_complete_once_on_one_worker -- --exact
|
|
||||||
cargo test -p myelin --lib node::worker_node_runtime::control_flow_properties::runtime_invariant_checker_rejects_duplicate_readiness_publication -- --exact
|
|
||||||
cargo test -p myelin --lib node::worker_node_runtime::control_flow_properties::helper_wait_generated_terminal_sequences_complete_once_on_one_worker -- --exact
|
|
||||||
cargo test -p myelin --lib node::worker_node_runtime::control_flow_properties::helper_invariant_checker_rejects_expected_output_after_terminal_error -- --exact
|
|
||||||
cargo test -p myelin --lib node::worker_node_runtime::control_flow_properties::stage_fetch_generated_observations_complete_once_on_one_worker -- --exact
|
|
||||||
cargo test -p myelin --lib node::worker_node_runtime::control_flow_properties::stage_invariant_checker_rejects_wrong_terminal_classification -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::app::serve_cluster_properties::serve_cluster_production_transitions_converge_once_without_growth -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::app::serve_cluster_properties::serve_cluster_lifecycle_invariants_reject_injected_duplicate_and_growth -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::control::properties::generated_http_bridge_sequences_terminate_without_control_actor_growth -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::control::properties::generated_duplicate_control_replies_deliver_first_once_and_remove_observer -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::control::properties::reply_observer_disappearance_returns_a_bounded_terminal_http_response -- --exact
|
|
||||||
cargo test -p myelin --lib orchestration::control::properties::http_bridge_invariant_rejects_a_controlled_duplicate_forward -- --exact
|
|
||||||
- name: Controlled E2E oracle contract
|
|
||||||
run: cargo test -p myelin --features test-support --test stateful_vastai e2e_oracle_rejects_controlled_lifecycle_faults -- --exact
|
|
||||||
- name: Dashboard bridge properties
|
|
||||||
run: |
|
|
||||||
cargo test -p dashboard --features demo-control --lib control::properties::generated_concurrent_bridge_commands_forward_once_and_shutdown -- --exact
|
|
||||||
cargo test -p dashboard --features demo-control --lib control::properties::bridge_invariant_rejects_a_controlled_duplicate_delivery -- --exact
|
|
||||||
cargo test -p dashboard --features demo-control --lib server::tests::generated_control_http_sequences_are_bounded_and_typed -- --exact
|
|
||||||
cargo test -p dashboard --features demo-control --lib server::tests::control_http_invariant_rejects_a_controlled_server_error -- --exact
|
|
||||||
- name: Demo actor properties
|
|
||||||
run: |
|
|
||||||
cargo test -p xtask --bin xtask demo::control::properties::generated_control_commands_forward_only_after_supervisor_registration -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::control::properties::control_transition_oracle_rejects_duplicate_forwarding -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::feed::properties::generated_supervisor_transitions_are_once_only_nonblocking_and_clean -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::feed::properties::supervisor_transition_oracle_rejects_duplicate_identity_resources -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::node::properties::generated_node_runtime_transitions_emit_heartbeats_and_stop_once -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::node::properties::node_transition_oracle_rejects_duplicate_resources -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::provider::properties::generated_process_reports_complete_exit_watchers_once_and_preserve_last_state -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::provider::properties::process_relay_oracle_rejects_lost_exit -- --exact
|
|
||||||
cargo test -p xtask --bin xtask demo::properties::direct_binary_signal_smoke_has_a_hard_timeout -- --exact
|
|
||||||
|
|
||||||
process-e2e:
|
|
||||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
||||||
needs: component-properties
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 30
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install pinned Rust toolchain
|
|
||||||
run: rustup toolchain install nightly-2026-02-07 --profile minimal --component rustc-dev --component rust-src --component llvm-tools-preview
|
|
||||||
- name: Stateful VastAI process E2E
|
|
||||||
env:
|
|
||||||
PROPTEST_CASES: "4"
|
|
||||||
run: |
|
|
||||||
set -o pipefail
|
|
||||||
mkdir -p artifacts
|
|
||||||
cargo test -p myelin --features test-support --test stateful_vastai stateful_vastai_dashboard_control_survives_restarts -- --ignored --exact --nocapture 2>&1 | tee artifacts/stateful-vastai.log
|
|
||||||
- name: Preserve E2E failure artifacts
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: stateful-vastai-failure-${{ github.run_id }}
|
|
||||||
path: |
|
|
||||||
artifacts/stateful-vastai.log
|
|
||||||
apps/myelin/proptest-regressions/tests/e2e_vastai.txt
|
|
||||||
if-no-files-found: warn
|
|
||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -962,7 +962,9 @@ dependencies = [
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"libc",
|
"libc",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"proptest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"swactor",
|
"swactor",
|
||||||
"swactor-engine",
|
"swactor-engine",
|
||||||
|
|
@ -4335,9 +4337,11 @@ dependencies = [
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"loom",
|
"loom",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"proc-macro2",
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-state-machine",
|
"proptest-state-machine",
|
||||||
"serde",
|
"serde",
|
||||||
|
"syn",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time 0.2.4",
|
"web-time 0.2.4",
|
||||||
]
|
]
|
||||||
|
|
@ -4489,11 +4493,13 @@ name = "telemetry"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"futures-channel",
|
||||||
"iroh",
|
"iroh",
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"swactor",
|
"swactor",
|
||||||
|
"swactor-engine",
|
||||||
"swactor-transport",
|
"swactor-transport",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
11
Cargo.toml
11
Cargo.toml
|
|
@ -75,4 +75,15 @@ criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
proptest-state-machine = "0.3"
|
proptest-state-machine = "0.3"
|
||||||
loom = "0.7"
|
loom = "0.7"
|
||||||
|
proc-macro2 = "1"
|
||||||
|
syn = { version = "2", features = ["full", "visit"] }
|
||||||
|
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
warnings = "deny"
|
||||||
|
|
||||||
|
[workspace.lints.clippy]
|
||||||
|
allow_attributes = "forbid"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
BASE_IMAGE=${BASE_IMAGE:-myelin-node-base:cuda12.6}
|
|
||||||
IMAGE=${IMAGE:-myelin-node:latest}
|
|
||||||
CONTAINER=${CONTAINER:-myelin-node-e2e-$$}
|
|
||||||
GPUS=${MYELIN_CUDA_GPUS:-all}
|
|
||||||
PROMPT=${MYELIN_NODE_SELF_TEST_PROMPT:-ping}
|
|
||||||
TIMEOUT_SECS=${MYELIN_NODE_E2E_TIMEOUT_SECS:-1800}
|
|
||||||
FRAME_LOG=${MYELIN_TELEMETRY_FRAME_LOG:-/var/log/myelin-telemetry.ndjson}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
cargo build --release -p myelin --bin myelin-worker
|
|
||||||
docker build -f apps/myelin/node-image/Dockerfile.base -t "$BASE_IMAGE" .
|
|
||||||
docker build -f apps/myelin/node-image/Dockerfile --build-arg BASE_IMAGE="$BASE_IMAGE" -t "$IMAGE" .
|
|
||||||
|
|
||||||
docker run -d \
|
|
||||||
--name "$CONTAINER" \
|
|
||||||
--gpus "$GPUS" \
|
|
||||||
-e MYELIN_NODE_SELF_TEST_PROMPT="$PROMPT" \
|
|
||||||
-e MYELIN_NODE_MAX_RUNTIME_SECS=1 \
|
|
||||||
-e MYELIN_SELF_TEST_MAX_TOKENS="${MYELIN_SELF_TEST_MAX_TOKENS:-1}" \
|
|
||||||
-e MYELIN_MODEL_CACHE_DIR=/var/cache/myelin-models \
|
|
||||||
-e MYELIN_TELEMETRY_FRAME_LOG="$FRAME_LOG" \
|
|
||||||
${HF_TOKEN:+-e HF_TOKEN="$HF_TOKEN"} \
|
|
||||||
"$IMAGE" >/dev/null
|
|
||||||
|
|
||||||
deadline=$((SECONDS + TIMEOUT_SECS))
|
|
||||||
while (( SECONDS < deadline )); do
|
|
||||||
logs=$(docker logs "$CONTAINER" 2>&1 || true)
|
|
||||||
if grep -q '"type":"ready"' <<<"$logs" && grep -q '"type":"self_test_completed"' <<<"$logs"; then
|
|
||||||
frames=$(docker exec "$CONTAINER" cat "$FRAME_LOG" 2>/dev/null || true)
|
|
||||||
if grep -q '"channel":"myelin.node.ready"' <<<"$frames" &&
|
|
||||||
grep -q '"channel":"myelin.worker.weights"' <<<"$frames" &&
|
|
||||||
grep -q '"channel":"myelin.worker.prompt"' <<<"$frames"; then
|
|
||||||
printf '%s\n' "$logs"
|
|
||||||
printf '%s\n' "$frames"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if grep -q 'WorkerFatal\|myelin-node: .*failed\|ModelLoadFailed\|GgufDownloadFailed' <<<"$logs"; then
|
|
||||||
printf '%s\n' "$logs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
|
|
||||||
docker logs "$CONTAINER" 2>&1 || true
|
|
||||||
echo "myelin-node Docker E2E timed out after ${TIMEOUT_SECS}s" >&2
|
|
||||||
exit 1
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# Seeds for stateful VastAI E2E failures. Proptest replays these before generated cases.
|
|
||||||
cc 26c3cd5944cb25de70a984389373e7380a0c6e6ba1c8f3e6eff5a0f78dbef168 # current-strategy replay; E2eCase is derived from e2e_case()
|
|
||||||
cc 5efb0a6c5c348475c859a2c4267d5177aaecd5adefcc5e8b7fc2e6d728deecd1 # shrinks to case = E2eCase { seed: 7099259626237328177, node_seed: 245, kill_mask: 24, offer_offset: 3, actions: [ConcurrentQueries, Search { count: 2 }, Restart { mode: FlushSafeAbrupt }, Query, Kill { node_slot: 218, command_slot: 5 }, Query, Provision { command_slot: 5, use_searched_offers: true }, EndpointProbe { node_slot: 217 }, Restart { mode: Graceful }, Flush, Kill { node_slot: 217, command_slot: 5 }] }
|
|
||||||
cc 6d005687858520ed65af20ec5b2de056efcda15771b6420d7f67e3cb6c246d5b # shrinks to case = E2eCase { seed: 17649392557414661864, node_seed: 249, kill_mask: 159, offer_offset: 5, actions: [Kill { node_slot: 88, command_slot: 242 }, Flush, ConcurrentQueries, EndpointProbe { node_slot: 88 }, Query, Search { count: 2 }, Provision { command_slot: 242, use_searched_offers: true }, Restart { mode: FlushSafeAbrupt }, Kill { node_slot: 89, command_slot: 242 }, Query, Restart { mode: Graceful }] }
|
|
||||||
cc 2e36f51e7cb347b571ac838f81638748f8414a32ec91cb9f4b2730363d72be61 # shrinks to case = E2eCase { seed: 13301618846512983428, node_seed: 99, kill_mask: 30, offer_offset: 2, actions: [Query, Restart { mode: FlushSafeAbrupt }, Kill { node_slot: 136, command_slot: 187 }, ConcurrentQueries, Restart { mode: Graceful }, Query, EndpointProbe { node_slot: 136 }, Kill { node_slot: 137, command_slot: 187 }, Provision { command_slot: 187, use_searched_offers: false }, Flush, Search { count: 0 }] }
|
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Tinygrad-backed device bridge probe for Rust Myelin bridge tests.
|
|
||||||
|
|
||||||
Line-delimited JSON control only. Payload bytes live in the arena file whose
|
|
||||||
path Rust passes during initialize.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
try:
|
|
||||||
from tinygrad import Tensor # type: ignore
|
|
||||||
except Exception as exc: # pragma: no cover - exercised from Rust process tests
|
|
||||||
print(
|
|
||||||
json.dumps(
|
|
||||||
{
|
|
||||||
"type": "worker_fatal",
|
|
||||||
"reason": "tinygrad_unavailable",
|
|
||||||
"message": str(exc),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
flush=True,
|
|
||||||
)
|
|
||||||
raise SystemExit(2)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DeviceObject:
|
|
||||||
dtype: str
|
|
||||||
shape: str
|
|
||||||
extent: int
|
|
||||||
values: list[int]
|
|
||||||
tensor: Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PendingCopy:
|
|
||||||
kind: str
|
|
||||||
copy_id: int
|
|
||||||
handle_id: int
|
|
||||||
host_offset: int
|
|
||||||
device_offset: int
|
|
||||||
length: int
|
|
||||||
|
|
||||||
|
|
||||||
arena_path: str | None = None
|
|
||||||
arena_bytes = 0
|
|
||||||
generation = 0
|
|
||||||
objects: dict[int, DeviceObject] = {}
|
|
||||||
pending_copies: dict[int, PendingCopy] = {}
|
|
||||||
fail_next: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def emit(obj: dict[str, Any]) -> None:
|
|
||||||
print(json.dumps(obj, separators=(",", ":")), flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def backend_error(reason: str) -> None:
|
|
||||||
emit({"type": "backend_error", "reason": reason})
|
|
||||||
|
|
||||||
|
|
||||||
def fatal(reason: str, message: str) -> None:
|
|
||||||
emit({"type": "worker_fatal", "reason": reason, "message": message})
|
|
||||||
|
|
||||||
|
|
||||||
def require_arena() -> str:
|
|
||||||
if arena_path is None:
|
|
||||||
raise RuntimeError("arena is not initialized")
|
|
||||||
return arena_path
|
|
||||||
|
|
||||||
|
|
||||||
def consume_failure(reason: str) -> bool:
|
|
||||||
global fail_next
|
|
||||||
if fail_next == reason:
|
|
||||||
fail_next = None
|
|
||||||
backend_error(reason)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_u32_range(offset: int, length: int) -> None:
|
|
||||||
if offset < 0 or length < 0 or offset % 4 != 0 or length % 4 != 0:
|
|
||||||
raise ValueError("u32 ranges must be non-negative and 4-byte aligned")
|
|
||||||
|
|
||||||
|
|
||||||
def tensor_from_values(values: list[int]) -> Any:
|
|
||||||
try:
|
|
||||||
return Tensor(values, dtype="uint32").realize()
|
|
||||||
except Exception:
|
|
||||||
return Tensor(values, dtype="int32").realize()
|
|
||||||
|
|
||||||
|
|
||||||
def allocate_tensor(dtype: str, extent: int) -> tuple[list[int], Any]:
|
|
||||||
if dtype == "u32":
|
|
||||||
if extent % 4 != 0:
|
|
||||||
raise ValueError("u32 extent must be 4-byte aligned")
|
|
||||||
values = [0] * (extent // 4)
|
|
||||||
return values, tensor_from_values(values)
|
|
||||||
if dtype == "f16":
|
|
||||||
values = [0] * (extent // 2)
|
|
||||||
return values, Tensor(values, dtype="float16").realize()
|
|
||||||
raise ValueError(f"unsupported dtype {dtype!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def update_tensor(obj: DeviceObject) -> None:
|
|
||||||
if obj.dtype == "u32":
|
|
||||||
obj.tensor = tensor_from_values(obj.values)
|
|
||||||
elif obj.dtype == "f16":
|
|
||||||
obj.tensor = Tensor(obj.values, dtype="float16").realize()
|
|
||||||
else:
|
|
||||||
raise ValueError(f"unsupported dtype {obj.dtype!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def materialized_values(obj: DeviceObject) -> list[int]:
|
|
||||||
return [int(v) for v in obj.tensor.tolist()]
|
|
||||||
|
|
||||||
|
|
||||||
def read_arena(offset: int, length: int) -> bytes:
|
|
||||||
path = require_arena()
|
|
||||||
with open(path, "rb", buffering=0) as f:
|
|
||||||
f.seek(offset)
|
|
||||||
data = f.read(length)
|
|
||||||
if len(data) != length:
|
|
||||||
raise EOFError("short arena read")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def write_arena(offset: int, data: bytes) -> None:
|
|
||||||
path = require_arena()
|
|
||||||
with open(path, "r+b", buffering=0) as f:
|
|
||||||
f.seek(offset)
|
|
||||||
f.write(data)
|
|
||||||
f.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def perform_host_to_device(copy: PendingCopy) -> None:
|
|
||||||
obj = objects[copy.handle_id]
|
|
||||||
if obj.dtype != "u32":
|
|
||||||
raise ValueError("payload copy is implemented for u32 test objects only")
|
|
||||||
check_u32_range(copy.device_offset, copy.length)
|
|
||||||
payload = read_arena(copy.host_offset, copy.length)
|
|
||||||
words = list(struct.unpack("<" + "I" * (copy.length // 4), payload))
|
|
||||||
start = copy.device_offset // 4
|
|
||||||
end = start + len(words)
|
|
||||||
if end > len(obj.values):
|
|
||||||
raise ValueError("device range out of bounds")
|
|
||||||
obj.values[start:end] = words
|
|
||||||
update_tensor(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def perform_device_to_host(copy: PendingCopy) -> None:
|
|
||||||
obj = objects[copy.handle_id]
|
|
||||||
if obj.dtype != "u32":
|
|
||||||
raise ValueError("payload copy is implemented for u32 test objects only")
|
|
||||||
check_u32_range(copy.device_offset, copy.length)
|
|
||||||
values = materialized_values(obj)
|
|
||||||
start = copy.device_offset // 4
|
|
||||||
end = start + (copy.length // 4)
|
|
||||||
if end > len(values):
|
|
||||||
raise ValueError("device range out of bounds")
|
|
||||||
payload = struct.pack("<" + "I" * (end - start), *values[start:end])
|
|
||||||
write_arena(copy.host_offset, payload)
|
|
||||||
|
|
||||||
|
|
||||||
def perform_copy(copy: PendingCopy) -> None:
|
|
||||||
if copy.kind == "host_to_device":
|
|
||||||
perform_host_to_device(copy)
|
|
||||||
elif copy.kind == "device_to_host":
|
|
||||||
perform_device_to_host(copy)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"unknown copy kind {copy.kind!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def handle(req: dict[str, Any]) -> bool:
|
|
||||||
global arena_path, arena_bytes, generation, fail_next
|
|
||||||
|
|
||||||
typ = req.get("type")
|
|
||||||
if typ == "initialize":
|
|
||||||
arena_path = str(req["arena_path"])
|
|
||||||
arena_bytes = int(req["arena_bytes"])
|
|
||||||
generation = int(req["generation"])
|
|
||||||
with open(arena_path, "r+b", buffering=0) as f:
|
|
||||||
f.truncate(arena_bytes)
|
|
||||||
emit({"type": "worker_ready", "generation": generation})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "alloc":
|
|
||||||
if consume_failure("allocation_failed"):
|
|
||||||
return True
|
|
||||||
handle_id = int(req["handle_id"])
|
|
||||||
dtype = str(req["dtype"])
|
|
||||||
shape = str(req["shape"])
|
|
||||||
extent = int(req["extent"])
|
|
||||||
values, tensor = allocate_tensor(dtype, extent)
|
|
||||||
objects[handle_id] = DeviceObject(dtype=dtype, shape=shape, extent=extent, values=values, tensor=tensor)
|
|
||||||
emit({"type": "allocated", "handle_id": handle_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ in ("host_to_device", "device_to_host"):
|
|
||||||
if consume_failure("copy_failed"):
|
|
||||||
return True
|
|
||||||
copy = PendingCopy(
|
|
||||||
kind=typ,
|
|
||||||
copy_id=int(req["copy_id"]),
|
|
||||||
handle_id=int(req["handle_id"]),
|
|
||||||
host_offset=int(req["host_offset"]),
|
|
||||||
device_offset=int(req["device_offset"]),
|
|
||||||
length=int(req["len"]),
|
|
||||||
)
|
|
||||||
if copy.handle_id not in objects:
|
|
||||||
backend_error("copy_failed")
|
|
||||||
return True
|
|
||||||
if bool(req.get("defer", False)):
|
|
||||||
pending_copies[copy.copy_id] = copy
|
|
||||||
emit({"type": "copy_started", "copy_id": copy.copy_id})
|
|
||||||
else:
|
|
||||||
perform_copy(copy)
|
|
||||||
emit({"type": "copy_completed", "copy_id": copy.copy_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "complete_copy":
|
|
||||||
copy_id = int(req["copy_id"])
|
|
||||||
copy = pending_copies.pop(copy_id, None)
|
|
||||||
if copy is None:
|
|
||||||
backend_error("copy_failed")
|
|
||||||
return True
|
|
||||||
perform_copy(copy)
|
|
||||||
emit({"type": "copy_completed", "copy_id": copy_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "wrap_for_tinygrad":
|
|
||||||
if consume_failure("invalid_view"):
|
|
||||||
return True
|
|
||||||
handle_id = int(req["handle_id"])
|
|
||||||
obj = objects.get(handle_id)
|
|
||||||
if obj is None:
|
|
||||||
backend_error("invalid_view")
|
|
||||||
return True
|
|
||||||
dtype = str(req["dtype"])
|
|
||||||
shape = str(req["shape"])
|
|
||||||
if obj.dtype != dtype or obj.shape != shape:
|
|
||||||
backend_error("invalid_view")
|
|
||||||
return True
|
|
||||||
# Force materialization at view time so success depends on live tensor state.
|
|
||||||
_ = obj.tensor.tolist()
|
|
||||||
emit({"type": "view", "handle_id": handle_id, "dtype": dtype, "shape": shape})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "free":
|
|
||||||
handle_id = int(req["handle_id"])
|
|
||||||
if handle_id not in objects:
|
|
||||||
backend_error("invalid_view")
|
|
||||||
return True
|
|
||||||
del objects[handle_id]
|
|
||||||
emit({"type": "freed", "handle_id": handle_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "restart":
|
|
||||||
generation = int(req["generation"])
|
|
||||||
objects.clear()
|
|
||||||
pending_copies.clear()
|
|
||||||
fail_next = None
|
|
||||||
emit({"type": "worker_ready", "generation": generation})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "fail_next":
|
|
||||||
fail_next = str(req["failure"])
|
|
||||||
emit({"type": "ok"})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if typ == "shutdown":
|
|
||||||
emit({"type": "worker_stopped"})
|
|
||||||
return False
|
|
||||||
|
|
||||||
fatal("protocol_error", f"unknown command type {typ!r}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
for line in sys.stdin:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
req = json.loads(line)
|
|
||||||
if not isinstance(req, dict):
|
|
||||||
raise ValueError("request must be an object")
|
|
||||||
if not handle(req):
|
|
||||||
return 0
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception as exc:
|
|
||||||
fatal("backend_exception", str(exc))
|
|
||||||
return 2
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,7 +8,7 @@ version = "0.1.0"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["jupyter", "ipykernel", "pytest"]
|
dev = ["jupyter", "ipykernel", "maturin>=1.7,<2", "pytest", "pytest-timeout"]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
cache-keys = [
|
cache-keys = [
|
||||||
|
|
|
||||||
|
|
@ -1282,6 +1282,30 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maturin"
|
||||||
|
version = "1.14.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistune"
|
name = "mistune"
|
||||||
version = "3.2.0"
|
version = "3.2.0"
|
||||||
|
|
@ -1644,6 +1668,19 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-timeout"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
|
{ name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
|
|
@ -2313,8 +2350,10 @@ dev = [
|
||||||
{ name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ name = "ipykernel", version = "6.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
{ name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
{ name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
{ name = "jupyter" },
|
{ name = "jupyter" },
|
||||||
|
{ name = "maturin" },
|
||||||
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
{ name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
{ name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "pytest-timeout" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
|
|
@ -2323,7 +2362,9 @@ dev = [
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "ipykernel" },
|
{ name = "ipykernel" },
|
||||||
{ name = "jupyter" },
|
{ name = "jupyter" },
|
||||||
|
{ name = "maturin", specifier = ">=1.7,<2" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-timeout" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,6 @@ crate-type = ["cdylib"]
|
||||||
swactor = { path = "../../..", default-features = false, features = ["wasm", "std"] }
|
swactor = { path = "../../..", default-features = false, features = ["wasm", "std"] }
|
||||||
swactor-engine = { path = "../../engine", default-features = false }
|
swactor-engine = { path = "../../engine", default-features = false }
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,12 @@ pub struct WasmRuntime {
|
||||||
backend: SteppingBackend,
|
backend: SteppingBackend,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for WasmRuntime {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl WasmRuntime {
|
impl WasmRuntime {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
|
|
|
||||||
0
crates/data-plane/tests/namespace_host_read_guarantees.rs
Executable file → Normal file
0
crates/data-plane/tests/namespace_host_read_guarantees.rs
Executable file → Normal file
|
|
@ -15,3 +15,6 @@ tokio = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,7 @@ use swactor::runtime::{ExternalSender, Runtime, RuntimeParts};
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
/// Retained so the engine owns the runtime handle it drives for its full
|
/// Retained so the engine owns the runtime handle it drives for its full
|
||||||
/// lifetime. Core workers are moved into substrate tasks at construction.
|
/// lifetime. Core workers are moved into substrate tasks at construction.
|
||||||
#[allow(dead_code)]
|
_runtime: Runtime,
|
||||||
runtime: Runtime,
|
|
||||||
backend: Arc<dyn ExecutionBackend>,
|
backend: Arc<dyn ExecutionBackend>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +40,10 @@ impl Engine {
|
||||||
// no Tokio feature gate — so core progression does not silently
|
// no Tokio feature gate — so core progression does not silently
|
||||||
// disappear when an alternate backend is used (ENGINE_SPEC.md).
|
// disappear when an alternate backend is used (ENGINE_SPEC.md).
|
||||||
crate::core_driver::install(workers, &backend);
|
crate::core_driver::install(workers, &backend);
|
||||||
Ok(Engine { runtime, backend })
|
Ok(Engine {
|
||||||
|
_runtime: runtime,
|
||||||
|
backend,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a clonable handle for scheduling engine work.
|
/// Return a clonable handle for scheduling engine work.
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
//! Imports only public `swactor` APIs and exposes no private engine state. See
|
//! Imports only public `swactor` APIs and exposes no private engine state. See
|
||||||
//! `ENGINE_SPEC.md`.
|
//! `ENGINE_SPEC.md`.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
@ -23,9 +21,10 @@ pub fn default_runtime_parts() -> (RuntimeParts, Runtime) {
|
||||||
runtime_parts(RuntimeConfig::default())
|
runtime_parts(RuntimeConfig::default())
|
||||||
}
|
}
|
||||||
pub fn runtime_parts_with_workers(worker_count: usize) -> (RuntimeParts, Runtime) {
|
pub fn runtime_parts_with_workers(worker_count: usize) -> (RuntimeParts, Runtime) {
|
||||||
let mut config = RuntimeConfig::default();
|
runtime_parts(RuntimeConfig {
|
||||||
config.worker_count = worker_count;
|
worker_count,
|
||||||
runtime_parts(config)
|
..RuntimeConfig::default()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_parts() -> RuntimeParts {
|
pub fn default_parts() -> RuntimeParts {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
//! These tests exercise the native Tokio backend specifically; the
|
//! These tests exercise the native Tokio backend specifically; the
|
||||||
//! non-Tokio portability proof lives in `engine_unit.rs`.
|
//! non-Tokio portability proof lives in `engine_unit.rs`.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
//! These tests exercise internal logic directly and use the [`SteppingBackend`]
|
//! These tests exercise internal logic directly and use the [`SteppingBackend`]
|
||||||
//! to prove substrate independence without Tokio (ENGINE_SPEC.md).
|
//! to prove substrate independence without Tokio (ENGINE_SPEC.md).
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -1318,8 +1318,7 @@ fn engine_instant_is_ordered() {
|
||||||
/// A tasks-only probe backend that shares a sentinel `Arc<()>` so the test can
|
/// A tasks-only probe backend that shares a sentinel `Arc<()>` so the test can
|
||||||
/// observe exactly when the engine's strong backend reference is released.
|
/// observe exactly when the engine's strong backend reference is released.
|
||||||
struct SentinelBackend {
|
struct SentinelBackend {
|
||||||
#[allow(dead_code)]
|
_sentinel: Arc<()>,
|
||||||
sentinel: Arc<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExecutionBackend for SentinelBackend {
|
impl ExecutionBackend for SentinelBackend {
|
||||||
|
|
@ -1347,7 +1346,7 @@ fn dropping_engine_releases_backend_even_with_live_handles() {
|
||||||
let engine = Engine::new(
|
let engine = Engine::new(
|
||||||
parts,
|
parts,
|
||||||
SentinelBackend {
|
SentinelBackend {
|
||||||
sentinel: sentinel.clone(),
|
_sentinel: sentinel.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("tasks capability present");
|
.expect("tasks capability present");
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,6 @@ ed25519-dalek = { version = "2", features = ["std", "rand_core", "serde"] }
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ pub fn hex_encode(bytes: &[u8]) -> String {
|
||||||
|
|
||||||
/// Hex-decode a string into bytes. Returns `None` on invalid input.
|
/// Hex-decode a string into bytes. Returns `None` on invalid input.
|
||||||
pub fn hex_decode(hex: &str) -> Option<Vec<u8>> {
|
pub fn hex_decode(hex: &str) -> Option<Vec<u8>> {
|
||||||
if hex.len() % 2 != 0 {
|
if !hex.len().is_multiple_of(2) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut bytes = Vec::with_capacity(hex.len() / 2);
|
let mut bytes = Vec::with_capacity(hex.len() / 2);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub fn hex_encode(bytes: &[u8]) -> String {
|
||||||
|
|
||||||
/// Hex-decode a string into bytes. Returns `None` on invalid input.
|
/// Hex-decode a string into bytes. Returns `None` on invalid input.
|
||||||
pub fn hex_decode(hex: &str) -> Option<Vec<u8>> {
|
pub fn hex_decode(hex: &str) -> Option<Vec<u8>> {
|
||||||
if hex.len() % 2 != 0 {
|
if !hex.len().is_multiple_of(2) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut out = Vec::with_capacity(hex.len() / 2);
|
let mut out = Vec::with_capacity(hex.len() / 2);
|
||||||
|
|
|
||||||
|
|
@ -352,7 +352,9 @@ fn round_trip_across_two_runtimes() {
|
||||||
|
|
||||||
// Verify all 3 replies
|
// Verify all 3 replies
|
||||||
for i in 0..3u32 {
|
for i in 0..3u32 {
|
||||||
let pong = inbox_a.try_recv().expect(&format!("missing pong #{i}"));
|
let pong = inbox_a
|
||||||
|
.try_recv()
|
||||||
|
.unwrap_or_else(|| panic!("missing pong #{i}"));
|
||||||
assert_eq!(pong, Pong { value: i * 10 + 1 });
|
assert_eq!(pong, Pong { value: i * 10 + 1 });
|
||||||
}
|
}
|
||||||
assert!(inbox_a.try_recv().is_none(), "no extra messages");
|
assert!(inbox_a.try_recv().is_none(), "no extra messages");
|
||||||
|
|
|
||||||
|
|
@ -611,7 +611,6 @@ pub(crate) struct ResumeSignal;
|
||||||
/// Minimal core interface: send, spawn, stop, and extension access.
|
/// Minimal core interface: send, spawn, stop, and extension access.
|
||||||
/// Registry methods (naming, monitoring, groups) and timer scheduling
|
/// Registry methods (naming, monitoring, groups) and timer scheduling
|
||||||
/// are provided by extension traits in `swactor-std`.
|
/// are provided by extension traits in `swactor-std`.
|
||||||
#[allow(private_interfaces)]
|
|
||||||
pub trait ContextInner {
|
pub trait ContextInner {
|
||||||
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
fn send_any(&self, addr: ActorAddress, msg: Box<dyn Any + Send>) -> Result<(), Error>;
|
||||||
fn spawn_any(&self, request: SpawnRequest);
|
fn spawn_any(&self, request: SpawnRequest);
|
||||||
|
|
|
||||||
12
src/admin.rs
12
src/admin.rs
|
|
@ -118,6 +118,10 @@ impl<T: Message> Admin<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type AdminBoxedReply = Box<dyn Any + Send>;
|
pub(crate) type AdminBoxedReply = Box<dyn Any + Send>;
|
||||||
|
pub(crate) type AdminGetState =
|
||||||
|
Box<dyn FnOnce(ActorAddress, &dyn AnyActor, ActorTypeMetadata) -> AdminBoxedReply + Send>;
|
||||||
|
pub(crate) type AdminReplaceState =
|
||||||
|
Box<dyn FnOnce(&mut dyn AnyActor, ActorTypeMetadata) -> AdminResult<OperationResult> + Send>;
|
||||||
|
|
||||||
pub(crate) enum AdminCommand {
|
pub(crate) enum AdminCommand {
|
||||||
ListActors {
|
ListActors {
|
||||||
|
|
@ -130,17 +134,13 @@ pub(crate) enum AdminCommand {
|
||||||
GetActorState {
|
GetActorState {
|
||||||
actor: ActorAddress,
|
actor: ActorAddress,
|
||||||
reply_to: ActorAddress,
|
reply_to: ActorAddress,
|
||||||
get: Box<
|
get: AdminGetState,
|
||||||
dyn FnOnce(ActorAddress, &dyn AnyActor, ActorTypeMetadata) -> AdminBoxedReply + Send,
|
|
||||||
>,
|
|
||||||
not_found: Box<dyn FnOnce(ActorAddress) -> AdminBoxedReply + Send>,
|
not_found: Box<dyn FnOnce(ActorAddress) -> AdminBoxedReply + Send>,
|
||||||
},
|
},
|
||||||
ReplaceActorState {
|
ReplaceActorState {
|
||||||
actor: ActorAddress,
|
actor: ActorAddress,
|
||||||
reply_to: ActorAddress,
|
reply_to: ActorAddress,
|
||||||
replace: Box<
|
replace: AdminReplaceState,
|
||||||
dyn FnOnce(&mut dyn AnyActor, ActorTypeMetadata) -> AdminResult<OperationResult> + Send,
|
|
||||||
>,
|
|
||||||
},
|
},
|
||||||
StopActor {
|
StopActor {
|
||||||
actor: ActorAddress,
|
actor: ActorAddress,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::Instant;
|
use crate::Instant;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
use std::cmp::Reverse;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
@ -124,7 +125,7 @@ impl Worker {
|
||||||
|| self
|
|| self
|
||||||
.worker_ext
|
.worker_ext
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |e| e.has_pending_work())
|
.is_some_and(|extension| extension.has_pending_work())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run one synchronous worker pass. Returns `true` if any work was done.
|
/// Run one synchronous worker pass. Returns `true` if any work was done.
|
||||||
|
|
@ -928,7 +929,7 @@ impl ActorPool {
|
||||||
let snap_depth = slot.mailbox.len();
|
let snap_depth = slot.mailbox.len();
|
||||||
let mut snap_type_counts: Vec<(&'static str, u64)> =
|
let mut snap_type_counts: Vec<(&'static str, u64)> =
|
||||||
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
|
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
|
||||||
snap_type_counts.sort_by(|a, b| b.1.cmp(&a.1));
|
snap_type_counts.sort_by_key(|&(_, count)| Reverse(count));
|
||||||
|
|
||||||
let ctx = Ctx::new(
|
let ctx = Ctx::new(
|
||||||
wctx,
|
wctx,
|
||||||
|
|
@ -1132,7 +1133,7 @@ impl ActorPool {
|
||||||
if is_on_stop_eligible(slot.stopping, slot.poisoned) {
|
if is_on_stop_eligible(slot.stopping, slot.poisoned) {
|
||||||
let mut type_counts: Vec<(&'static str, u64)> =
|
let mut type_counts: Vec<(&'static str, u64)> =
|
||||||
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
|
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
|
||||||
type_counts.sort_by(|a, b| b.1.cmp(&a.1));
|
type_counts.sort_by_key(|&(_, count)| Reverse(count));
|
||||||
let ctx = Ctx::new(
|
let ctx = Ctx::new(
|
||||||
inner,
|
inner,
|
||||||
addr,
|
addr,
|
||||||
|
|
@ -1159,7 +1160,7 @@ impl ActorPool {
|
||||||
out.extend(self.actors.iter().map(|(&addr, slot)| {
|
out.extend(self.actors.iter().map(|(&addr, slot)| {
|
||||||
let mut type_counts: Vec<(&'static str, u64)> =
|
let mut type_counts: Vec<(&'static str, u64)> =
|
||||||
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
|
slot.msg_type_counts.iter().map(|(&k, &v)| (k, v)).collect();
|
||||||
type_counts.sort_by(|a, b| b.1.cmp(&a.1));
|
type_counts.sort_by_key(|&(_, count)| Reverse(count));
|
||||||
let metadata = slot.actor.metadata();
|
let metadata = slot.actor.metadata();
|
||||||
ActorSnapshot {
|
ActorSnapshot {
|
||||||
address: addr,
|
address: addr,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Covers: spawning, on_start, lifecycle decision paths, parent-child delegation,
|
//! Covers: spawning, on_start, lifecycle decision paths, parent-child delegation,
|
||||||
//! graceful stop, panic isolation, dead actor cleanup, and watching (ActorExited).
|
//! graceful stop, panic isolation, dead actor cleanup, and watching (ActorExited).
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
// Shared types and helpers for runtime test files.
|
// Shared types and helpers for runtime test files.
|
||||||
|
|
||||||
#![allow(dead_code, unused_imports)]
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,6 @@ impl RuntimeExtension for SeamExtension {
|
||||||
&self,
|
&self,
|
||||||
dead: &[(ActorAddress, StopReason, Option<ExitValue>)],
|
dead: &[(ActorAddress, StopReason, Option<ExitValue>)],
|
||||||
) -> Vec<(ActorAddress, Box<dyn Any + Send>)> {
|
) -> Vec<(ActorAddress, Box<dyn Any + Send>)> {
|
||||||
let _ = dead
|
|
||||||
.iter()
|
|
||||||
.map(|(_, reason, value)| (reason, value))
|
|
||||||
.count();
|
|
||||||
let Some(report_to) = *self.state.death_report_to.lock() else {
|
let Some(report_to) = *self.state.death_report_to.lock() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
205
tests/lint_policy.rs
Normal file
205
tests/lint_policy.rs
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use syn::visit::Visit;
|
||||||
|
use syn::{Attribute, Meta};
|
||||||
|
|
||||||
|
const SOURCE_ROOTS: &[&str] = &["src", "crates", "apps", "xtask", "tools", "tests"];
|
||||||
|
const FORBIDDEN_ATTRIBUTES: &[&str] = &["allow", "expect"];
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct SuppressionVisitor {
|
||||||
|
found: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ast> Visit<'ast> for SuppressionVisitor {
|
||||||
|
fn visit_attribute(&mut self, attribute: &'ast Attribute) {
|
||||||
|
if FORBIDDEN_ATTRIBUTES
|
||||||
|
.iter()
|
||||||
|
.any(|name| attribute.path().is_ident(name))
|
||||||
|
|| matches!(
|
||||||
|
&attribute.meta,
|
||||||
|
Meta::List(list)
|
||||||
|
if list.path.is_ident("cfg_attr") && tokens_contain_suppression(&list.tokens)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.found = true;
|
||||||
|
}
|
||||||
|
syn::visit::visit_attribute(self, attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_sources_do_not_suppress_lints() {
|
||||||
|
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let mut rust_sources = Vec::new();
|
||||||
|
for root in SOURCE_ROOTS {
|
||||||
|
collect_rust_sources(&workspace.join(root), &mut rust_sources);
|
||||||
|
}
|
||||||
|
rust_sources.sort();
|
||||||
|
|
||||||
|
let mut violations = Vec::new();
|
||||||
|
for source in rust_sources {
|
||||||
|
let text = fs::read_to_string(&source)
|
||||||
|
.unwrap_or_else(|error| panic!("read {}: {error}", source.display()));
|
||||||
|
let syntax = syn::parse_file(&text)
|
||||||
|
.unwrap_or_else(|error| panic!("parse {}: {error}", source.display()));
|
||||||
|
let mut visitor = SuppressionVisitor::default();
|
||||||
|
visitor.visit_file(&syntax);
|
||||||
|
if visitor.found {
|
||||||
|
violations.push(
|
||||||
|
source
|
||||||
|
.strip_prefix(&workspace)
|
||||||
|
.unwrap_or(&source)
|
||||||
|
.display()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
violations.is_empty(),
|
||||||
|
"lint suppression attributes are forbidden; fix the warning instead:\n{}",
|
||||||
|
violations.join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suppression_visitor_detects_direct_and_conditional_attributes() {
|
||||||
|
for source in [
|
||||||
|
"#[allow(dead_code)] fn hidden() {}",
|
||||||
|
"#![expect(unused_imports)]",
|
||||||
|
"#[cfg_attr(test, allow(clippy::too_many_arguments))] fn hidden() {}",
|
||||||
|
"#![cfg_attr(feature = \"strict\", expect(dead_code))]",
|
||||||
|
] {
|
||||||
|
let syntax = syn::parse_file(source).expect("valid suppression probe");
|
||||||
|
let mut visitor = SuppressionVisitor::default();
|
||||||
|
visitor.visit_file(&syntax);
|
||||||
|
assert!(visitor.found, "suppression escaped detection: {source}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let syntax = syn::parse_file("#[derive(Clone)] struct Clean;").unwrap();
|
||||||
|
let mut visitor = SuppressionVisitor::default();
|
||||||
|
visitor.visit_file(&syntax);
|
||||||
|
assert!(!visitor.found);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_tree_test_modules_are_wired() {
|
||||||
|
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let mut test_directories = Vec::new();
|
||||||
|
for root in SOURCE_ROOTS {
|
||||||
|
collect_source_test_directories(&workspace.join(root), &mut test_directories);
|
||||||
|
}
|
||||||
|
test_directories.sort();
|
||||||
|
|
||||||
|
let mut unwired = Vec::new();
|
||||||
|
for directory in test_directories {
|
||||||
|
let module_path = directory.join("mod.rs");
|
||||||
|
let module_source = fs::read_to_string(&module_path)
|
||||||
|
.unwrap_or_else(|error| panic!("read {}: {error}", module_path.display()));
|
||||||
|
let module = syn::parse_file(&module_source)
|
||||||
|
.unwrap_or_else(|error| panic!("parse {}: {error}", module_path.display()));
|
||||||
|
let declared = module
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
syn::Item::Mod(item) => Some(item.ident.to_string()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&directory)
|
||||||
|
.unwrap_or_else(|error| panic!("read directory {}: {error}", directory.display()))
|
||||||
|
{
|
||||||
|
let path = entry.expect("read test-module entry").path();
|
||||||
|
if path.extension().is_some_and(|extension| extension == "rs")
|
||||||
|
&& path.file_stem().is_some_and(|stem| stem != "mod")
|
||||||
|
{
|
||||||
|
let module_name = path
|
||||||
|
.file_stem()
|
||||||
|
.expect("test module stem")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
if !declared.contains(&module_name) {
|
||||||
|
unwired.push(
|
||||||
|
path.strip_prefix(&workspace)
|
||||||
|
.unwrap_or(&path)
|
||||||
|
.display()
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
unwired.is_empty(),
|
||||||
|
"source-tree test modules must be declared by their adjacent mod.rs:\n{}",
|
||||||
|
unwired.join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_source_test_directories(directory: &Path, directories: &mut Vec<PathBuf>) {
|
||||||
|
if !directory.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if directory.file_name().is_some_and(|name| name == "tests")
|
||||||
|
&& directory
|
||||||
|
.parent()
|
||||||
|
.and_then(Path::file_name)
|
||||||
|
.is_some_and(|name| name == "src")
|
||||||
|
{
|
||||||
|
directories.push(directory.to_path_buf());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entries = fs::read_dir(directory)
|
||||||
|
.unwrap_or_else(|error| panic!("read directory {}: {error}", directory.display()));
|
||||||
|
for entry in entries {
|
||||||
|
let path = entry.expect("read source-tree entry").path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_source_test_directories(&path, directories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_rust_sources(directory: &Path, sources: &mut Vec<PathBuf>) {
|
||||||
|
if !directory.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let entries = fs::read_dir(directory)
|
||||||
|
.unwrap_or_else(|error| panic!("read directory {}: {error}", directory.display()));
|
||||||
|
for entry in entries {
|
||||||
|
let entry = entry.unwrap_or_else(|error| panic!("read directory entry: {error}"));
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
collect_rust_sources(&path, sources);
|
||||||
|
} else if path.extension().is_some_and(|extension| extension == "rs") {
|
||||||
|
sources.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokens_contain_suppression(tokens: &proc_macro2::TokenStream) -> bool {
|
||||||
|
let mut token_trees = tokens.clone().into_iter().peekable();
|
||||||
|
while let Some(token) = token_trees.next() {
|
||||||
|
match token {
|
||||||
|
proc_macro2::TokenTree::Ident(identifier)
|
||||||
|
if FORBIDDEN_ATTRIBUTES.iter().any(|name| identifier == *name)
|
||||||
|
&& matches!(
|
||||||
|
token_trees.peek(),
|
||||||
|
Some(proc_macro2::TokenTree::Group(group))
|
||||||
|
if group.delimiter() == proc_macro2::Delimiter::Parenthesis
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
proc_macro2::TokenTree::Group(group) if tokens_contain_suppression(&group.stream()) => {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Covers: routing correctness at scale, send-from-within-handler patterns,
|
//! Covers: routing correctness at scale, send-from-within-handler patterns,
|
||||||
//! address error handling, and fairness/budgets.
|
//! address error handling, and fairness/budgets.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! `docs/specs/drafts/MULTICORE_SPEC.md`. They observe behavior through public
|
//! `docs/specs/drafts/MULTICORE_SPEC.md`. They observe behavior through public
|
||||||
//! APIs only — never inspecting source layout.
|
//! APIs only — never inspecting source layout.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
@ -157,9 +157,10 @@ fn worker_of(stats: &swactor::stats::RuntimeStats, addr: ActorAddress) -> usize
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_with(workers: usize) -> RuntimeConfig {
|
fn config_with(workers: usize) -> RuntimeConfig {
|
||||||
let mut c = RuntimeConfig::default();
|
RuntimeConfig {
|
||||||
c.worker_count = workers;
|
worker_count: workers,
|
||||||
c
|
..RuntimeConfig::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Phase 1: single-thread host advances every worker once ─────────────────
|
// ─── Phase 1: single-thread host advances every worker once ─────────────────
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Uses proptest for randomized testing and proptest-state-machine for
|
//! Uses proptest for randomized testing and proptest-state-machine for
|
||||||
//! stateful property testing with automatic shrinking of failing sequences.
|
//! stateful property testing with automatic shrinking of failing sequences.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::plain_host;
|
use common::plain_host;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Runtime Admin API tests — inventory, typed actor state, lifecycle control, and scheduling.
|
//! Runtime Admin API tests — inventory, typed actor state, lifecycle control, and scheduling.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Covers: high-volume delivery, panic isolation under load, and sustained
|
//! Covers: high-volume delivery, panic isolation under load, and sustained
|
||||||
//! throughput with no message loss. All tests are tick-driven (single worker).
|
//! throughput with no message loss. All tests are tick-driven (single worker).
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Tests only the std APIs used by production crates: runtime naming,
|
//! Tests only the std APIs used by production crates: runtime naming,
|
||||||
//! runtime groups, actor-side watch, actor-side group join, and extension install.
|
//! runtime groups, actor-side watch, actor-side group join, and extension install.
|
||||||
|
|
||||||
mod common;
|
pub mod common;
|
||||||
use common::*;
|
use common::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,6 @@ publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
7
tools/actor-control-flow-lint/tests/fixtures/fail-domain-capabilities/Cargo.lock
generated
vendored
7
tools/actor-control-flow-lint/tests/fixtures/fail-domain-capabilities/Cargo.lock
generated
vendored
|
|
@ -10,6 +10,12 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.1"
|
version = "2.13.1"
|
||||||
|
|
@ -182,6 +188,7 @@ dependencies = [
|
||||||
name = "swactor"
|
name = "swactor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
#![allow(clippy::disallowed_methods, dead_code, unused_must_use)]
|
|
||||||
|
|
||||||
use std::thread::sleep as renamed_sleep;
|
use std::thread::sleep as renamed_sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
|
||||||
7
tools/actor-control-flow-lint/tests/fixtures/pass-actor-domain/Cargo.lock
generated
vendored
7
tools/actor-control-flow-lint/tests/fixtures/pass-actor-domain/Cargo.lock
generated
vendored
|
|
@ -10,6 +10,12 @@ dependencies = [
|
||||||
"swactor-engine",
|
"swactor-engine",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic-waker"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.1"
|
version = "2.13.1"
|
||||||
|
|
@ -182,6 +188,7 @@ dependencies = [
|
||||||
name = "swactor"
|
name = "swactor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,6 @@ urlencoding = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wiremock = "0.6"
|
wiremock = "0.6"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -48,50 +48,53 @@ fn env_optional_positive_f64(name: &str) -> Option<f64> {
|
||||||
impl SelectionPolicy {
|
impl SelectionPolicy {
|
||||||
/// Build selection policy from the historical `PP_*` environment knobs.
|
/// Build selection policy from the historical `PP_*` environment knobs.
|
||||||
pub fn from_env() -> Self {
|
pub fn from_env() -> Self {
|
||||||
let mut policy = Self::default();
|
let defaults = Self::default();
|
||||||
policy.min_gpu_ram_mb = env_positive_u64(ENV_GPU_MIN_RAM_MB);
|
let mut blacklist_hosts = defaults.blacklist_hosts;
|
||||||
if let Some(min_compute_cap) = env_positive_u64(ENV_MIN_COMPUTE_CAP) {
|
|
||||||
policy.min_compute_cap = Some(min_compute_cap);
|
|
||||||
}
|
|
||||||
policy.min_down_mbps = env_nonnegative_f64(ENV_MIN_INET_DOWN_MBPS, 100.0);
|
|
||||||
policy.min_reliability = std::env::var(ENV_MIN_RELIABILITY)
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.trim().parse::<f64>().ok())
|
|
||||||
.filter(|&v| (0.0..=1.0).contains(&v))
|
|
||||||
.unwrap_or(0.95);
|
|
||||||
policy.require_verified = truthy_env(ENV_REQUIRE_VERIFIED);
|
|
||||||
policy.min_up_mbps = env_optional_positive_f64(ENV_MIN_INET_UP_MBPS);
|
|
||||||
policy.max_dph_total = env_optional_positive_f64(ENV_MAX_DPH_TOTAL);
|
|
||||||
policy.drop_cheap_frac = std::env::var(ENV_DROP_CHEAP_FRAC)
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.trim().parse::<f64>().ok())
|
|
||||||
.filter(|v| v.is_finite())
|
|
||||||
.map(|v| v.clamp(0.0, 0.99))
|
|
||||||
.unwrap_or(0.30);
|
|
||||||
policy.image_size_gb = env_optional_positive_f64(ENV_IMAGE_SIZE_GB);
|
|
||||||
if let Ok(raw) = std::env::var(ENV_BLACKLIST_HOSTS) {
|
if let Ok(raw) = std::env::var(ENV_BLACKLIST_HOSTS) {
|
||||||
policy
|
blacklist_hosts.extend(
|
||||||
.blacklist_hosts
|
raw.split(',')
|
||||||
.extend(raw.split(',').filter_map(|s| s.trim().parse::<u64>().ok()));
|
.filter_map(|value| value.trim().parse::<u64>().ok()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
gpu_name: defaults.gpu_name,
|
||||||
|
min_gpu_ram_mb: env_positive_u64(ENV_GPU_MIN_RAM_MB),
|
||||||
|
min_compute_cap: env_positive_u64(ENV_MIN_COMPUTE_CAP).or(defaults.min_compute_cap),
|
||||||
|
min_down_mbps: env_nonnegative_f64(ENV_MIN_INET_DOWN_MBPS, 100.0),
|
||||||
|
min_reliability: std::env::var(ENV_MIN_RELIABILITY)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.trim().parse::<f64>().ok())
|
||||||
|
.filter(|&value| (0.0..=1.0).contains(&value))
|
||||||
|
.unwrap_or(0.95),
|
||||||
|
require_verified: truthy_env(ENV_REQUIRE_VERIFIED),
|
||||||
|
min_up_mbps: env_optional_positive_f64(ENV_MIN_INET_UP_MBPS),
|
||||||
|
max_dph_total: env_optional_positive_f64(ENV_MAX_DPH_TOTAL),
|
||||||
|
blacklist_hosts,
|
||||||
|
drop_cheap_frac: std::env::var(ENV_DROP_CHEAP_FRAC)
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.trim().parse::<f64>().ok())
|
||||||
|
.filter(|value| value.is_finite())
|
||||||
|
.map(|value| value.clamp(0.0, 0.99))
|
||||||
|
.unwrap_or(0.30),
|
||||||
|
image_size_gb: env_optional_positive_f64(ENV_IMAGE_SIZE_GB),
|
||||||
}
|
}
|
||||||
policy
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LifecyclePolicy {
|
impl LifecyclePolicy {
|
||||||
/// Build lifecycle policy from environment, using caller-provided poll cadence.
|
/// Build lifecycle policy from environment, using caller-provided poll cadence.
|
||||||
pub fn from_env(poll_interval: Duration) -> Self {
|
pub fn from_env(poll_interval: Duration) -> Self {
|
||||||
let mut policy = Self::default();
|
Self {
|
||||||
policy.lease_pace = Duration::from_millis(
|
lease_pace: Duration::from_millis(
|
||||||
std::env::var(ENV_LEASE_PACE_MS)
|
std::env::var(ENV_LEASE_PACE_MS)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
.and_then(|value| value.trim().parse::<u64>().ok())
|
||||||
.unwrap_or(600),
|
.unwrap_or(600),
|
||||||
);
|
),
|
||||||
policy.poll_interval = poll_interval;
|
poll_interval,
|
||||||
if let Some(state_timeout_secs) = env_positive_u64(ENV_STATE_TIMEOUT_SECS) {
|
state_timeout: env_positive_u64(ENV_STATE_TIMEOUT_SECS)
|
||||||
policy.state_timeout = Duration::from_secs(state_timeout_secs);
|
.map(Duration::from_secs)
|
||||||
|
.unwrap_or_else(|| Self::default().state_timeout),
|
||||||
}
|
}
|
||||||
policy
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ pub(crate) fn reachable_offers(offers: Vec<Offer>, policy: &SelectionPolicy) ->
|
||||||
.filter(|o| {
|
.filter(|o| {
|
||||||
o.geolocation
|
o.geolocation
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map_or(false, |g| !g.to_uppercase().contains("CN"))
|
.is_some_and(|geolocation| !geolocation.to_uppercase().contains("CN"))
|
||||||
})
|
})
|
||||||
.filter(|o| o.host_id.map_or(true, |h| !blacklist.contains(&h)))
|
.filter(|offer| offer.host_id.is_none_or(|host| !blacklist.contains(&host)))
|
||||||
.filter(|o| o.verification.as_deref() != Some("deverified"))
|
.filter(|o| o.verification.as_deref() != Some("deverified"))
|
||||||
.filter(|o| policy.max_dph_total.is_none_or(|max| o.dph_total <= max))
|
.filter(|o| policy.max_dph_total.is_none_or(|max| o.dph_total <= max))
|
||||||
.filter(|o| {
|
.filter(|o| {
|
||||||
|
|
|
||||||
|
|
@ -83,15 +83,15 @@ fn next_eligible_offer<'a>(
|
||||||
failed_host_ids: &HashSet<u64>,
|
failed_host_ids: &HashSet<u64>,
|
||||||
preferred_offer_id: Option<u64>,
|
preferred_offer_id: Option<u64>,
|
||||||
) -> Option<&'a Offer> {
|
) -> Option<&'a Offer> {
|
||||||
if let Some(offer_id) = preferred_offer_id {
|
if let Some(offer_id) = preferred_offer_id
|
||||||
if let Some(offer) = pool.iter().find(|o| o.id == offer_id) {
|
&& let Some(offer) = pool.iter().find(|offer| offer.id == offer_id)
|
||||||
if !tried_offer_ids.contains(&offer.id)
|
&& !tried_offer_ids.contains(&offer.id)
|
||||||
&& offer.host_id.is_none_or(|h| !failed_host_ids.contains(&h))
|
&& offer
|
||||||
|
.host_id
|
||||||
|
.is_none_or(|host| !failed_host_ids.contains(&host))
|
||||||
{
|
{
|
||||||
return Some(offer);
|
return Some(offer);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pool.iter().find(|o| {
|
pool.iter().find(|o| {
|
||||||
!tried_offer_ids.contains(&o.id)
|
!tried_offer_ids.contains(&o.id)
|
||||||
|
|
@ -108,18 +108,32 @@ fn env_for_index(req: &ProvisionRequest, index: u32) -> BTreeMap<String, String>
|
||||||
env
|
env
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn provision_one(
|
struct ProvisionOneRequest<'a> {
|
||||||
client: &reqwest::Client,
|
client: &'a reqwest::Client,
|
||||||
base_url: &str,
|
base_url: &'a str,
|
||||||
api_key: &str,
|
api_key: &'a str,
|
||||||
req: &ProvisionRequest,
|
request: &'a ProvisionRequest,
|
||||||
pool: &[Offer],
|
pool: &'a [Offer],
|
||||||
index: u32,
|
index: u32,
|
||||||
tried_offer_ids: &mut Vec<u64>,
|
tried_offer_ids: &'a mut Vec<u64>,
|
||||||
used_host_ids: &mut HashSet<u64>,
|
used_host_ids: &'a mut HashSet<u64>,
|
||||||
failed_host_ids: &mut HashSet<u64>,
|
failed_host_ids: &'a mut HashSet<u64>,
|
||||||
preferred_offer_id: Option<u64>,
|
preferred_offer_id: Option<u64>,
|
||||||
) -> Result<ProvisionedInstance, String> {
|
}
|
||||||
|
|
||||||
|
async fn provision_one(request: ProvisionOneRequest<'_>) -> Result<ProvisionedInstance, String> {
|
||||||
|
let ProvisionOneRequest {
|
||||||
|
client,
|
||||||
|
base_url,
|
||||||
|
api_key,
|
||||||
|
request: req,
|
||||||
|
pool,
|
||||||
|
index,
|
||||||
|
tried_offer_ids,
|
||||||
|
used_host_ids,
|
||||||
|
failed_host_ids,
|
||||||
|
preferred_offer_id,
|
||||||
|
} = request;
|
||||||
let mut attempt = 1_u64;
|
let mut attempt = 1_u64;
|
||||||
loop {
|
loop {
|
||||||
let offer = match next_eligible_offer(
|
let offer = match next_eligible_offer(
|
||||||
|
|
@ -230,20 +244,21 @@ pub async fn provision_fleet(
|
||||||
let mut used_host_ids = HashSet::new();
|
let mut used_host_ids = HashSet::new();
|
||||||
let mut failed_host_ids = HashSet::new();
|
let mut failed_host_ids = HashSet::new();
|
||||||
for index in 0..req.count {
|
for index in 0..req.count {
|
||||||
match provision_one(
|
match provision_one(ProvisionOneRequest {
|
||||||
client,
|
client,
|
||||||
base_url,
|
base_url,
|
||||||
api_key,
|
api_key,
|
||||||
&req,
|
request: &req,
|
||||||
&pool,
|
pool: &pool,
|
||||||
index,
|
index,
|
||||||
&mut tried_offer_ids,
|
tried_offer_ids: &mut tried_offer_ids,
|
||||||
&mut used_host_ids,
|
used_host_ids: &mut used_host_ids,
|
||||||
&mut failed_host_ids,
|
failed_host_ids: &mut failed_host_ids,
|
||||||
req.preferred_offer_id
|
preferred_offer_id: req
|
||||||
|
.preferred_offer_id
|
||||||
.filter(|_| req.count == 1)
|
.filter(|_| req.count == 1)
|
||||||
.or_else(|| first_wave.get(index as usize).map(|offer| offer.id)),
|
.or_else(|| first_wave.get(index as usize).map(|offer| offer.id)),
|
||||||
)
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(info) => created.push(info),
|
Ok(info) => created.push(info),
|
||||||
|
|
@ -279,18 +294,18 @@ pub async fn provision_fleet(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
eprintln!("lease_chain: replacing index {index}");
|
eprintln!("lease_chain: replacing index {index}");
|
||||||
match provision_one(
|
match provision_one(ProvisionOneRequest {
|
||||||
client,
|
client,
|
||||||
base_url,
|
base_url,
|
||||||
api_key,
|
api_key,
|
||||||
&req,
|
request: &req,
|
||||||
&pool,
|
pool: &pool,
|
||||||
index,
|
index,
|
||||||
&mut tried_offer_ids,
|
tried_offer_ids: &mut tried_offer_ids,
|
||||||
&mut used_host_ids,
|
used_host_ids: &mut used_host_ids,
|
||||||
&mut failed_host_ids,
|
failed_host_ids: &mut failed_host_ids,
|
||||||
None,
|
preferred_offer_id: None,
|
||||||
)
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(info) => created[idx] = info,
|
Ok(info) => created[idx] = info,
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,11 @@ pub fn plan_distinct_host_first_wave(
|
||||||
if selected.len() == target {
|
if selected.len() == target {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Some(host_id) = offer.host_id {
|
if let Some(host_id) = offer.host_id
|
||||||
if !selected_hosts.insert(host_id) {
|
&& !selected_hosts.insert(host_id)
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
selected_ids.insert(offer.id);
|
selected_ids.insert(offer.id);
|
||||||
selected.push(offer.clone());
|
selected.push(offer.clone());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,6 @@ libc = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1"
|
proptest = "1"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,10 @@ mod properties {
|
||||||
codes in prop::collection::vec(any::<u8>(), 0..=32),
|
codes in prop::collection::vec(any::<u8>(), 0..=32),
|
||||||
registration in 0_usize..=32,
|
registration in 0_usize..=32,
|
||||||
) {
|
) {
|
||||||
let mut config = RuntimeConfig::default();
|
let parts = RuntimeParts::new(RuntimeConfig {
|
||||||
config.worker_count = 1;
|
worker_count: 1,
|
||||||
let parts = RuntimeParts::new(config);
|
..RuntimeConfig::default()
|
||||||
|
});
|
||||||
let runtime = parts.runtime().clone();
|
let runtime = parts.runtime().clone();
|
||||||
let backend = SteppingBackend::new();
|
let backend = SteppingBackend::new();
|
||||||
let _engine =
|
let _engine =
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,6 @@ pub struct NodeEdgeAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NodeEdgeAgent {
|
impl NodeEdgeAgent {
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
attempt: u64,
|
attempt: u64,
|
||||||
logical_node: String,
|
logical_node: String,
|
||||||
|
|
@ -756,7 +755,7 @@ impl swactor::actor::ActorInterface for EdgeAckRelay {
|
||||||
if let Some(addr) = self.supervisor.get() {
|
if let Some(addr) = self.supervisor.get() {
|
||||||
let _ = self
|
let _ = self
|
||||||
.sender
|
.sender
|
||||||
.send_to(addr.clone(), crate::demo::feed::SupervisorMsg::EdgeAck(ack));
|
.send_to(*addr, crate::demo::feed::SupervisorMsg::EdgeAck(ack));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,25 +215,43 @@ pub struct SupervisorActor {
|
||||||
shutdown: Option<ShutdownState>,
|
shutdown: Option<ShutdownState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SupervisorConfig {
|
||||||
|
pub driver: ClusterDriver,
|
||||||
|
pub executor: IdempotentEffectExecutor<DemoBackend, EngineSpawner>,
|
||||||
|
pub manager: NodeManager,
|
||||||
|
pub driver_handle: std::sync::Arc<crate::demo::DemoDriverHandle>,
|
||||||
|
pub telemetry: SupervisorTelemetry,
|
||||||
|
pub dashboard: dashboard::DashboardHandle,
|
||||||
|
pub sender: swactor::runtime::ExternalSender,
|
||||||
|
pub registry: provisioning::BootstrapRegistry,
|
||||||
|
pub collector: std::sync::Arc<dyn provisioning::NodeTelemetryCollector>,
|
||||||
|
pub engine: EngineHandle,
|
||||||
|
pub remote_sub: telemetry::TelemetrySubscription,
|
||||||
|
pub initial_slots: Vec<String>,
|
||||||
|
pub run_id: RunId,
|
||||||
|
pub launch: crate::demo::LaunchStyle,
|
||||||
|
pub edge_actor: ActorAddress,
|
||||||
|
}
|
||||||
|
|
||||||
impl SupervisorActor {
|
impl SupervisorActor {
|
||||||
#[allow(clippy::too_many_arguments)]
|
pub fn new(config: SupervisorConfig) -> Self {
|
||||||
pub fn new(
|
let SupervisorConfig {
|
||||||
driver: ClusterDriver,
|
driver,
|
||||||
executor: IdempotentEffectExecutor<DemoBackend, EngineSpawner>,
|
executor,
|
||||||
manager: NodeManager,
|
manager,
|
||||||
driver_handle: std::sync::Arc<crate::demo::DemoDriverHandle>,
|
driver_handle,
|
||||||
mut telemetry: SupervisorTelemetry,
|
mut telemetry,
|
||||||
dashboard: dashboard::DashboardHandle,
|
dashboard,
|
||||||
sender: swactor::runtime::ExternalSender,
|
sender,
|
||||||
registry: provisioning::BootstrapRegistry,
|
registry,
|
||||||
collector: std::sync::Arc<dyn provisioning::NodeTelemetryCollector>,
|
collector,
|
||||||
engine: EngineHandle,
|
engine,
|
||||||
remote_sub: telemetry::TelemetrySubscription,
|
remote_sub,
|
||||||
initial_slots: Vec<String>,
|
initial_slots,
|
||||||
run_id: RunId,
|
run_id,
|
||||||
launch: crate::demo::LaunchStyle,
|
launch,
|
||||||
edge_actor: ActorAddress,
|
edge_actor,
|
||||||
) -> Self {
|
} = config;
|
||||||
let events_channel = telemetry.register("prov.reconciler.events");
|
let events_channel = telemetry.register("prov.reconciler.events");
|
||||||
let snapshot_channel = telemetry.register("prov.reconciler.snapshot");
|
let snapshot_channel = telemetry.register("prov.reconciler.snapshot");
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -814,7 +832,7 @@ impl SupervisorActor {
|
||||||
/// cards and the control view stay live between lifecycle transitions.
|
/// cards and the control view stay live between lifecycle transitions.
|
||||||
fn emit_node_status(&mut self, now: SystemTime) {
|
fn emit_node_status(&mut self, now: SystemTime) {
|
||||||
self.status_tick = self.status_tick.wrapping_add(1);
|
self.status_tick = self.status_tick.wrapping_add(1);
|
||||||
if self.status_tick % 4 != 0 {
|
if !self.status_tick.is_multiple_of(4) {
|
||||||
return; // 250ms ticks → heartbeat every second
|
return; // 250ms ticks → heartbeat every second
|
||||||
}
|
}
|
||||||
let attempts: Vec<u64> = self.nodes.keys().copied().collect();
|
let attempts: Vec<u64> = self.nodes.keys().copied().collect();
|
||||||
|
|
@ -968,7 +986,6 @@ impl SupervisorActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn publish_frame(
|
fn publish_frame(
|
||||||
dashboard: &dashboard::DashboardHandle,
|
dashboard: &dashboard::DashboardHandle,
|
||||||
stream: &telemetry::frame::StreamId,
|
stream: &telemetry::frame::StreamId,
|
||||||
|
|
@ -1489,7 +1506,7 @@ mod properties {
|
||||||
|
|
||||||
fn control_command(kind: u8, attempt: u64, index: usize) -> dashboard::control::ControlCommand {
|
fn control_command(kind: u8, attempt: u64, index: usize) -> dashboard::control::ControlCommand {
|
||||||
let command_id = format!("generated-command-{index}");
|
let command_id = format!("generated-command-{index}");
|
||||||
if kind % 2 == 0 {
|
if kind.is_multiple_of(2) {
|
||||||
dashboard::control::ControlCommand::Kill {
|
dashboard::control::ControlCommand::Kill {
|
||||||
command_id,
|
command_id,
|
||||||
node: format!("node-{attempt}"),
|
node: format!("node-{attempt}"),
|
||||||
|
|
@ -1568,9 +1585,10 @@ mod properties {
|
||||||
fn generated_supervisor_transitions_are_once_only_nonblocking_and_clean(
|
fn generated_supervisor_transitions_are_once_only_nonblocking_and_clean(
|
||||||
actions in supervisor_actions(),
|
actions in supervisor_actions(),
|
||||||
) {
|
) {
|
||||||
let mut config = RuntimeConfig::default();
|
let parts = RuntimeParts::new(RuntimeConfig {
|
||||||
config.worker_count = 1;
|
worker_count: 1,
|
||||||
let parts = RuntimeParts::new(config);
|
..RuntimeConfig::default()
|
||||||
|
});
|
||||||
let runtime = parts.runtime().clone();
|
let runtime = parts.runtime().clone();
|
||||||
let sender = runtime.create_sender();
|
let sender = runtime.create_sender();
|
||||||
let backend = SteppingBackend::new();
|
let backend = SteppingBackend::new();
|
||||||
|
|
@ -1644,25 +1662,25 @@ mod properties {
|
||||||
.spawn(AnnounceActor::new(manager.clone(), sender.clone()))
|
.spawn(AnnounceActor::new(manager.clone(), sender.clone()))
|
||||||
.expect("spawn generated announce actor");
|
.expect("spawn generated announce actor");
|
||||||
let supervisor = runtime
|
let supervisor = runtime
|
||||||
.spawn(SupervisorActor::new(
|
.spawn(SupervisorActor::new(SupervisorConfig {
|
||||||
driver,
|
driver,
|
||||||
executor,
|
executor,
|
||||||
manager.clone(),
|
manager: manager.clone(),
|
||||||
driver_handle,
|
driver_handle,
|
||||||
telemetry,
|
telemetry,
|
||||||
dashboard,
|
dashboard,
|
||||||
sender.clone(),
|
sender: sender.clone(),
|
||||||
registry,
|
registry,
|
||||||
Arc::new(evidence.clone()),
|
collector: Arc::new(evidence.clone()),
|
||||||
engine.handle(),
|
engine: engine.handle(),
|
||||||
remote_sub,
|
remote_sub,
|
||||||
Vec::new(),
|
initial_slots: Vec::new(),
|
||||||
RunId(1),
|
run_id: RunId(1),
|
||||||
crate::demo::LaunchStyle::Process {
|
launch: crate::demo::LaunchStyle::Process {
|
||||||
exe: "generated-demo-node".into(),
|
exe: "generated-demo-node".into(),
|
||||||
},
|
},
|
||||||
*edge_inbox.addr(),
|
edge_actor: *edge_inbox.addr(),
|
||||||
))
|
}))
|
||||||
.expect("spawn generated supervisor actor");
|
.expect("spawn generated supervisor actor");
|
||||||
drive(&backend, 8);
|
drive(&backend, 8);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ use provisioning::reconciler::ClusterDriver;
|
||||||
use provisioning::{ClusterShape, RunId};
|
use provisioning::{ClusterShape, RunId};
|
||||||
|
|
||||||
use feed::{
|
use feed::{
|
||||||
EngineSpawner, SupervisorActor, SupervisorMsg, SupervisorTelemetry, demo_retry_policy,
|
EngineSpawner, SupervisorActor, SupervisorConfig, SupervisorMsg, SupervisorTelemetry,
|
||||||
initial_slots,
|
demo_retry_policy, initial_slots,
|
||||||
};
|
};
|
||||||
use provider::{DemoBackend, DemoProvider, NodeManager};
|
use provider::{DemoBackend, DemoProvider, NodeManager};
|
||||||
|
|
||||||
|
|
@ -68,9 +68,10 @@ pub const DEFAULT_NODES: u64 = 3;
|
||||||
pub(super) fn shared_test_driver() -> Arc<IrohDriver> {
|
pub(super) fn shared_test_driver() -> Arc<IrohDriver> {
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static DRIVER: (Arc<IrohDriver>, Engine) = {
|
static DRIVER: (Arc<IrohDriver>, Engine) = {
|
||||||
let mut config = RuntimeConfig::default();
|
let parts = RuntimeParts::new(RuntimeConfig {
|
||||||
config.worker_count = 1;
|
worker_count: 1,
|
||||||
let parts = RuntimeParts::new(config);
|
..RuntimeConfig::default()
|
||||||
|
});
|
||||||
let backend = TokioBackend::new(TokioConfig::default())
|
let backend = TokioBackend::new(TokioConfig::default())
|
||||||
.expect("create shared demo test backend");
|
.expect("create shared demo test backend");
|
||||||
let engine =
|
let engine =
|
||||||
|
|
@ -99,11 +100,11 @@ pub(super) fn shared_test_driver() -> Arc<IrohDriver> {
|
||||||
/// replaced by a rebuild while this process runs, so fall back through
|
/// replaced by a rebuild while this process runs, so fall back through
|
||||||
/// argv[0] and PATH.
|
/// argv[0] and PATH.
|
||||||
fn resolve_exe() -> std::path::PathBuf {
|
fn resolve_exe() -> std::path::PathBuf {
|
||||||
if let Ok(path) = std::env::current_exe() {
|
if let Ok(path) = std::env::current_exe()
|
||||||
if !path.to_string_lossy().ends_with(" (deleted)") {
|
&& !path.to_string_lossy().ends_with(" (deleted)")
|
||||||
|
{
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(arg0) = std::env::args_os().next() {
|
if let Some(arg0) = std::env::args_os().next() {
|
||||||
let candidate = std::path::PathBuf::from(&arg0);
|
let candidate = std::path::PathBuf::from(&arg0);
|
||||||
if candidate.is_absolute() {
|
if candidate.is_absolute() {
|
||||||
|
|
@ -189,12 +190,14 @@ impl provisioning::NodeTelemetryCollector for DemoTelemetryCollector {
|
||||||
};
|
};
|
||||||
iroh_driver::spawn_pull_collector_to_actor(
|
iroh_driver::spawn_pull_collector_to_actor(
|
||||||
&self.engine,
|
&self.engine,
|
||||||
self.endpoint.clone(),
|
iroh_driver::PullCollectorConfig {
|
||||||
addr,
|
endpoint: self.endpoint.clone(),
|
||||||
|
peer: addr,
|
||||||
flow_id,
|
flow_id,
|
||||||
Vec::new(),
|
token: Vec::new(),
|
||||||
telemetry::SubscriptionRequest::all(),
|
request: telemetry::SubscriptionRequest::all(),
|
||||||
Arc::clone(&self.fanout),
|
fanout: Arc::clone(&self.fanout),
|
||||||
|
},
|
||||||
self.sender.clone(),
|
self.sender.clone(),
|
||||||
header_actor,
|
header_actor,
|
||||||
);
|
);
|
||||||
|
|
@ -347,15 +350,15 @@ fn run_supervisor(args: &[String]) -> Result<(), String> {
|
||||||
let route_view: RouteView =
|
let route_view: RouteView =
|
||||||
Arc::new(std::sync::RwLock::new(std::collections::HashMap::new()));
|
Arc::new(std::sync::RwLock::new(std::collections::HashMap::new()));
|
||||||
let outbox: Outbox = Arc::new(std::sync::Mutex::new(Vec::new()));
|
let outbox: Outbox = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||||
driver.enable_actor_bridge(
|
driver.enable_actor_bridge(iroh_driver::ActorBridgeConfig {
|
||||||
runtime.clone(),
|
runtime: runtime.clone(),
|
||||||
Arc::new(codec),
|
codec: Arc::new(codec),
|
||||||
routes,
|
routes,
|
||||||
announce,
|
swim: announce,
|
||||||
relay_mirror,
|
relay_mirror,
|
||||||
route_view,
|
route_view,
|
||||||
outbox,
|
outbox,
|
||||||
);
|
});
|
||||||
driver.install_actor_bridge_pump(Duration::from_millis(250));
|
driver.install_actor_bridge_pump(Duration::from_millis(250));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -458,23 +461,23 @@ fn run_supervisor(args: &[String]) -> Result<(), String> {
|
||||||
sender.clone(),
|
sender.clone(),
|
||||||
supervisor_slot.clone(),
|
supervisor_slot.clone(),
|
||||||
)?;
|
)?;
|
||||||
let supervisor = SupervisorActor::new(
|
let supervisor = SupervisorActor::new(SupervisorConfig {
|
||||||
cluster_driver,
|
driver: cluster_driver,
|
||||||
executor,
|
executor,
|
||||||
manager.clone(),
|
manager: manager.clone(),
|
||||||
driver_handle,
|
driver_handle,
|
||||||
telemetry,
|
telemetry,
|
||||||
dashboard.clone(),
|
dashboard: dashboard.clone(),
|
||||||
sender.clone(),
|
sender: sender.clone(),
|
||||||
bootstrap_registry,
|
registry: bootstrap_registry,
|
||||||
collector,
|
collector,
|
||||||
engine.handle(),
|
engine: engine.handle(),
|
||||||
remote_sub,
|
remote_sub,
|
||||||
slots,
|
initial_slots: slots,
|
||||||
RunId(1),
|
run_id: RunId(1),
|
||||||
launch.clone(),
|
launch: launch.clone(),
|
||||||
edge_actor,
|
edge_actor,
|
||||||
);
|
});
|
||||||
|
|
||||||
// Control plane: dashboard → supervisor.
|
// Control plane: dashboard → supervisor.
|
||||||
control::install(&runtime, supervisor_slot.clone());
|
control::install(&runtime, supervisor_slot.clone());
|
||||||
|
|
@ -483,7 +486,7 @@ fn run_supervisor(args: &[String]) -> Result<(), String> {
|
||||||
.spawn(supervisor)
|
.spawn(supervisor)
|
||||||
.map_err(|e| format!("spawn supervisor actor: {e}"))?;
|
.map_err(|e| format!("spawn supervisor actor: {e}"))?;
|
||||||
supervisor_slot
|
supervisor_slot
|
||||||
.set(supervisor_addr.clone())
|
.set(supervisor_addr)
|
||||||
.expect("supervisor address slot set once");
|
.expect("supervisor address slot set once");
|
||||||
manager.set_spawn_actor(sender.clone(), supervisor_slot.clone());
|
manager.set_spawn_actor(sender.clone(), supervisor_slot.clone());
|
||||||
|
|
||||||
|
|
@ -650,9 +653,10 @@ mod properties {
|
||||||
let pid = child.id() as i32;
|
let pid = child.id() as i32;
|
||||||
let stdout = child.stdout.take().expect("capture demo supervisor stdout");
|
let stdout = child.stdout.take().expect("capture demo supervisor stdout");
|
||||||
|
|
||||||
let mut config = RuntimeConfig::default();
|
let parts = RuntimeParts::new(RuntimeConfig {
|
||||||
config.worker_count = 1;
|
worker_count: 1,
|
||||||
let parts = RuntimeParts::new(config);
|
..RuntimeConfig::default()
|
||||||
|
});
|
||||||
let runtime = parts.runtime().clone();
|
let runtime = parts.runtime().clone();
|
||||||
let engine = Engine::new(
|
let engine = Engine::new(
|
||||||
parts,
|
parts,
|
||||||
|
|
|
||||||
|
|
@ -179,15 +179,15 @@ pub fn run_node_role(supervisor_addr_json: &str, attempt: u64) -> Result<(), Str
|
||||||
let route_view: RouteView =
|
let route_view: RouteView =
|
||||||
Arc::new(std::sync::RwLock::new(std::collections::HashMap::new()));
|
Arc::new(std::sync::RwLock::new(std::collections::HashMap::new()));
|
||||||
let outbox: Outbox = Arc::new(std::sync::Mutex::new(Vec::new()));
|
let outbox: Outbox = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||||
driver.enable_actor_bridge(
|
driver.enable_actor_bridge(iroh_driver::ActorBridgeConfig {
|
||||||
runtime.clone(),
|
runtime: runtime.clone(),
|
||||||
Arc::new(codec),
|
codec: Arc::new(codec),
|
||||||
routes,
|
routes,
|
||||||
edge_agent,
|
swim: edge_agent,
|
||||||
relay_mirror,
|
relay_mirror,
|
||||||
route_view,
|
route_view,
|
||||||
outbox,
|
outbox,
|
||||||
);
|
});
|
||||||
driver.install_actor_bridge_pump(Duration::from_millis(250));
|
driver.install_actor_bridge_pump(Duration::from_millis(250));
|
||||||
}
|
}
|
||||||
let driver = Arc::new(driver);
|
let driver = Arc::new(driver);
|
||||||
|
|
@ -199,7 +199,7 @@ pub fn run_node_role(supervisor_addr_json: &str, attempt: u64) -> Result<(), Str
|
||||||
|
|
||||||
// Join, then announce identity + advertised address to the supervisor's
|
// Join, then announce identity + advertised address to the supervisor's
|
||||||
// bootstrap actor over the control plane (readiness + telemetry dial).
|
// bootstrap actor over the control plane (readiness + telemetry dial).
|
||||||
driver.join(&[supervisor_addr.clone()]);
|
driver.join(std::slice::from_ref(&supervisor_addr));
|
||||||
let addr_json =
|
let addr_json =
|
||||||
serde_json::to_string(&driver.endpoint_addr()).map_err(|e| format!("addr: {e}"))?;
|
serde_json::to_string(&driver.endpoint_addr()).map_err(|e| format!("addr: {e}"))?;
|
||||||
|
|
||||||
|
|
@ -546,9 +546,10 @@ mod properties {
|
||||||
actions in node_actions(),
|
actions in node_actions(),
|
||||||
attempt in any::<u64>(),
|
attempt in any::<u64>(),
|
||||||
) {
|
) {
|
||||||
let mut config = RuntimeConfig::default();
|
let parts = RuntimeParts::new(RuntimeConfig {
|
||||||
config.worker_count = 1;
|
worker_count: 1,
|
||||||
let parts = RuntimeParts::new(config);
|
..RuntimeConfig::default()
|
||||||
|
});
|
||||||
let runtime = parts.runtime().clone();
|
let runtime = parts.runtime().clone();
|
||||||
let backend = SteppingBackend::new();
|
let backend = SteppingBackend::new();
|
||||||
let engine =
|
let engine =
|
||||||
|
|
|
||||||
|
|
@ -485,10 +485,12 @@ impl EffectBackend for DemoBackend {
|
||||||
.expect("demo provider")
|
.expect("demo provider")
|
||||||
.create_node(spec, null_sink())
|
.create_node(spec, null_sink())
|
||||||
.map_err(EffectError::definite)?;
|
.map_err(EffectError::definite)?;
|
||||||
Ok(OperationOutcome::LeaseCreated(CreateLeaseResult {
|
Ok(OperationOutcome::LeaseCreated(Box::new(
|
||||||
|
CreateLeaseResult {
|
||||||
lease: demo_lease(attempt),
|
lease: demo_lease(attempt),
|
||||||
endpoint: Some(demo_endpoint()),
|
endpoint: Some(demo_endpoint()),
|
||||||
}))
|
},
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
NodeManagerCommand::LookupEndpoint(_) => {
|
NodeManagerCommand::LookupEndpoint(_) => {
|
||||||
Ok(OperationOutcome::EndpointLookup(Some(demo_endpoint())))
|
Ok(OperationOutcome::EndpointLookup(Some(demo_endpoint())))
|
||||||
|
|
@ -701,9 +703,10 @@ mod properties {
|
||||||
actions.push(terminal);
|
actions.push(terminal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut config = RuntimeConfig::default();
|
let parts = RuntimeParts::new(RuntimeConfig {
|
||||||
config.worker_count = 1;
|
worker_count: 1,
|
||||||
let parts = RuntimeParts::new(config);
|
..RuntimeConfig::default()
|
||||||
|
});
|
||||||
let runtime = parts.runtime().clone();
|
let runtime = parts.runtime().clone();
|
||||||
let backend = SteppingBackend::new();
|
let backend = SteppingBackend::new();
|
||||||
let _engine =
|
let _engine =
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::process::{Command, ExitCode};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, ExitCode, Stdio};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
struct TestStep {
|
struct TestStep {
|
||||||
|
|
@ -6,46 +7,27 @@ struct TestStep {
|
||||||
args: &'static [&'static str],
|
args: &'static [&'static str],
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASIC_TESTS: &[TestStep] = &[
|
const TEST_STEPS: &[TestStep] = &[
|
||||||
TestStep {
|
TestStep {
|
||||||
label: "root crate",
|
label: "strict workspace lint",
|
||||||
args: &["test"],
|
args: &["lint"],
|
||||||
},
|
},
|
||||||
TestStep {
|
TestStep {
|
||||||
label: "telemetry",
|
label: "all Rust tests (60s per-test timeout)",
|
||||||
args: &["test", "-p", "telemetry"],
|
args: &["nextest", "run", "--workspace", "--all-features"],
|
||||||
},
|
},
|
||||||
TestStep {
|
TestStep {
|
||||||
label: "distribution",
|
label: "all Rust doctests",
|
||||||
args: &["test", "-p", "distribution"],
|
args: &[
|
||||||
},
|
"test",
|
||||||
TestStep {
|
"--workspace",
|
||||||
label: "iroh-driver",
|
"--all-features",
|
||||||
args: &["test", "-p", "iroh-driver"],
|
"--doc",
|
||||||
},
|
"--exclude",
|
||||||
TestStep {
|
"python",
|
||||||
label: "myelin",
|
"--exclude",
|
||||||
args: &["test", "-p", "myelin"],
|
"wasm-runtime",
|
||||||
},
|
],
|
||||||
TestStep {
|
|
||||||
label: "swactor-process",
|
|
||||||
args: &["test", "-p", "swactor-process"],
|
|
||||||
},
|
|
||||||
TestStep {
|
|
||||||
label: "swactor-transport",
|
|
||||||
args: &["test", "-p", "swactor-transport"],
|
|
||||||
},
|
|
||||||
TestStep {
|
|
||||||
label: "dashboard",
|
|
||||||
args: &["test", "-p", "dashboard"],
|
|
||||||
},
|
|
||||||
TestStep {
|
|
||||||
label: "swactor-vastai",
|
|
||||||
args: &["test", "-p", "swactor-vastai"],
|
|
||||||
},
|
|
||||||
TestStep {
|
|
||||||
label: "xtask",
|
|
||||||
args: &["test", "-p", "xtask"],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -65,16 +47,20 @@ COMMANDS:
|
||||||
Run the visual provisioning-reconciler demo.
|
Run the visual provisioning-reconciler demo.
|
||||||
check-telemetry-isolation
|
check-telemetry-isolation
|
||||||
Verify no frame types appear in control-plane modules.
|
Verify no frame types appear in control-plane modules.
|
||||||
test Run the non-binding repository test barrier."
|
test Run strict lint plus every Rust and Python test."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_step(step: &TestStep) -> bool {
|
fn run_step(step: &TestStep, python: &Path) -> bool {
|
||||||
println!("\n=== {} ===", step.label);
|
println!("\n=== {} ===", step.label);
|
||||||
println!(" cargo {}", step.args.join(" "));
|
println!(" cargo {}", step.args.join(" "));
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
match swactor_process::command_status(Command::new(cargo_bin()).args(step.args)) {
|
match swactor_process::command_status(
|
||||||
|
Command::new(cargo_bin())
|
||||||
|
.args(step.args)
|
||||||
|
.env("PYO3_PYTHON", python),
|
||||||
|
) {
|
||||||
Ok(status) => status.success(),
|
Ok(status) => status.success(),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Failed to execute cargo: {error}");
|
eprintln!("Failed to execute cargo: {error}");
|
||||||
|
|
@ -85,8 +71,17 @@ fn run_step(step: &TestStep) -> bool {
|
||||||
|
|
||||||
fn run_tests() -> ExitCode {
|
fn run_tests() -> ExitCode {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
for (index, step) in BASIC_TESTS.iter().enumerate() {
|
let Some(python) = PythonTestTools::discover() else {
|
||||||
if !run_step(step) {
|
return ExitCode::from(1);
|
||||||
|
};
|
||||||
|
if !nextest_available() {
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
|
if !check_telemetry_isolation() {
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
|
for (index, step) in TEST_STEPS.iter().enumerate() {
|
||||||
|
if !run_step(step, &python.python) {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"\n--- FAILED after {:.1}s ({index} passed, 1 failed) ---",
|
"\n--- FAILED after {:.1}s ({index} passed, 1 failed) ---",
|
||||||
start.elapsed().as_secs_f64()
|
start.elapsed().as_secs_f64()
|
||||||
|
|
@ -94,18 +89,99 @@ fn run_tests() -> ExitCode {
|
||||||
return ExitCode::from(1);
|
return ExitCode::from(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !python.run() {
|
||||||
|
eprintln!(
|
||||||
|
"\n--- FAILED after {:.1}s (Python tests failed) ---",
|
||||||
|
start.elapsed().as_secs_f64()
|
||||||
|
);
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"\n--- All {} step(s) passed in {:.1}s ---",
|
"\n--- All {} step(s) passed in {:.1}s ---",
|
||||||
BASIC_TESTS.len(),
|
TEST_STEPS.len() + 1,
|
||||||
start.elapsed().as_secs_f64()
|
start.elapsed().as_secs_f64()
|
||||||
);
|
);
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn nextest_available() -> bool {
|
||||||
|
let available = swactor_process::command_status(
|
||||||
|
Command::new(cargo_bin())
|
||||||
|
.args(["nextest", "--version"])
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null()),
|
||||||
|
)
|
||||||
|
.is_ok_and(|status| status.success());
|
||||||
|
if !available {
|
||||||
|
eprintln!("cargo-nextest is required; install it from https://nexte.st/docs/installation/");
|
||||||
|
}
|
||||||
|
available
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PythonTestTools {
|
||||||
|
directory: PathBuf,
|
||||||
|
maturin: PathBuf,
|
||||||
|
python: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PythonTestTools {
|
||||||
|
fn discover() -> Option<Self> {
|
||||||
|
let directory = workspace_root().join("crates/bindings/python");
|
||||||
|
let tools = Self {
|
||||||
|
maturin: directory.join(".venv/bin/maturin"),
|
||||||
|
python: directory.join(".venv/bin/python"),
|
||||||
|
directory,
|
||||||
|
};
|
||||||
|
if tools.maturin.is_file() && tools.python.is_file() {
|
||||||
|
Some(tools)
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Python test environment is missing; run `uv sync --project {}` first",
|
||||||
|
tools.directory.display()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self) -> bool {
|
||||||
|
println!("\n=== all Python tests ===");
|
||||||
|
let built = swactor_process::command_status(
|
||||||
|
Command::new(&self.maturin)
|
||||||
|
.current_dir(&self.directory)
|
||||||
|
.arg("develop")
|
||||||
|
.env("PYO3_PYTHON", &self.python),
|
||||||
|
)
|
||||||
|
.is_ok_and(|status| status.success());
|
||||||
|
if !built {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
swactor_process::command_status(
|
||||||
|
Command::new(&self.python)
|
||||||
|
.current_dir(&self.directory)
|
||||||
|
.args([
|
||||||
|
"-m",
|
||||||
|
"pytest",
|
||||||
|
"-q",
|
||||||
|
"--timeout=60",
|
||||||
|
"tests/test_bootstrap.py",
|
||||||
|
"../../../tests/test_python.py",
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.is_ok_and(|status| status.success())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> PathBuf {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.expect("xtask must live directly under the workspace root")
|
||||||
|
.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
/// Verify that control-plane modules never import telemetry frame/read-side
|
/// Verify that control-plane modules never import telemetry frame/read-side
|
||||||
/// types. They may emit through the producer API only.
|
/// types. They may emit through the producer API only.
|
||||||
fn check_telemetry_isolation() -> ExitCode {
|
fn check_telemetry_isolation() -> bool {
|
||||||
const CONTROL_DIRS: &[&str] = &[
|
const CONTROL_DIRS: &[&str] = &[
|
||||||
"apps/myelin/src/orchestration",
|
"apps/myelin/src/orchestration",
|
||||||
"crates/distribution/src",
|
"crates/distribution/src",
|
||||||
|
|
@ -150,10 +226,10 @@ fn check_telemetry_isolation() -> ExitCode {
|
||||||
"\ntelemetry-isolation: control-plane code must not import frame types \
|
"\ntelemetry-isolation: control-plane code must not import frame types \
|
||||||
or read-side modules. Use the telemetry producer API for emission."
|
or read-side modules. Use the telemetry producer API for emission."
|
||||||
);
|
);
|
||||||
ExitCode::from(1)
|
false
|
||||||
} else {
|
} else {
|
||||||
println!("telemetry-isolation: OK — no frame types in control-plane modules.");
|
println!("telemetry-isolation: OK — no frame types in control-plane modules.");
|
||||||
ExitCode::SUCCESS
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,7 +256,13 @@ mod demo;
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
match args.next().as_deref() {
|
match args.next().as_deref() {
|
||||||
Some("check-telemetry-isolation") => check_telemetry_isolation(),
|
Some("check-telemetry-isolation") => {
|
||||||
|
if check_telemetry_isolation() {
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else {
|
||||||
|
ExitCode::from(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some("test") if args.next().is_none() => run_tests(),
|
Some("test") if args.next().is_none() => run_tests(),
|
||||||
Some("demo") => demo::run(&args.collect::<Vec<_>>()),
|
Some("demo") => demo::run(&args.collect::<Vec<_>>()),
|
||||||
Some("help" | "--help" | "-h") | None => {
|
Some("help" | "--help" | "-h") | None => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue