SoFunction
Updated on 2025-03-02

About the pitfalls and solutions for fastapi asynchronous interface jamming

background

The development task is to use fastapi to write a communication interface for industrial equipment (PLC), which facilitates other back-end services to connect with the equipment, and abstracts the functions of the equipment for MES calls.

The communication protocol uses modbus TCP. Since fastapi is an asynchronous framework, many codes developed in synchronous functions have been ported and found exceptions. This is also a process of constantly trampling on pitfalls. After the problem is solved, you can also experience the beauty and efficiency of the asynchronous framework.

Problem details

In the view function, we need to create a socket connection and use it to communicate with plc. After completion, remember to close the connection, otherwise the asynchronous framework will be stuck and other requests will not be in.

At the beginning my code looked like this:

@("/start/{id}")
async def start(
    id:int,
    batsize:int=Body(default=15,description='Batch size'),
    noksize:int=Body(default=3,description='Maximum number of consecutive failures')
    ):

    dip = await (pk=id)   # Configure the database to query device network information    zmqconn = get_conn(,)   # Get network connection    # Send command    try:
        ret = (action='start_batch',rqargs={'batch_size':batsize,'nok_num':noksize})
    except Exception as e:
        raise HTTPException(status_code=503,detail=str(e))
    else:
        return ret

After writing it, I tested it myself and found that the command can be initiated normally, and the interface also returned the expected result. However, I clicked it a few times quickly and found that the problem came. During the sixth time, the interface was stuck and could not return, and no actions were performed in the asynchronous function, which did not appear in the previous synchronization code.

Later, after debugging, I found that the socket connection was not successfully created. I suspected that the previous connection was not closed, which caused the network blockage, so I added the code to close the connection.

@("/start/{id}")
async def start(
    id:int,
    batsize:int=Body(default=15,description='Batch size'),
    noksize:int=Body(default=3,description='Maximum number of consecutive failures')
    ):

    dip = await (pk=id)
    zmqconn = get_conn(,)
        
    try:
        ret = (action='start_batch',rqargs={'batch_size':batsize,'nok_num':noksize})
    except Exception as e:
        raise HTTPException(status_code=503,detail=str(e))
    finally:
        ()
    return ret

At this time, there is no problem of being stuck at all, and you can click the interface all the time.

Therefore, for asynchronous functions, we must pay attention to manually releasing resources, otherwise it may cause unexpected problems.

Fastapi dependency injection

But there is another problem with this. Our service often uses the previous socket framework, and other interfaces must also be used. You cannot copy and paste this code on one side of each interface.

As the best asynchronous framework, fastapi has a big feature of very demanding code reusability, which takes into account the issue of duplicate code during design.

As for how strict it is, for example, if different interfaces use the same set of parameters, then you can encapsulate this set of parameters and write it into the dependency. When using it, then your new interface will have a set of exactly the same parameters.

Here we use fastapi's dependency injection to encapsulate interface parameters and socket connections for other interfaces to call.

Depend on code

async def get_conn_dep(
    id:int = Path(description='Path parameter, device id'),
):
    dip = await (pk=id)
    try:
        zmqconn = get_conn(,)
        yield zmqconn                # Inject links into interface functions    finally:
        ()     # After the interface function is executed, it will be closed asynchronously

Our interface functions can be very simple

@("/start/{id}")
async def start(
    zmqconn = Depends(get_conn_dep),     # This line directly reuses a bunch of code and corresponding path fields, which is simply too elegant    batsize:int=Body(default=15,description='Batch size'),
    noksize:int=Body(default=3,description='Maximum number of consecutive failures')
    ):

    try:
        ret = (action='start_batch',rqargs={'batch_size':batsize,'nok_num':noksize})
    except Exception as e:
        raise HTTPException(status_code=503,detail=str(e))
    else:
        return ret

We used swagger to click on the interface and passed different parameters for testing. We found that the interface response was very smooth, and the error message we wanted was returned when handling exceptions.

Our code above uses the fastapi asynchronous interface (it is said that the performance is close to Go) and uses tortoise asynchronous ORM. The use of tortoise is almost the same as that of django-orm, but it is a pure asynchronous orm. These two are very comfortable to develop together.

  • fastapi.
  • tortoise-orm.

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.