Skip to content

Blocking open calls in async functions (do not merge) - #4

Open
g-fabiani4-unipi wants to merge 2 commits into
VA602AA-master:mainfrom
g-fabiani4-unipi:fix/blocking_calls
Open

Blocking open calls in async functions (do not merge)#4
g-fabiani4-unipi wants to merge 2 commits into
VA602AA-master:mainfrom
g-fabiani4-unipi:fix/blocking_calls

Conversation

@g-fabiani4-unipi

@g-fabiani4-unipi g-fabiani4-unipi commented May 8, 2026

Copy link
Copy Markdown
Contributor

The objective of this PR is only to demonstrate the problem. Once we decide on a strategy I can work on that and make a new PR.

Description of the problem

upload_graph, get_graph_summary, get_node_types_counts and get_edge_types_counts are all defined as asynchronous but call the blocking method open from python standard IO.

As per Ruff documentation on this issue ASYNC230

Blocking an async function via a blocking call will block the entire event loop, preventing it from executing other tasks while waiting for the call to complete, negating the benefits of asynchronous programming.

Troubleshooting

In 76d4cc5 I set up some logging in get_node_types_counts and get_edges_types_counts in order to check when the functions are called and when the file is opened and closed. I left the blocking open calls where there are any.

  1. Checkout commit 76d4cc5
  2. Start the server using the log configuration
uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-config=log_config.json
  1. Use first-d3-2026 as client and click the Update button
  2. Inspect the log: you should see that the callback handling the second request is called only after the response to the first request is sent
2026-05-08 10:20:14,612 - DEBUG:    (get_node_type_counts) Calling.
2026-05-08 10:20:14,612 - DEBUG:    (get_node_type_counts) Opening file.
2026-05-08 10:20:14,724 - DEBUG:    (get_node_type_counts) Closing file.
2026-05-08 10:20:14,881 - INFO:     127.0.0.1:45980 - "GET /node-types/default HTTP/1.1" 200 OK
2026-05-08 10:20:14,882 - DEBUG:    (get_edge_type_counts) Calling.
2026-05-08 10:20:14,882 - DEBUG:    (get_edge_type_counts) Opening file.
2026-05-08 10:20:14,937 - DEBUG:    (get_edge_type_counts) Closing file.
2026-05-08 10:20:15,102 - INFO:     127.0.0.1:45994 - "GET /edge-types/default HTTP/1.1" 200 OK

In 52a80a7 I used the asynchronous method anyio.open_file for opening the file in both get_node_types_counts and get_edges_types_counts.

  1. Check out commit 52a80a7
  2. Start the server using the log configuration
uvicorn main:app --reload --host 0.0.0.0 --port 8000 --log-config=log_config.json
  1. Use first-d3-2026 as client and click the Update button
  2. Inspect the log: you should see that the callback handling the second request is called immediately and that the requests are handled in parallel
2026-05-08 10:30:00,736 - DEBUG:    (get_node_type_counts) Calling.
2026-05-08 10:30:00,737 - DEBUG:    (get_edge_type_counts) Calling.
2026-05-08 10:30:00,738 - DEBUG:    (get_node_type_counts) Opening file.
2026-05-08 10:30:00,739 - DEBUG:    (get_edge_type_counts) Opening file.
2026-05-08 10:30:00,788 - DEBUG:    (get_edge_type_counts) Closing file.
2026-05-08 10:30:00,789 - DEBUG:    (get_node_type_counts) Closing file.
2026-05-08 10:30:01,039 - INFO:     127.0.0.1:43552 - "GET /edge-types/default HTTP/1.1" 200 OK
2026-05-08 10:30:01,223 - INFO:     127.0.0.1:43544 - "GET /node-types/default HTTP/1.1" 200 OK

Possible solutions

A. Remove async from path operation functions that need to use standard IO

Since standard IO has no support for await, we can simply declare path operation functions with just def and be done with it.
As per FastAPI documentation on concurrency:

Note: You can mix def and async def in your path operation functions as much as you need and define each one using the best option for you. FastAPI will do the right thing with them.

Anyway, in any of the cases above, FastAPI will still work asynchronously and be extremely fast.

But by following the steps above, it will be able to do some performance optimizations.

B. Use an equivalent asynchronous function for opening files

Instead of

async def foo():
    with open("bar.txt") as f:
        contents = f.read()

Use

import anyio


async def foo():
    async with await anyio.open_file("bar.txt") as f:
        contents = await f.read()

As partially implemented in 52a80a7.
I don't see any problems with this solution. With the exception of upload_graph we are only opening the file for reading, so we don't have to concern ourselves with race conditions. In addition to that, the file is written into only the first time and never updated, so I think that it would make sense for upload_graph to open it in exclusive creation mode:

import anyio


@app.post("/upload/", summary="Upload a NetworkX graph JSON file")
async def upload_graph(file: UploadFile = File(...)):
...
        async with await anyio.open_file(file_path, 'x') as f:
            f.write(contents)

@g-fabiani4-unipi

Copy link
Copy Markdown
Contributor Author

Just anecdotally, I analyzed the performance of my demo visualization performing the same list of operation that a user may conceivably perform during exploratory analysis (loading the initial visualization and then some filtering) with both solutions.

This is without async callbacks
Screenshot From 2026-05-12 17-14-38

And this is with asynchronous handling of IO operations
Screenshot From 2026-05-12 17-15-50

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant