Answer status to the client that asked, not everyone
status is a terminal event. Broadcast, the healthcheck's answer ended any ipx fetch that was watching a scan, which stopped reading at the next probe while the scan carried on. It could not happen while status waited behind the scan; answering it at once made it happen every 30 seconds. Each connection's writer now takes private replies beside the broadcast, and the test checks another client hears nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TAC7sLVqfKmY6rsTLXzNgk
This commit is contained in:
@@ -88,7 +88,8 @@ printf '{"cmd":"fetch","force":true}\n' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/
|
|||||||
`download_error`, `torrent_deferred`, `reaped`, `reap_done`, `scan_done`, `status`, `error`.
|
`download_error`, `torrent_deferred`, `reaped`, `reap_done`, `scan_done`, `status`, `error`.
|
||||||
`scan_done`, `reap_done` and `status` are terminal: a client that asked for work stops reading
|
`scan_done`, `reap_done` and `status` are terminal: a client that asked for work stops reading
|
||||||
there. Commands run one at a time, in the order they arrive, except `status`: the socket answers it
|
there. Commands run one at a time, in the order they arrive, except `status`: the socket answers it
|
||||||
straight away, so the Docker healthcheck is never left waiting behind a scan or a download.
|
straight away, so the Docker healthcheck is never left waiting behind a scan or a download, and
|
||||||
|
answers only the client that asked, since `status` would end any other client's session.
|
||||||
|
|
||||||
Progress carries the enclosure id, without which a UI cannot tell one download from another and
|
Progress carries the enclosure id, without which a UI cannot tell one download from another and
|
||||||
ends up animating every pending row. It is throttled to whole percents. The stream is a broadcast,
|
ends up animating every pending row. It is throttled to whole percents. The stream is a broadcast,
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ database; a worker stuck on one job would still pass. Asking a daemon that downl
|
|||||||
podcasts to be idle within five seconds was never a fair test of whether it was alive. A test holds
|
podcasts to be idle within five seconds was never a fair test of whether it was alive. A test holds
|
||||||
the queue full and checks `status` still comes back.
|
the queue full and checks `status` still comes back.
|
||||||
|
|
||||||
|
The first version broadcast the answer, as the queued one had been. Timing `status` during a forced
|
||||||
|
scan in production showed the catch: `status` is a terminal event, so the `ipx fetch` watching that
|
||||||
|
scan stopped reading at the first probe and printed the status line as its last, while the scan
|
||||||
|
carried on. When `status` waited behind the scan it could never arrive first, so this had never
|
||||||
|
shown. The answer now goes only to the client that asked, and the test checks that another client
|
||||||
|
hears nothing.
|
||||||
|
|
||||||
## 2026-09-12 — Signing in through Authentik, for real
|
## 2026-09-12 — Signing in through Authentik, for real
|
||||||
|
|
||||||
Ray could not get Authentik's sign-in to reach ipx, following `docs/sso.md`, which had been written
|
Ray could not get Authentik's sign-in to reach ipx, following `docs/sso.md`, which had been written
|
||||||
|
|||||||
32
src/ipc.rs
32
src/ipc.rs
@@ -174,8 +174,9 @@ pub async fn daemon_is_live(path: &Path) -> bool {
|
|||||||
|
|
||||||
/// Answers `status` for the socket, without the worker. The worker runs one job at a time, and a
|
/// Answers `status` for the socket, without the worker. The worker runs one job at a time, and a
|
||||||
/// healthcheck left waiting behind a scan or a long download timed out and called a busy daemon
|
/// healthcheck left waiting behind a scan or a long download timed out and called a busy daemon
|
||||||
/// dead.
|
/// dead. The answer goes to the client that asked and no one else: broadcast, it ended any
|
||||||
pub type StatusFn = std::sync::Arc<dyn Fn() + Send + Sync>;
|
/// `ipx fetch` that was watching a scan, since `status` is a terminal event.
|
||||||
|
pub type StatusFn = std::sync::Arc<dyn Fn() -> Event + Send + Sync>;
|
||||||
|
|
||||||
/// Accepts connections, feeding commands to `cmds` and events from `events` back out.
|
/// Accepts connections, feeding commands to `cmds` and events from `events` back out.
|
||||||
pub async fn serve(
|
pub async fn serve(
|
||||||
@@ -218,9 +219,17 @@ async fn handle(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (read, mut write) = stream.into_split();
|
let (read, mut write) = stream.into_split();
|
||||||
|
|
||||||
// Events out.
|
// Events out: everything broadcast, and the answers meant for this client alone.
|
||||||
|
let (reply, mut replies) = mpsc::channel::<Event>(4);
|
||||||
let writer = tokio::spawn(async move {
|
let writer = tokio::spawn(async move {
|
||||||
while let Ok(ev) = rx.recv().await {
|
loop {
|
||||||
|
let ev = tokio::select! {
|
||||||
|
Some(ev) = replies.recv() => ev,
|
||||||
|
got = rx.recv() => match got {
|
||||||
|
Ok(ev) => ev,
|
||||||
|
Err(_) => break,
|
||||||
|
},
|
||||||
|
};
|
||||||
let mut line = serde_json::to_string(&ev).unwrap_or_default();
|
let mut line = serde_json::to_string(&ev).unwrap_or_default();
|
||||||
line.push('\n');
|
line.push('\n');
|
||||||
if write.write_all(line.as_bytes()).await.is_err() {
|
if write.write_all(line.as_bytes()).await.is_err() {
|
||||||
@@ -240,7 +249,11 @@ async fn handle(
|
|||||||
// Answered here, not queued behind whatever the worker is on: see StatusFn.
|
// Answered here, not queued behind whatever the worker is on: see StatusFn.
|
||||||
Ok(Command::Status) => {
|
Ok(Command::Status) => {
|
||||||
tracing::info!(target: "ipx::io", "-> {line}");
|
tracing::info!(target: "ipx::io", "-> {line}");
|
||||||
status();
|
let ev = status();
|
||||||
|
if let Ok(json) = serde_json::to_string(&ev) {
|
||||||
|
tracing::info!(target: "ipx::io", "<- {json}");
|
||||||
|
}
|
||||||
|
let _ = reply.send(ev).await;
|
||||||
}
|
}
|
||||||
Ok(cmd) => {
|
Ok(cmd) => {
|
||||||
if cmds.send(cmd).await.is_err() {
|
if cmds.send(cmd).await.is_err() {
|
||||||
@@ -350,10 +363,10 @@ mod tests {
|
|||||||
let (cmds, _worker) = mpsc::channel::<Command>(1);
|
let (cmds, _worker) = mpsc::channel::<Command>(1);
|
||||||
cmds.send(Command::Reap { dry_run: true }).await.unwrap();
|
cmds.send(Command::Reap { dry_run: true }).await.unwrap();
|
||||||
let (events, _) = broadcast::channel::<Event>(8);
|
let (events, _) = broadcast::channel::<Event>(8);
|
||||||
let tx = events.clone();
|
// Another client, watching a scan: it must not be handed someone else's answer, which
|
||||||
let status: StatusFn = std::sync::Arc::new(move || {
|
// would end its session.
|
||||||
let _ = tx.send(Event::Status { feeds: 1, pending: 2, downloaded: 3 });
|
let mut watcher = events.subscribe();
|
||||||
});
|
let status: StatusFn = std::sync::Arc::new(|| Event::Status { feeds: 1, pending: 2, downloaded: 3 });
|
||||||
let (client, server) = UnixStream::pair().unwrap();
|
let (client, server) = UnixStream::pair().unwrap();
|
||||||
tokio::spawn(handle(server, events.subscribe(), cmds, status));
|
tokio::spawn(handle(server, events.subscribe(), cmds, status));
|
||||||
|
|
||||||
@@ -365,5 +378,6 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(line.contains(r#""ev":"status""#), "{line}");
|
assert!(line.contains(r#""ev":"status""#), "{line}");
|
||||||
|
assert!(watcher.try_recv().is_err(), "the answer went to every client, not just the one asking");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ async fn daemon(
|
|||||||
// status is answered by the socket itself; everything else waits its turn in the queue.
|
// status is answered by the socket itself; everything else waits its turn in the queue.
|
||||||
let answer: ipc::StatusFn = {
|
let answer: ipc::StatusFn = {
|
||||||
let ctx = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
Arc::new(move || ctx.out.emit(status(&ctx)))
|
Arc::new(move || status(&ctx))
|
||||||
};
|
};
|
||||||
let server = tokio::spawn(ipc::serve(socket.clone(), events.clone(), tx_cmd, answer));
|
let server = tokio::spawn(ipc::serve(socket.clone(), events.clone(), tx_cmd, answer));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user