Both Unix-socket clients (Korban Processor, Talmudic Engine) read a reply with a single :gen_tcp.recv/3, which returns at most one receive buffer — 1460 bytes, measured — whatever the packet mode. A bedikah of forty claims answered in 18 KB: the client decoded the first 1460 bytes, failed, reconnected, failed again, and returned an error with the retry's socket still open. The single-threaded Korban Processor then sat on that dead conversation and every later bedikah, however small, timed out at 15 s. Live, for every caller, from the first long output. Clients now read until the newline (recv_line/2, 16 MiB ceiling) in raw mode, and close the retry's socket on failure. The Korban Processor drops a connection that says nothing for 120 s, so one stuck client can no longer hold the door. tablet50_socket_line_test stands up a 60 KB-line server and asks both clients. Elixir 552/0 (L3 on and off), korban 280. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
86 lines
3.2 KiB
Elixir
86 lines
3.2 KiB
Elixir
defmodule VavPipeline.Tablet50SocketLineTest do
|
|
@moduledoc """
|
|
Tablet 50 (2026-10-06) — one reply is one line, however long.
|
|
|
|
Both socket clients read a reply with a single `:gen_tcp.recv/3`, which
|
|
returns at most one receive buffer (1460 bytes). A bedikah of forty
|
|
claims answered in 18 KB; the client decoded the first 1460 bytes,
|
|
failed, reconnected, failed again, and left the retry's socket open —
|
|
the single-threaded Korban Processor then waited on that dead
|
|
conversation and every later bedikah timed out. Live, for everyone.
|
|
|
|
This test stands up a one-line server that answers with a long line
|
|
and a short one, and asks both clients for them.
|
|
"""
|
|
|
|
use ExUnit.Case, async: false
|
|
|
|
alias VavPipeline.{KorbanProcessorSocket, TalmudicEngineSocket}
|
|
|
|
@long_pad String.duplicate("x", 60_000)
|
|
|
|
setup do
|
|
path = Path.join(System.tmp_dir!(), "tablet50_#{System.unique_integer([:positive])}.sock")
|
|
File.rm(path)
|
|
|
|
{:ok, listen} =
|
|
:gen_tcp.listen(0, [:binary, active: false, packet: :raw, ifaddr: {:local, String.to_charlist(path)}])
|
|
|
|
parent = self()
|
|
|
|
server =
|
|
spawn_link(fn ->
|
|
loop = fn loop ->
|
|
{:ok, conn} = :gen_tcp.accept(listen)
|
|
serve = fn serve ->
|
|
case read_line(conn) do
|
|
{:ok, line} ->
|
|
send(parent, {:server_got, byte_size(line)})
|
|
reply = Jason.encode!(%{"status" => "ok", "examination" => %{"pad" => @long_pad, "claims" => []}, "violations" => [], "action" => "t"})
|
|
:ok = :gen_tcp.send(conn, reply <> "\n")
|
|
serve.(serve)
|
|
_ -> :gen_tcp.close(conn)
|
|
end
|
|
end
|
|
serve.(serve)
|
|
loop.(loop)
|
|
end
|
|
loop.(loop)
|
|
end)
|
|
|
|
on_exit(fn -> Process.exit(server, :kill); File.rm(path) end)
|
|
%{path: path}
|
|
end
|
|
|
|
test "the Korban client reads a 60 KB reply whole, and the next call still works", %{path: path} do
|
|
{:ok, pid} = GenServer.start_link(KorbanProcessorSocket, [socket_path: path], name: :tablet50_korban)
|
|
on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end)
|
|
|
|
for _ <- 1..3 do
|
|
assert {:ok, %{"status" => "ok", "examination" => %{"pad" => pad}}} =
|
|
GenServer.call(:tablet50_korban, {:request, Jason.encode!(%{"action" => "examine", "output" => String.duplicate("y", 20_000)})}, 15_000)
|
|
|
|
assert byte_size(pad) == 60_000
|
|
assert_receive {:server_got, got}, 5_000
|
|
assert got > 20_000
|
|
end
|
|
end
|
|
|
|
test "the Talmudic client reads a 60 KB reply whole", %{path: path} do
|
|
{:ok, pid} = GenServer.start_link(TalmudicEngineSocket, [socket_path: path], name: :tablet50_talmudic)
|
|
on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end)
|
|
|
|
assert {:ok, %{"status" => "ok"} = reply} = GenServer.call(:tablet50_talmudic, {:request, Jason.encode!(%{"action" => "halt_check"})}, 15_000)
|
|
assert byte_size(reply["examination"]["pad"]) == 60_000
|
|
end
|
|
|
|
defp read_line(conn, acc \\ "") do
|
|
case :gen_tcp.recv(conn, 0, 5_000) do
|
|
{:ok, chunk} ->
|
|
acc = acc <> chunk
|
|
if String.ends_with?(acc, "\n"), do: {:ok, acc}, else: read_line(conn, acc)
|
|
err -> err
|
|
end
|
|
end
|
|
end
|