From a56db5cc7044a3f23455da13a5f1e72e04225bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Sun, 2 Nov 2025 15:08:53 +0100 Subject: [PATCH 1/2] add interruptible stdin to docs --- doc/other/best-practices.md | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/doc/other/best-practices.md b/doc/other/best-practices.md index 6b482403..458f57db 100644 --- a/doc/other/best-practices.md +++ b/doc/other/best-practices.md @@ -51,4 +51,45 @@ blocking methods within the forks. Virtual threads are normally not visible when using tools such as `jstack` or IntelliJ's debugger. To inspect their stack traces, you'll need to create a thread dump to a file using `jcmd [pid] Thread.dump_to_file [file]`, or use -Intellij's thread dump utility, when paused in the debugger. \ No newline at end of file +Intellij's thread dump utility, when paused in the debugger. + +## Dealing with uninterruptible stdin + +Some I/O operations, like reading from stdin, block the thread on the `read` syscall and only unblock when data becomes +available or the stream is closed. The problem with stdin specifically is that it can't be easily closed, making it +impossible to interrupt such operations directly. This pattern extends to other similar blocking operations that behave +like stdin. + +The solution is to delegate the blocking operation to a separate thread and use a [channel](../streaming/channels.md) +for communication. Since receiving from a channel is interruptible, this makes the overall operation interruptible as well: + +```scala +import ox.*, channels.* +import scala.io.StdIn + +object stdinSupport: + private lazy val chan: Channel[String] = + val rvChan = Channel.rendezvous[String] + + Thread + .ofVirtual() + .start(() => forever(rvChan.sendOrClosed(StdIn.readLine()).discard)) + + rvChan + + def readLineInterruptibly: String = + try chan.receive() + catch + case iex: InterruptedException => + // Handle interruption appropriately + throw iex +``` + +This pattern allows you to use stdin (or similar blocking operations) with ox's timeout and interruption mechanisms, +such as `timeoutOption` or scope cancellation. + +Note that for better stdin performance, you can use `Channel.buffered` instead of a rendezvous channel, or even use +`java.lang.System.in` directly and proxy raw data through the channel. Keep in mind that this solution leaks a thread +that will remain blocked on stdin for the lifetime of the application. It's possible to avoid this trade-off by using +libraries that employ JNI/JNA to access stdin, such as JLine 3, which can use raw mode with non-blocking or +timeout-based reads, allowing the thread to be properly interrupted and cleaned up. \ No newline at end of file From e33ff1b6df7179a3049c23efe9302f02f2c1f0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Bia=C5=82y?= Date: Wed, 5 Nov 2025 12:05:37 +0100 Subject: [PATCH 2/2] code review fixes --- doc/other/best-practices.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/other/best-practices.md b/doc/other/best-practices.md index 458f57db..152b4c97 100644 --- a/doc/other/best-practices.md +++ b/doc/other/best-practices.md @@ -61,7 +61,11 @@ impossible to interrupt such operations directly. This pattern extends to other like stdin. The solution is to delegate the blocking operation to a separate thread and use a [channel](../streaming/channels.md) -for communication. Since receiving from a channel is interruptible, this makes the overall operation interruptible as well: +for communication. This thread cannot be managed by Ox as Ox always attempts to run clean up when work is done and that +means interrupting all forks living in the scope that's getting shut down. Blocking I/O can't, however, be interrupted +on the JVM and the advised way of dealing with that is to just close the resource which in turn makes read/write methods +throw an `IOException`. In the case of stdin closing it is usually not what you want to do. To work around that you can +sacrifice a thread and since receiving from a channel is interruptible, this makes the overall operation interruptible as well: ```scala import ox.*, channels.* @@ -91,5 +95,5 @@ such as `timeoutOption` or scope cancellation. Note that for better stdin performance, you can use `Channel.buffered` instead of a rendezvous channel, or even use `java.lang.System.in` directly and proxy raw data through the channel. Keep in mind that this solution leaks a thread that will remain blocked on stdin for the lifetime of the application. It's possible to avoid this trade-off by using -libraries that employ JNI/JNA to access stdin, such as JLine 3, which can use raw mode with non-blocking or -timeout-based reads, allowing the thread to be properly interrupted and cleaned up. \ No newline at end of file +libraries that employ JNI/JNA to access stdin, such as [JLine 3](https://jline.org/docs/intro), which can use raw mode +with non-blocking or timeout-based reads, allowing the thread to be properly interrupted and cleaned up. \ No newline at end of file