fix(executor): spawn_blocking for sync fs I/O in speaker identity and…#637
Merged
matedev01 merged 4 commits intoJul 6, 2026
Conversation
… tegrastats reads
… fallout in handle_health
…g-for-sync-fs-io-on-shared-executor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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) andmem_available_mb()(/proc/meminfo) both ran synchronously inside async handlers oncurrent_threadruntimes with nospawn_blocking— the former on every voice follow-up utterance ingenie-core, the latter on every dashboard poll ingenie-apiand every governor tick ingenie-governor. Both now route throughtokio::task::spawn_blocking, matching the patternmemory::with_shared_memoryalready established for the same bug class.Changes
crates/genie-core/src/voice/identity.rs—SpeakerIdentityProvider::identifyandLocalBiometricRecognizer::identifyare nowasync fn; the latter moves its directory scan + per-profile file reads tospawn_blocking(owningprofile_dir/wav_path/min_scorebefore the move, since the closure must be'static).enroll_speaker_file/identify_speaker_file/list_speaker_profiles/remove_speaker_profileare left as plain sync functions — they're only ever called fromgenie-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— addedmem_available_mb_async(), aspawn_blockingwrapper around the existing synchronousmem_available_mb()(kept as-is forgenie-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 ofmem_available_mb()migrated tomem_available_mb_async().await. This turned out to be more widespread thanissue.mdscoped (it named onlygenie-api/routes.rs):genie-governor'stick()— 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_auditandskills::load_all_with_policyare now called viaspawn_blockingfrommain(), rather than directly on the executor before the server starts.Real Behavior Proof
Tested profile / hardware (check all that apply):
jetsonraspberry_piportable_sbclaptopmacWhat I ran
What I observed
genie-core,genie-api,genie-governor,genie-common,genie-ctl,genie-healthall touched or downstream of touched crates).--no-default-featuresaxes.genie-corelib 793 passed,genie-commonlib 107 passed (1 new:mem_available_mb_async_matches_sync_version),genie-governor18 passed, no regressions anywhere else.Test plan
cargo test -p genie-core --lib voice::identity::— both existingSpeakerIdentityProvider::identifytests converted to#[tokio::test]and pass unchanged; confirms thespawn_blockingmigration 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.awaitadded.cargo test -p genie-common --lib tegrastats::— newmem_available_mb_async_matches_sync_versionproves the async wrapper returns identical results to the direct call, on the real/proc/meminfoof this machine.cargo test -p genie-governor/-p genie-api— unaffected existing tests confirm no behavior change at the call sites.Notes for reviewers
issue.mdintentionally NOT touched here:crates/genie-core/src/profile/toml_profile.rsandcrates/genie-core/src/profile/ingest.rs—profile::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.rs—Config::load()is called once at startup ingenie-core/genie-governor/genie-health/genie-api, which is genuinely low-severity (matchesissue.md's own ranking). Making itasyncwould ripple.awaitintogenie-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 theSpeakerIdentityProvider/LocalBiometricRecognizerasync wrapper (the thing actually called from the voice loop) changed.mem_available_mbfix ended up touching one more file (genie-governor/governor.rs) thanissue.mdscoped, becausegovernor.rs'stick()— the governor's main periodic loop — turned out to call the same function even more frequently than the dashboard pollissue.mdnamed. Worth a second look in case there are other undiscovered callers I missed.