Here is a more complete example of what I am doing.

import vibe.core.core;
import vibe.core.log;
import vibe.core.net;
import vibe.stream.tls;
import vibe.stream.operations;
import vibe.stream.wrapper;
import eventcore.core;
import core.time: seconds;
import core.stdc.signal;

void main()
{
	ConnectionStream conn;

	auto listener = runTask({
		const server_cert_path = "";
		const client_cert_path = "";
		const private_key_path = "";
		auto sslctx = createTLSContext(TLSContextKind.client);
		sslctx.useTrustedCertificateFile(server_cert_path);
		sslctx.useCertificateChainFile(client_cert_path);
		sslctx.usePrivateKeyFile(private_key_path);

		auto tcp_connection = connectTCP("example.org", 4711);
		conn = createConnectionProxyStream(
			createTLSStream(tcp_connection, sslctx, "example.org"),
			tcp_connection
		);

		auto heartbeat = runTask({
			setTimer(
				seconds(60),
				{conn.write("still alive");}, // succeeds
				true);
		});

		auto recvbuf = new ubyte[12345];
		while (true) {
			conn.read(recvbuf);
			logInfo("Still alive...");
		}
	});

	eventDriver.signals.listen(SIGINT, (id, status, sig) {
		auto l = yieldLock();
		eventDriver.signals.releaseRef(id);
		auto shutdown = runTask({
			conn.write("shutting down");
			// AssertError@eventcore/drivers/posix/driver.d(340):
			// Overwriting notification callback.
		});
	});

	runApplication();
}

Both the heartbeat and the shutdown task write to the socket (more precisely, the TLS output stream) while read is blocked in the listener task. heartbeat succeeds, shutdown fails. Any idea why that might be?

Now I use listener.interrupt() to interrupt the listener Task.

import vibe.core.core;
import vibe.core.log;
import vibe.core.net;
import vibe.stream.tls;
import vibe.stream.operations;
import vibe.stream.wrapper;
import eventcore.core;
import core.time: seconds;
import core.stdc.signal;

void main()
{
	auto listener = runTask({
		const server_cert_path = "";
		const client_cert_path = "";
		const private_key_path = "";
		auto sslctx = createTLSContext(TLSContextKind.client);
		sslctx.useTrustedCertificateFile(server_cert_path);
		sslctx.useCertificateChainFile(client_cert_path);
		sslctx.usePrivateKeyFile(private_key_path);

		auto tcp_connection = connectTCP("example.org", 4711);
		auto conn = createConnectionProxyStream(
			createTLSStream(tcp_connection, sslctx, "example.org"),
			tcp_connection
		);

		auto heartbeat = runTask({
			setTimer(
				seconds(60),
				{conn.write("still alive");},
				true);
		});

		auto recvbuf = new ubyte[12345];
		while (true) {
			// This would be wrapped in a try/catch to call
			// conn.write("shutting down") and break the loop if interrupted.
			conn.read(recvbuf);
			logInfo("Still alive...");
		}
	});

	eventDriver.signals.listen(SIGINT, (id, status, sig) {
		auto l = yieldLock();
		eventDriver.signals.releaseRef(id);
		auto shutdown = runTask({
			listener.interrupt();
			listener.join();
		});
	});

	runApplication();
}

I would expect the read call to throw InterruptException when interrupted. Instead it throws object.Exception@../../.dub/packages/vibe-d-0.8.4/vibe-d/tls/vibe/stream/openssl.d(381): Reading from TLS stream: error:80000001:lib(128):func(0):reason(1) (2147483649). Do you know what I am doing wrong?