Skip to content

Keep the daemon off the main thread. - #21

Draft
im-ian wants to merge 1 commit into
mainfrom
desktop-commands-off-the-main-thread
Draft

im-ian wants to merge 1 commit into
mainfrom
desktop-commands-off-the-main-thread

Conversation

@im-ian

@im-ian im-ian commented Sep 21, 2026

Copy link
Copy Markdown
Owner

TL;DR

데몬을 기다리는 Tauri command 32개가 메인 스레드에서 돌고 있었어요. #[tauri::command(async)] 로 옮기고, 규칙이 다시 무너지지 않게 테스트로 고정했어요.

원인

#[tauri::command] 로 선언된 command 는 메인 스레드에서 실행돼요. 그중

  • 30개가 client::rpc 로 데몬 왕복을 기다려요
  • 그중 12개는 client::ensure_daemon() 까지 불러요 — 데몬을 spawn 하고 최대 4초 기다려요

#19 이전엔 데몬이 안 뜨면 앱이 아예 안 떠서 이 상태에 도달할 수 없었어요. 이제 창이 뜨니까, 데몬이 못 뜨는 상태에서 "새 봇" 을 누르면 클릭 한 번에 4초씩 창 전체가 멈춰요 — 그 이유를 설명하는 배너까지 같이 얼어요.

고친 것

  • 데몬을 만지는 command 30개 + 자식 프로세스를 기다리는 list_models, open_path → #[tauri::command(async)].
  • 시그니처는 그대로라, add_channel 이 list_channels() 를 Rust 함수로 직접 부르는 기존 호출부도 그대로 동작해요.
  • 로컬 파일/메모리/스킬만 만지는 13개는 sync 로 남겨요.

규칙을 테스트로 고정 — 토큰 하나 빠지면 다시 깨지는데, "데몬이 느릴 때 UI 가 멈춘다" 는 증상은 누락된 어트리뷰트처럼 안 보여요. no_command_waits_on_the_daemon_from_the_main_thread 가 이 파일을 직접 읽어서 위반한 command 이름을 뱉어요.

검증

  • cargo test --locked -p crew — 212 passed
  • 뮤테이션 확인: list_agents 를 sync 로 되돌리면 → FAILED, these talk to the daemon on the main thread; declare them #[tauri::command(async)]: ["list_agents"]
  • npm test — 130 passed / npm run build 통과
  • cargo fmt --check — desktop.rs clean
  • 실제 실행: 빈 설정으로 앱 기동 → app=ALIVE ready=YES, stderr 없음 (async 배선이 깨졌으면 전부 실패했을 경로)

참고

ensure_daemon 의 4초 대기 자체는 안 건드렸어요 — 그건 #20 의 readiness 처리와 Child 회수(다음 PR) 쪽 얘기예요. 이 PR 은 "기다리더라도 창은 계속 그려진다" 까지만 해요.

A `#[tauri::command]` runs on the main thread. Thirty of them wait on a
daemon round-trip, and twelve of those call `ensure_daemon`, which
spawns a daemon and waits up to four seconds for it. Until the window
opened without a daemon this was hard to reach; now clicking "New bot"
against a daemon that will not start freezes everything — the banner
explaining the failure included — for the whole deadline, once per
click.

They are `#[tauri::command(async)]` now, which hands them to a worker
and leaves the signature alone, so the ones called directly as Rust
functions still are. `list_models` and `open_path` wait on a child
process and join them.

The rule is one missing token away from being broken again, and a window
that stops repainting while a daemon is slow does not read as a missing
attribute. `no_command_waits_on_the_daemon_from_the_main_thread` reads
this file and fails with the names.
@im-ian
im-ian marked this pull request as draft September 21, 2026 04:57
@im-ian

im-ian commented Sep 21, 2026

Copy link
Copy Markdown
Owner Author

리뷰 결과 머지 보류. async 전환이 메인 스레드 직렬화라는 암묵적 불변식을 제거하는데, 세 곳이 거기에 기대고 있었어요.

  • refreshList 의 Promise.all([listAgents, listChannels]) 가 이제 ensure_daemon 을 동시에 두 번 불러요. ensure_daemon 에 락이 없어서 버전 불일치 경로에서 두 스레드가 각각 stop_daemon() → spawn 하고, 한쪽이 다른 쪽의 bind 된 소켓을 unlink 해요. 데몬 2개.
  • #[tauri::command(async)] 는 async_runtime::spawn 으로 가요 — blocking pool 이 아니라 async worker 라서, blocking 바디가 워커를 점유해요. 코어가 적으면 daemon_ping 까지 큐에 밀려 창이 결국 멈춰요. 필요한 건 spawn_blocking.
  • refreshMessages 에 staleness 가드가 없어서 FIFO 완료에 의존하고 있었어요. 봇 전환 중이면 이전 봇 메시지가 새 창에 그려져요.
  • 가드 테스트도 좁아요: 헬퍼 경유 호출(rpc_set_avatar)을 못 잡고, 어트리뷰트 철자가 다르면 조용히 0건으로 통과해요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant