Skip to content

Commit fd7aa9c

Browse files
authored
add interruptible stdin hint to docs (#377)
1 parent e420c24 commit fd7aa9c

File tree

1 file changed

+46
-1
lines changed

1 file changed

+46
-1
lines changed

doc/other/best-practices.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,49 @@ blocking methods within the forks.
5151

5252
Virtual threads are normally not visible when using tools such as `jstack` or IntelliJ's debugger. To inspect their
5353
stack traces, you'll need to create a thread dump to a file using `jcmd [pid] Thread.dump_to_file [file]`, or use
54-
Intellij's thread dump utility, when paused in the debugger.
54+
Intellij's thread dump utility, when paused in the debugger.
55+
56+
## Dealing with uninterruptible stdin
57+
58+
Some I/O operations, like reading from stdin, block the thread on the `read` syscall and only unblock when data becomes
59+
available or the stream is closed. The problem with stdin specifically is that it can't be easily closed, making it
60+
impossible to interrupt such operations directly. This pattern extends to other similar blocking operations that behave
61+
like stdin.
62+
63+
The solution is to delegate the blocking operation to a separate thread and use a [channel](../streaming/channels.md)
64+
for communication. This thread cannot be managed by Ox as Ox always attempts to run clean up when work is done and that
65+
means interrupting all forks living in the scope that's getting shut down. Blocking I/O can't, however, be interrupted
66+
on the JVM and the advised way of dealing with that is to just close the resource which in turn makes read/write methods
67+
throw an `IOException`. In the case of stdin closing it is usually not what you want to do. To work around that you can
68+
sacrifice a thread and since receiving from a channel is interruptible, this makes the overall operation interruptible as well:
69+
70+
```scala
71+
import ox.*, channels.*
72+
import scala.io.StdIn
73+
74+
object stdinSupport:
75+
private lazy val chan: Channel[String] =
76+
val rvChan = Channel.rendezvous[String]
77+
78+
Thread
79+
.ofVirtual()
80+
.start(() => forever(rvChan.sendOrClosed(StdIn.readLine()).discard))
81+
82+
rvChan
83+
84+
def readLineInterruptibly: String =
85+
try chan.receive()
86+
catch
87+
case iex: InterruptedException =>
88+
// Handle interruption appropriately
89+
throw iex
90+
```
91+
92+
This pattern allows you to use stdin (or similar blocking operations) with ox's timeout and interruption mechanisms,
93+
such as `timeoutOption` or scope cancellation.
94+
95+
Note that for better stdin performance, you can use `Channel.buffered` instead of a rendezvous channel, or even use
96+
`java.lang.System.in` directly and proxy raw data through the channel. Keep in mind that this solution leaks a thread
97+
that will remain blocked on stdin for the lifetime of the application. It's possible to avoid this trade-off by using
98+
libraries that employ JNI/JNA to access stdin, such as [JLine 3](https://jline.org/docs/intro), which can use raw mode
99+
with non-blocking or timeout-based reads, allowing the thread to be properly interrupted and cleaned up.

0 commit comments

Comments
 (0)