From 577108ec6a003db9504b8376f3d88c21ccaf0765 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 08:08:54 -0300 Subject: A language server that failed a moment ago is said to have failed, with when it is tried again, not `language server for .zig not found` During the backoff after a failed spawn or handshake, ensure() answered NoServer, which the client turned into "not found" for a server that is installed. It is now Backoff, said `zls failed recently; retry in Ns`. Co-Authored-By: Claude Opus 5.5 --- src/lsp/lsp_client.zig | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/lsp/lsp_client.zig b/src/lsp/lsp_client.zig index 91899a15..a3f3a954 100644 --- a/src/lsp/lsp_client.zig +++ b/src/lsp/lsp_client.zig @@ -195,7 +195,7 @@ const wedged_strikes = 3; // consecutive timeouts before a restart const max_rows = 2000; const max_doc_bytes = 8 << 20; -const Err = error{ Dead, Timeout, Protocol, OutOfMemory, NoServer, WriteFailed }; +const Err = error{ Dead, Timeout, Protocol, OutOfMemory, NoServer, Backoff, WriteFailed }; /// Long-lived state outlives every query arena and cannot borrow the caller's /// gpa (a different one shows up in the harness than in the shell), so @@ -438,6 +438,15 @@ pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, asked: lsp.Req, o if (req.kind == .explain) return traceOut(&tr, req, out, rows, us); // No server to start for the language: said, not "did not answer". if (failure) |err| if (err == error.NoServer) return lsp.none(out, "language server for {s} not found", .{std.fs.path.extension(req.path)}); + // Installed, but it failed a moment ago: said so, and when it is tried + // again, not that it is not there. + if (failure) |err| if (err == error.Backoff) { + const c = &conns[si]; + c.lock(); + const left_ms = c.retry_after_ms -| nowMs(); + c.unlock(); + return lsp.none(out, "{s} failed recently; retry in {d}s", .{ specs[si].name, @divFloor(left_ms + 999, 1000) }); + }; if (failure) |err| return err; try out.writeAll(scratch.buffered()); } @@ -481,7 +490,7 @@ fn answer(arena: std.mem.Allocator, si: usize, req: lsp.Req, out: *std.Io.Writer } } }, - error.OutOfMemory, error.NoServer, error.WriteFailed => {}, + error.OutOfMemory, error.NoServer, error.Backoff, error.WriteFailed => {}, } return e; }; @@ -1166,7 +1175,7 @@ fn ensure(c: *Conn, si: usize, arena: std.mem.Allocator, req: lsp.Req, tr: *Trac // already said when the next try is due. if (nowMs() < c.retry_after_ms) { tr.note("STOP: {s} failed recently; retry due in {d}ms", .{ specs[si].name, c.retry_after_ms - nowMs() }); - return error.NoServer; + return error.Backoff; } // Look before forking; a missing binary must cost one probe and one @@ -2327,3 +2336,26 @@ test "Lspinfo before any query says a server is not started yet, not off" { try status(.{ .kind = .status, .path = "", .source = "", .offset = 0 }, &out.writer); try std.testing.expect(std.mem.indexOf(u8, out.written(), "not started yet (its first query starts it)") != null); } + +test "a server that failed a moment ago is said to have failed, and when it is tried again, not to be missing" { + if (getenv("PARDES_LSP_ZIG") != null) return error.SkipZigTest; + const si = specFor("/x/a.zig").?; + const c = &conns[si]; + c.lock(); + const was = .{ c.state, c.retry_after_ms }; + c.state = .dead; + c.retry_after_ms = nowMs() + 4500; + c.unlock(); + defer { + c.lock(); + c.state = was[0]; + c.retry_after_ms = was[1]; + c.unlock(); + } + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + var out: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer out.deinit(); + try query(std.testing.allocator, arena.allocator(), .{ .kind = .hover, .path = "/x/a.zig", .source = "", .offset = 0 }, &out.writer); + try std.testing.expect(std.mem.indexOf(u8, out.written(), "zls failed recently; retry in 5s") != null); +} -- cgit v1.3