Skip to content

os.read() blocks even after monkey patching. #973

@ethan-vanderheijden

Description

@ethan-vanderheijden

This is likely the root cause behind #634.

I thought that monkey patching the os module means all blocking functions are converted into non-blocking functions. However, while using Watchdog, I found that os.read() still block. Simple example using watchdog:

import eventlet
from eventlet.greenthread import spawn, sleep

eventlet.monkey_patch()

import logging
from watchdog.observers import Observer
from watchdog.events import LoggingEventHandler

increment = 0


def heartbeat():
    global increment
    while True:
        print('second:', increment)
        increment += 1
        sleep(1)


if __name__ == "__main__":
    spawn(heartbeat)
    sleep(2)

    logging.basicConfig(level=logging.INFO,
                        format='%(asctime)s - %(message)s',
                        datefmt='%Y-%m-%d %H:%M:%S')
    event_handler = LoggingEventHandler()
    observer = Observer(generate_full_events=True)
    observer.schedule(event_handler, '/home/ethan/test', recursive=True)
    observer.start()
    try:
        while observer.is_alive():
            observer.join(1)
    finally:
        observer.stop()
        observer.join()

I expect it to print out time stamps forever ("seconds: 0", "seconds: 1", "seconds: 2", "seconds: 3" ...), but once Watchdog starts, it stops printing because Watchdog blocks on the main thread.

The issue is that Watchdog spawns a new thread and reads from the INotify API. The read calls are executed through os.read(fd, size) here:
https://github.com/gorakhargosh/watchdog/blob/9f23b599f369cdd873c0cf3dd9e69567234d78fb/src/watchdog/observers/inotify_c.py#L302

Of course the read calls are redirected to the monkey patched version:

def read(fd, n):
"""read(fd, buffersize) -> string
Read a file descriptor."""
while True:
try:
return __original_read__(fd, n)
except OSError as e:
if get_errno(e) == errno.EPIPE:
return ''
if get_errno(e) != errno.EAGAIN:
raise
try:
hubs.trampoline(fd, read=True)
except hubs.IOClosed:
return ''

I believe the issue is that the monkey patched method immediately calls the original read function, which blocks because no output is available yet, before it calls trampoline. If I change the order of method calls to call trampoline first, it works fine:

def read(fd, n):
    while True:
        try:
            hubs.trampoline(fd, read=True)
        except hubs.IOClosed:
            return ''

        try:
            return __original_read__(fd, n)
        except OSError as e:
            if get_errno(e) == errno.EPIPE:
                return ''
            if get_errno(e) != errno.EAGAIN:
                raise

It makes more sense to me that Eventlet waits until data is available before calling a potentially blocking function. Is this a bug with Eventlet? If not, is os.read designed to be blocking (even after being monkey patched)?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions