Skip to content

fix(executor): spawn_blocking for sync fs I/O in speaker identity and…#637

Merged
matedev01 merged 4 commits into
GeniePod:mainfrom
dale053:fix/NNN-spawn-blocking-for-sync-fs-io-on-shared-executor
Jul 6, 2026
Merged

fix(executor): spawn_blocking for sync fs I/O in speaker identity and…#637
matedev01 merged 4 commits into
GeniePod:mainfrom
dale053:fix/NNN-spawn-blocking-for-sync-fs-io-on-shared-executor

Conversation

@dale053

@dale053 dale053 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #636

Fixes the two proven-live hot-path instances from issue.md's "synchronous filesystem I/O still blocks the single-threaded executor" defect, plus the three cleanly self-contained startup-only instances of the same pattern. identify() (speaker recognition) and mem_available_mb() (/proc/meminfo) both ran synchronously inside async handlers on current_thread runtimes with no spawn_blocking — the former on every voice follow-up utterance in genie-core, the latter on every dashboard poll in genie-api and every governor tick in genie-governor. Both now route through tokio::task::spawn_blocking, matching the pattern memory::with_shared_memory already established for the same bug class.

Changes

  • crates/genie-core/src/voice/identity.rsSpeakerIdentityProvider::identify and LocalBiometricRecognizer::identify are now async fn; the latter moves its directory scan + per-profile file reads to spawn_blocking (owning profile_dir/wav_path/min_score before the move, since the closure must be 'static). enroll_speaker_file/identify_speaker_file/list_speaker_profiles/remove_speaker_profile are left as plain sync functions — they're only ever called from genie-ctl's one-shot CLI commands, where blocking briefly is expected behavior, not a bug.
  • crates/genie-core/src/voice_loop.rs — both .identify(...) call sites updated with .await.
  • crates/genie-common/src/tegrastats.rs — added mem_available_mb_async(), a spawn_blocking wrapper around the existing synchronous mem_available_mb() (kept as-is for genie-ctl's CLI usage and as the function that actually does the work).
  • crates/genie-api/src/routes.rs, crates/genie-core/src/server.rs, crates/genie-core/src/tools/system.rs, crates/genie-governor/src/governor.rs — all four async call sites of mem_available_mb() migrated to mem_available_mb_async().await. This turned out to be more widespread than issue.md scoped (it named only genie-api/routes.rs): genie-governor's tick() — its main periodic loop — calls this too, at higher frequency than the dashboard poll.
  • crates/genie-core/src/main.rs — two of the six lower-severity startup-only sites fixed: security::audit::run_audit and skills::load_all_with_policy are now called via spawn_blocking from main(), rather than directly on the executor before the server starts.

Real Behavior Proof

  • I have built and run the affected code locally (or noted why I could not).
  • I have verified the change end-to-end on Jetson hardware.
  • I have NOT verified on Jetson hardware, and I explain the equivalent verification path or validation gap below.

Tested profile / hardware (check all that apply):

  • jetson
  • raspberry_pi
  • portable_sbc
  • laptop
  • mac
  • CI-only / docs-only
  • Not run locally (no Jetson device in this environment)

What I ran

cargo build --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo clippy -p genie-core -p genie-ctl --no-default-features -- -D warnings
cargo fmt --all -- --check
cargo test --workspace --locked --all-targets

What I observed

  • Build: clean across the workspace (genie-core, genie-api, genie-governor, genie-common, genie-ctl, genie-health all touched or downstream of touched crates).
  • Clippy: clean on both the default and --no-default-features axes.
  • Fmt: clean.
  • Full test suite: all green, zero failures — genie-core lib 793 passed, genie-common lib 107 passed (1 new: mem_available_mb_async_matches_sync_version), genie-governor 18 passed, no regressions anywhere else.

Test plan

  • cargo test -p genie-core --lib voice::identity:: — both existing SpeakerIdentityProvider::identify tests converted to #[tokio::test] and pass unchanged; confirms the spawn_blocking migration preserves identical matching behavior (enroll → identify → match / no-match / removal).
  • cargo test -p genie-core --lib voice_loop:: and --test voice_loop_integration — confirm the two .identify(...) call sites still compile and drive a full mock voice cycle correctly with .await added.
  • cargo test -p genie-common --lib tegrastats:: — new mem_available_mb_async_matches_sync_version proves the async wrapper returns identical results to the direct call, on the real /proc/meminfo of this machine.
  • cargo test -p genie-governor / -p genie-api — unaffected existing tests confirm no behavior change at the call sites.

Notes for reviewers

  • Scope decision — 3 of the 8 files in issue.md intentionally NOT touched here:
    • crates/genie-core/src/profile/toml_profile.rs and crates/genie-core/src/profile/ingest.rsprofile::load_profile's architecture is already being restructured on a separate branch (the subprocess-timeout-guard work, which also splits PDF extraction out of the memory-locked path). Converting these here too would duplicate that effort and risk merge conflicts; better done as part of that branch or as its own explicit follow-up once that lands.
    • crates/genie-common/src/config.rsConfig::load() is called once at startup in genie-core/genie-governor/genie-health/genie-api, which is genuinely low-severity (matches issue.md's own ranking). Making it async would ripple .await into genie-ctl's ~7 call sites too, even though blocking briefly there is correct, expected one-shot-CLI behavior, not a bug. Given the real fix (the two hot paths) is done, I didn't think this ripple was worth it for a "consistency" nice-to-have — flagging in case reviewers disagree.
  • identify_speaker_file/enroll_speaker_file/etc. staying synchronous is deliberate, not an oversight — see Changes above. Only the SpeakerIdentityProvider/LocalBiometricRecognizer async wrapper (the thing actually called from the voice loop) changed.
  • The mem_available_mb fix ended up touching one more file (genie-governor/governor.rs) than issue.md scoped, because governor.rs's tick() — the governor's main periodic loop — turned out to call the same function even more frequently than the dashboard poll issue.md named. Worth a second look in case there are other undiscovered callers I missed.

@github-actions github-actions Bot added the bug Something isn't working label Jul 5, 2026

@matedev01 matedev01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — same spawn_blocking pattern as #583/#606, moves speaker-identity synchronize fs I/O off the shared executor. 826 lib, BFCL 96%.

@matedev01 matedev01 merged commit b73822e into GeniePod:main Jul 6, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

2 participants