From 3fed5a3048cea90f565733f1983ec2fa7fc56ee4 Mon Sep 17 00:00:00 2001 From: teernisse Date: Fri, 13 Mar 2026 10:59:16 -0400 Subject: [PATCH] fix(ollama): resolve 3 bugs preventing cron-triggered Ollama auto-start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. PATH blindness in cron: find_ollama_binary() used `which ollama` which fails in cron's minimal PATH (/usr/bin:/bin). Added well-known install locations (/opt/homebrew/bin, /usr/local/bin, /usr/bin, /snap/bin) as fallback. ensure_ollama() now spawns using the discovered absolute path instead of bare "ollama". 2. IPv6-first DNS resolution: is_ollama_reachable() only tried the first address from to_socket_addrs(), which on macOS is ::1 (IPv6). Ollama only listens on 127.0.0.1 (IPv4), so the check always failed. Now iterates all resolved addresses — "Connection refused" on ::1 is instant so there's no performance cost. 3. Excessive blocking on cold start: ensure_ollama() blocked for 30s waiting for readiness, then reported failure even though ollama serve was successfully spawned and still booting. Reduced wait to 5s (catches hot restarts), and reports started=true on timeout since the ~90s ingestion phase gives Ollama plenty of time to cold-start before the embed stage needs it. --- src/core/ollama_mgmt.rs | 69 +++++++++++++---------------------------- 1 file changed, 22 insertions(+), 47 deletions(-) diff --git a/src/core/ollama_mgmt.rs b/src/core/ollama_mgmt.rs index d9e43e6..63b5bb8 100644 --- a/src/core/ollama_mgmt.rs +++ b/src/core/ollama_mgmt.rs @@ -47,11 +47,9 @@ fn is_local_url(base_url: &str) -> bool { // ── Detection (sync, fast) ── -/// Find the `ollama` binary. Checks PATH first, then well-known install -/// locations as fallback (cron jobs have a minimal PATH that typically -/// excludes Homebrew and other user-installed paths). +/// Find the `ollama` binary. Checks PATH first, then well-known locations +/// as fallback for cron/launchd contexts where PATH is minimal. pub fn find_ollama_binary() -> Option { - // Try PATH first (works in interactive shells) let from_path = Command::new("which") .arg("ollama") .output() @@ -63,12 +61,11 @@ pub fn find_ollama_binary() -> Option { return from_path; } - // Fallback: check well-known locations (for cron/launchd contexts) const WELL_KNOWN: &[&str] = &[ - "/opt/homebrew/bin/ollama", // macOS Apple Silicon (Homebrew) - "/usr/local/bin/ollama", // macOS Intel (Homebrew) / Linux manual - "/usr/bin/ollama", // Linux package manager - "/snap/bin/ollama", // Linux Snap + "/opt/homebrew/bin/ollama", + "/usr/local/bin/ollama", + "/usr/bin/ollama", + "/snap/bin/ollama", ]; WELL_KNOWN @@ -77,20 +74,20 @@ pub fn find_ollama_binary() -> Option { .find(|p| p.is_file()) } -/// Quick sync check: can we TCP-connect to Ollama's HTTP port? -/// Resolves the hostname from the URL (supports both local and remote hosts). +/// TCP-connect to Ollama's port. Tries all resolved addresses (IPv4/IPv6) +/// since `localhost` resolves to `::1` first on macOS but Ollama only +/// listens on `127.0.0.1`. pub fn is_ollama_reachable(base_url: &str) -> bool { let port = extract_port(base_url); let host = extract_host(base_url); let addr_str = format!("{host}:{port}"); - let Ok(mut addrs) = addr_str.to_socket_addrs() else { + let Ok(addrs) = addr_str.to_socket_addrs() else { return false; }; - let Some(addr) = addrs.next() else { - return false; - }; - TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() + addrs + .into_iter() + .any(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok()) } /// Platform-appropriate installation instructions. @@ -104,41 +101,28 @@ pub fn install_instructions() -> &'static str { } } -// ── Ensure (sync, spawns ollama if needed) ── +// ── Ensure (spawns ollama if needed) ── -/// Result of attempting to ensure Ollama is running. #[derive(Debug, Serialize)] pub struct OllamaEnsureResult { - /// Whether the `ollama` binary was found. pub installed: bool, - /// Whether Ollama was already running before we tried anything. pub was_running: bool, - /// Whether we successfully spawned `ollama serve`. pub started: bool, - /// Whether Ollama is reachable now (after any start attempt). pub running: bool, - /// Error message if something went wrong. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Installation instructions (set when ollama is not installed). #[serde(skip_serializing_if = "Option::is_none")] pub install_hint: Option, } -/// Ensure Ollama is running. If not installed, returns error with install -/// instructions. If installed but not running, attempts to start it. +/// Ensure Ollama is available. For local URLs, spawns `ollama serve` if +/// not already running. For remote URLs, only checks reachability. /// -/// Only attempts to start `ollama serve` when the configured URL points at -/// localhost. For remote URLs, only checks reachability. -/// -/// After spawning, waits only briefly (5 seconds) for hot restarts. Cold -/// starts can take 30-60 seconds, but the embed stage runs much later -/// (after ingestion, typically 60-90s) and will find Ollama ready by then. -/// This avoids blocking the sync pipeline unnecessarily. +/// Waits briefly (5s) for hot restarts; cold starts finish during the +/// ingestion phase (~90s) before the embed stage needs Ollama. pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult { let is_local = is_local_url(base_url); - // Step 1: Is the binary installed? (only relevant for local) let binary_path = if is_local { let path = find_ollama_binary(); if path.is_none() { @@ -156,7 +140,6 @@ pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult { None }; - // Step 2: Already running? if is_ollama_reachable(base_url) { return OllamaEnsureResult { installed: true, @@ -168,10 +151,9 @@ pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult { }; } - // Step 3: For remote URLs, we can't start ollama — just report unreachable if !is_local { return OllamaEnsureResult { - installed: true, // unknown, but irrelevant for remote + installed: true, was_running: false, started: false, running: false, @@ -182,10 +164,8 @@ pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult { }; } - // Step 4: Try to start it (local only, using discovered absolute path) - // Using the absolute path is critical — cron has a minimal PATH that - // typically excludes Homebrew and other user-installed locations. - let ollama_bin = binary_path.expect("binary_path is Some for local URLs after step 1"); + // Spawn using the absolute path (cron PATH won't include Homebrew etc.) + let ollama_bin = binary_path.expect("binary_path is Some for local URLs after binary check"); let spawn_result = Command::new(&ollama_bin) .arg("serve") .stdout(std::process::Stdio::null()) @@ -203,10 +183,7 @@ pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult { }; } - // Step 5: Brief wait for hot restarts (5 seconds). - // Cold starts take 30-60s but we don't block for that — ingestion runs - // for 60-90s before the embed stage needs Ollama, giving it plenty of - // time to boot in the background. + // Brief poll for hot restarts; cold starts finish during ingestion. for _ in 0..10 { std::thread::sleep(Duration::from_millis(500)); if is_ollama_reachable(base_url) { @@ -221,8 +198,6 @@ pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult { } } - // Spawn succeeded but Ollama is still starting up — report as started - // (not an error). It should be ready by the time the embed stage runs. OllamaEnsureResult { installed: true, was_running: false,