fix(ollama): resolve 3 bugs preventing cron-triggered Ollama auto-start
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.
This commit is contained in:
@@ -47,11 +47,9 @@ fn is_local_url(base_url: &str) -> bool {
|
|||||||
|
|
||||||
// ── Detection (sync, fast) ──
|
// ── Detection (sync, fast) ──
|
||||||
|
|
||||||
/// Find the `ollama` binary. Checks PATH first, then well-known install
|
/// Find the `ollama` binary. Checks PATH first, then well-known locations
|
||||||
/// locations as fallback (cron jobs have a minimal PATH that typically
|
/// as fallback for cron/launchd contexts where PATH is minimal.
|
||||||
/// excludes Homebrew and other user-installed paths).
|
|
||||||
pub fn find_ollama_binary() -> Option<PathBuf> {
|
pub fn find_ollama_binary() -> Option<PathBuf> {
|
||||||
// Try PATH first (works in interactive shells)
|
|
||||||
let from_path = Command::new("which")
|
let from_path = Command::new("which")
|
||||||
.arg("ollama")
|
.arg("ollama")
|
||||||
.output()
|
.output()
|
||||||
@@ -63,12 +61,11 @@ pub fn find_ollama_binary() -> Option<PathBuf> {
|
|||||||
return from_path;
|
return from_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check well-known locations (for cron/launchd contexts)
|
|
||||||
const WELL_KNOWN: &[&str] = &[
|
const WELL_KNOWN: &[&str] = &[
|
||||||
"/opt/homebrew/bin/ollama", // macOS Apple Silicon (Homebrew)
|
"/opt/homebrew/bin/ollama",
|
||||||
"/usr/local/bin/ollama", // macOS Intel (Homebrew) / Linux manual
|
"/usr/local/bin/ollama",
|
||||||
"/usr/bin/ollama", // Linux package manager
|
"/usr/bin/ollama",
|
||||||
"/snap/bin/ollama", // Linux Snap
|
"/snap/bin/ollama",
|
||||||
];
|
];
|
||||||
|
|
||||||
WELL_KNOWN
|
WELL_KNOWN
|
||||||
@@ -77,20 +74,20 @@ pub fn find_ollama_binary() -> Option<PathBuf> {
|
|||||||
.find(|p| p.is_file())
|
.find(|p| p.is_file())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quick sync check: can we TCP-connect to Ollama's HTTP port?
|
/// TCP-connect to Ollama's port. Tries all resolved addresses (IPv4/IPv6)
|
||||||
/// Resolves the hostname from the URL (supports both local and remote hosts).
|
/// 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 {
|
pub fn is_ollama_reachable(base_url: &str) -> bool {
|
||||||
let port = extract_port(base_url);
|
let port = extract_port(base_url);
|
||||||
let host = extract_host(base_url);
|
let host = extract_host(base_url);
|
||||||
let addr_str = format!("{host}:{port}");
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(addr) = addrs.next() else {
|
addrs
|
||||||
return false;
|
.into_iter()
|
||||||
};
|
.any(|addr| TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok())
|
||||||
TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Platform-appropriate installation instructions.
|
/// 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)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct OllamaEnsureResult {
|
pub struct OllamaEnsureResult {
|
||||||
/// Whether the `ollama` binary was found.
|
|
||||||
pub installed: bool,
|
pub installed: bool,
|
||||||
/// Whether Ollama was already running before we tried anything.
|
|
||||||
pub was_running: bool,
|
pub was_running: bool,
|
||||||
/// Whether we successfully spawned `ollama serve`.
|
|
||||||
pub started: bool,
|
pub started: bool,
|
||||||
/// Whether Ollama is reachable now (after any start attempt).
|
|
||||||
pub running: bool,
|
pub running: bool,
|
||||||
/// Error message if something went wrong.
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
/// Installation instructions (set when ollama is not installed).
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub install_hint: Option<String>,
|
pub install_hint: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure Ollama is running. If not installed, returns error with install
|
/// Ensure Ollama is available. For local URLs, spawns `ollama serve` if
|
||||||
/// instructions. If installed but not running, attempts to start it.
|
/// not already running. For remote URLs, only checks reachability.
|
||||||
///
|
///
|
||||||
/// Only attempts to start `ollama serve` when the configured URL points at
|
/// Waits briefly (5s) for hot restarts; cold starts finish during the
|
||||||
/// localhost. For remote URLs, only checks reachability.
|
/// ingestion phase (~90s) before the embed stage needs Ollama.
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult {
|
pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult {
|
||||||
let is_local = is_local_url(base_url);
|
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 binary_path = if is_local {
|
||||||
let path = find_ollama_binary();
|
let path = find_ollama_binary();
|
||||||
if path.is_none() {
|
if path.is_none() {
|
||||||
@@ -156,7 +140,6 @@ pub fn ensure_ollama(base_url: &str) -> OllamaEnsureResult {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Step 2: Already running?
|
|
||||||
if is_ollama_reachable(base_url) {
|
if is_ollama_reachable(base_url) {
|
||||||
return OllamaEnsureResult {
|
return OllamaEnsureResult {
|
||||||
installed: true,
|
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 {
|
if !is_local {
|
||||||
return OllamaEnsureResult {
|
return OllamaEnsureResult {
|
||||||
installed: true, // unknown, but irrelevant for remote
|
installed: true,
|
||||||
was_running: false,
|
was_running: false,
|
||||||
started: false,
|
started: false,
|
||||||
running: 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)
|
// Spawn using the absolute path (cron PATH won't include Homebrew etc.)
|
||||||
// Using the absolute path is critical — cron has a minimal PATH that
|
let ollama_bin = binary_path.expect("binary_path is Some for local URLs after binary check");
|
||||||
// typically excludes Homebrew and other user-installed locations.
|
|
||||||
let ollama_bin = binary_path.expect("binary_path is Some for local URLs after step 1");
|
|
||||||
let spawn_result = Command::new(&ollama_bin)
|
let spawn_result = Command::new(&ollama_bin)
|
||||||
.arg("serve")
|
.arg("serve")
|
||||||
.stdout(std::process::Stdio::null())
|
.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).
|
// Brief poll for hot restarts; cold starts finish during ingestion.
|
||||||
// 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.
|
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
std::thread::sleep(Duration::from_millis(500));
|
std::thread::sleep(Duration::from_millis(500));
|
||||||
if is_ollama_reachable(base_url) {
|
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 {
|
OllamaEnsureResult {
|
||||||
installed: true,
|
installed: true,
|
||||||
was_running: false,
|
was_running: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user