diff --git a/python/tests/test_e2e.py b/python/tests/test_e2e.py index de9e927..e96d1c1 100644 --- a/python/tests/test_e2e.py +++ b/python/tests/test_e2e.py @@ -278,6 +278,23 @@ def test_daemon_restart(self, server): assert result.success assert "after" in result.stdout + def test_daemon_stays_connected_after_long_command(self, server): + """A command longer than the daemon's ack timeout (30s) must not make it reconnect + + The daemon handles a command inline, so heartbeat acks go unread while it runs. + It used to read that as an unresponsive controller and reconnect right after + answering, so the next call failed with "daemon not found". + """ + result = server.exec("daemon-debian-2", "sleep 35 && echo done", timeout=60) + assert result.success + assert "done" in result.stdout + + # No wait: a reconnecting daemon is missing from the registry for its 5s backoff. + time.sleep(2) + result = server.exec("daemon-debian-2", "echo still-here", timeout=5) + assert result.success + assert "still-here" in result.stdout + class TestE2EStats: """Test statistics with Docker daemons""" diff --git a/sandd/src/main.rs b/sandd/src/main.rs index 0499fa8..5eddcad 100644 --- a/sandd/src/main.rs +++ b/sandd/src/main.rs @@ -620,6 +620,15 @@ where { error!("Error handling message: {}", e); } + + // While the message was handled inline (an exec can run for minutes) + // this loop read nothing, so the controller's acks sat unread in the + // socket. That is OUR silence, not the controller's: without this, any + // command longer than ack_timeout made the next ack_check reconnect, + // dropping the daemon from the registry right after it answered. The + // buffered acks are read on the next iterations; a controller that + // really stopped acking is still caught one ack_timeout from now. + last_ack = Instant::now(); } } };