cancelreader

repository·main·Indexed 41 days ago

https://github.com/muesli/cancelreader

A Go package that provides a way to interrupt blocking Read calls on readers, primarily targeting files and stdin. It allows developers to wrap an io.Reader using NewReader to obtain a Reader instance with a Cancel() method. When successfully interrupted, Read calls return ErrCanceled. Support varies by platform, with specific implementations for Linux (epoll), BSD (kqueue), Windows (os.Stdin), and other Unix-like systems using the select syscall.

Tokens
3.3K
Snippets
17
Records
19
Agent score
49%

What's inside cancelreader

  1. Use CancelReader to interrupt blocking reads

    main

    The cancelreader package provides a way to interrupt blocking Read calls on an input reader.

    To use it, wrap your reader with NewReader(reader). This returns a Reader instance that includes a Cancel() method.

    Behavioral Notes:

    • Files: If the input reader is an *os.File, calling Cancel() will successfully interrupt a blocking Read call. The Read call will then return an error that can be checked using errors.Is(err, cancelreader.ErrCanceled).
    • Non-Files: If the input reader is not a File, the Cancel() method does nothing and always returns false.
    • Windows Limitation: On Windows, cancellation currently only supports os.Stdin (via WaitForMultipleObject with overlapping reads from CONIN$).
    r, err := cancelreader.NewReader(file)
    if err != nil {
        // handle error
        ...
    }
    
    // cancel after five seconds
    go func() {
        time.Sleep(5 * time.Second)
        r.Cancel()
    }()
    
    // keep reading
    for {
        var buf [1024]byte
        _, err := r.Read(buf[:])
    
        if errors.Is(err, cancelreader.ErrCanceled) {
            fmt.Println("canceled!")
            break
        }
        if err != nil {
            // handle other errors
            ...
        }
    
        // handle data
        ...
    }
  2. How the select-based CancelReader works

    main

    The selectCancelReader implementation uses the POSIX select syscall to allow a blocking Read call to be interrupted by a cancellation signal.

    It works by creating an internal os.Pipe. When Cancel() is called, a byte is written to the pipe's writer. The Read method uses unix.Select to monitor both the target file descriptor and the pipe's reader descriptor. If the pipe's descriptor becomes ready, the Read method recognizes this as a cancellation signal and returns ErrCanceled.

    Limitations:

    • This implementation is only available on solaris, darwin, freebsd, netbsd, openbsd, or dragonfly.
    • It only works if the input reader implements the File interface and its file descriptor is less than unix.FD_SETSIZE (typically 1024). If these conditions are not met, it falls back to a different implementation.
  3. Cancel a blocking Read using CancelReader

    main

    To interrupt a blocking read operation, use a CancelReader. When Cancel() is invoked, the subsequent or currently blocking Read call will return 0, ErrCanceled.

    Note: The underlying reader must satisfy the File interface (providing an Fd() method) for the cancellation to successfully interrupt the syscall via select.

    // Assuming 'r' is a CancelReader and 'file' is an io.Reader that implements File
    // err := r.Read(data)
    // if errors.Is(err, cancelreader.ErrCanceled) {
    //     // Handle cancellation
    // }
    // 
    // r.Cancel()
  4. Create a cancelable reader with NewReader

    main

    Use NewReader to wrap an existing reader and obtain a version that supports cancellation.

    Signature: NewReader(r io.Reader) (Reader, error)

    Returns:

    • A Reader which implements io.Reader and provides a Cancel() method.
    • An error if the reader cannot be wrapped.
    r, err := cancelreader.NewReader(file)
  5. Check for cancellation errors with ErrCanceled

    main

    When a Read call is successfully interrupted by Cancel(), it returns an error. Use errors.Is with cancelreader.ErrCanceled to detect this specific state.

    if errors.Is(err, cancelreader.ErrCanceled) {
        // The read was successfully interrupted
    }
  6. CancelReader interface methods

    main

    The CancelReader (implemented by selectCancelReader) provides the following methods to manage lifecycle and cancellation:

    • Read(data []byte) (int, error): Reads data from the underlying reader. If the reader is canceled via Cancel(), this returns 0, ErrCanceled.
    • Cancel() bool: Sends a signal to interrupt the current Read operation. Returns true if the signal was sent successfully.
    • Close() error: Closes the internal pipes used for signaling. Should be called to clean up resources.
  7. Read data from a CancelReader

    main

    The Read(data []byte) method reads data into the provided slice.

    If the reader has been canceled (either before the call or during the wait), it returns 0 and ErrCanceled. Otherwise, it returns the number of bytes read and any error encountered.

    // data is the buffer to fill
    n, err := reader.Read(data)
    if err == cancelreader.ErrCanceled {
    	// Handle cancellation
    }
  8. Initialize a CancelReader with NewReader

    main

    Use NewReader(reader io.Reader) to create a reader that supports cancellation.

    On Windows, if the provided reader is a File with the same file descriptor as os.Stdin, the returned CancelReader can interrupt blocking read calls. In this specific case, the Cancel() method returns true if the cancellation was successful.

    If the input reader is not os.Stdin, the implementation falls back to a standard reader where Cancel() does nothing and always returns false.

    // Example usage for interrupting os.Stdin
    reader, err := cancelreader.NewReader(os.Stdin)
    if err != nil {
    	log.Fatal(err)
    }
    defer reader.Close()
    
    // In another goroutine or via signal handler:
    // reader.Cancel() will interrupt the blocking Read call below
    
    buf := make([]byte, 1024)
    n, err := reader.Read(buf)
  9. Cancel an ongoing Read call

    main

    The Cancel() method marks the reader as canceled and attempts to interrupt any ongoing Read() calls.

    Windows Specific Behavior: On Windows Terminal, WaitForMultipleObjects may sometimes return immediately without input being available. If this occurs, a graceful cancellation of an ongoing read is not possible, and Cancel() will return false.

    Return Value:

    • true: The cancellation of the ongoing Read() was successful.
    • false: The cancellation failed (e.g., due to the Windows Terminal behavior mentioned above).
    success := reader.Cancel()
    if !success {
    	// Handle case where cancellation didn't interrupt the current Read
    }
  10. Use the File interface for file descriptor access

    main

    The File interface extends io.ReadWriteCloser and provides access to low-level file information, specifically the file descriptor and the file name.

    type File interface {
    	io.ReadWriteCloser
    	Fd() uintptr
    	Name() string
    }
  11. Create a CancelReader with NewReader

    main

    Use NewReader to wrap an io.Reader with a CancelReader. This allows you to interrupt blocking read operations.

    Behavioral Constraints:

    • If the input reader is a *os.File, the returned cancel function can interrupt a blocking read call. The cancel function returns true if the cancellation was successful.
    • If the input reader is not a *os.File, or if the file descriptor is 1024 or larger, the cancel function will do nothing and always return false.
    • On Unix-like systems (specifically Solaris in this implementation), the mechanism is based on the posix select syscall.
    reader, err := cancelreader.NewReader(myFile)
    if err != nil {
        // handle error
    }
    
    // To cancel a blocking read:
    success := reader.Cancel()
  12. Create a non-cancellable reader with NewReader

    main

    When running on platforms that do not support native cancellation (such as certain non-standard operating systems), NewReader provides a fallback implementation. It returns a CancelReader that wraps an io.Reader but does not actually support the cancellation functionality. This ensures that code expecting a CancelReader interface can still run, even if it cannot be interrupted.

    import "github.com/muesli/cancelreader"
    
    // reader is your standard io.Reader
    // cancelReader will satisfy the CancelReader interface but won't support cancellation
    cancelReader, err := cancelreader.NewReader(reader)