Search This Blog

Monday, May 13, 2024

FastAPI middleware performance

 As per the FastAPI docs, the way to create and add custom middlewares is


@app.middleware("http")
async def add_my_middlware(request: Request, call_next):
response = await call_next(request)
return response

Seems simple enough. Before the await, you can do something with the request. After the await, you can do something with the response.

But if you run benchmark, you will find something very surprising.

Saturday, May 11, 2024

Python - calling async function from a sync code flow

 Recently I ran into a scenario where I needed to call a init function for a third party library in my FastAPI application. The problem was, the function was async.

One way of calling an async function from a sync flow is using asyncio event loop.


import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(init())

But this gave event loop is already running error.

The problem is, FastAPI application is run with uvicorn which starts a loop.

So I tried creating a new loop.

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(init())

But this still didn't work as asyncio only supports one loop at a time.

The most suggested approach is to use nest-asyncio

import nest_asyncio
loop = asyncio.new_event_loop()
nest_asyncio.apply(loop)
asyncio.set_event_loop(loop)
loop.run_until_complete(init())

This raised an exception: Can't patch uvloop.Loop.

uvicorn patches asyncio to use uvloop which is better performant that vanilla asyncio (previous claims were 2x to 4x. In the simple test I ran, even with performance changes in 3.12, asyncio was about 25% slower than uvloop.).

Did some research, but couldn't find any solution around this other than forcing uvicorn to run with vanilla asyncio


uvicorn main:app --loop asyncio


It didn't make sense to take a performance hit, just to call a function.

So I decided to dig deeper into why asyncio.new_event_loop returns uvloop.Loop.

The way uvloop does it is, it sets the asyncio event_loop_policy.

This gave me an idea, what if we temporarily restore the event loop policy, get a loop, apply nest_asyncio and then restore the event loop policy.


import nest_asyncio
import asyncio
_cur_event_loop_policy = asyncio.get_event_loop_policy()
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
loop = asyncio.new_event_loop()
nest_asyncio.apply(loop) # type: ignore
asyncio.set_event_loop(loop)
result = loop.run_until_complete(init())
loop.close()
asyncio.set_event_loop_policy(_cur_event_loop_policy)


This seems to do the trick and I am able to call an async function in my FastAPI application main.py before initializing app.

Thursday, June 22, 2023

FastAPI and Swagger2

 I recently started working on a web project with Python backend. The hosting was on Google Cloud. 

 We built the POC version of the Python backend using the most popular Python3 framework FastAPI. We deployed it on Cloud Run (public) with Docker image and everything worked as expected. 

 We then moved on to making the Cloud Run private and adding an API gateway infront of it. We saved the http://localhost:8000/openapi.json and ran the gcloud cli to create an API gateway and it failed!

 Looking at the documentation https://cloud.google.com/api-gateway/docs/openapi-overview we realized, Google, for some reason, still only supports OpenAPI 2.0 (Swagger 2.0). Working on AWS for quite some time, I had assumed OpenAPI 3 would be supported.

 We tried figuring out a way to configure FastAPI to generate Swagger 2 spec. But FastAPI only supports OpenAPI 3.x. Many have suggested downloading the openapi.json, passing it through some convertor and then some manual intervention to convert it to Swagger 2. 

 Since I wanted to use CI/CD, I didn't want to deal with manual conversion every time developers made some change. So, I referred the FastAPI OpenAPI related code and came up with Package version

Requirements

Python 3.8+
FastAPI 0.79.0+

Installation

$ pip install fastapi_swagger2

Example

from typing import Union
from fastapi import FastAPI
from fastapi_swagger2 import FastAPISwagger2

app = FastAPI()
FastAPISwagger2(app)


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}


This adds following endpoints:
http://localhost:8000/swagger2.json
http://localhost:8000/swagger2/docs
http://localhost:8000/swagger2/redoc


Generate spec for CI/CD

import os

import yaml

from app.main import app

URL = os.environ["CLOUD_RUN_URL"]

app.servers.append(URL)

spec = app.swagger2()
spec['x-google-backend'] = {'address': URL}

print(yaml.dump(spec))


Monday, April 12, 2021

Run GUI applications inside Multipass (Ubuntu) on macOS

Multipass is a nice docker like vm platform from Ubuntu. It can configure the vm with cloud-init so you can prototype your cloud launches locally for free. 

E.g. Launch an instance (by default you get the current Ubuntu LTS)

$ multipass launch --name foo

You can read more about multipass in the docs.

Primarly it does seem like just another alternative to docker. But then I found one nice little difference, not a resource hog unlike docker and you don't run as root.

Currently to run a GUI application which you just want to try without having to clean up the system, most common approach is create a vm with VirtualBox, VMware Fusion or Parallels. But it can be a time consuming task as it installs everything from scratch.

Getting GUI applications to work on docker is possible configuration. But then, atleast on Mac, Docker is a resource hog.

With multipass I was able to get IntelliJ IDEA with following:

Prerequisites:

  • Install Multipass
  • Install XQuartz. Also configure XAuthLocation.
    echo "XAuthLocation /opt/X11/bin/xauth" >> ~/.ssh/config

Launch a vm with current LTS ubuntu with 4 cores CPU and 4 GB memory

$ multipass launch -n idea -c 4 -m 4G

$ multipass shell idea

$ sudo apt update

$ sudo apt install -y libxrender-dev libxtst6 libxi6 fontconfig

$ cd /tmp

$ curl -L -O https://download.jetbrains.com/idea/ideaIC-2021.1.tar.gz

$ sudo tar -xzf ideaIC-2021.1.tar.gz -C /opt

$ echo 'PATH=/opt/idea-IC-211.6693.111/bin:$PATH' >> ~/.bashrc

$ sudo apt install -y openjdk-11-jdk

You could automate the above with cloud-init. (Currently as of this writing multipass only supports yaml configs)

Now time for the magic part (on the host):

multipass shell internally is just doing a SSH

$ sudo cp '/var/root/Library/Application Support/multipassd/ssh-keys/id_rsa' ~/.ssh/multipass_id_rsa

Save this script to your PATH

$ cat ~/bin/multipass_x_ssh

 

#!/bin/bash

IP=$(multipass info $1 | grep IPv4 | awk '{ print $2 }')

ssh -X -i ~/.ssh/multipass_id_rsa ubuntu@$IP

Now when I want to run idea, all I have to do is:

$ multipass_x_ssh idea

$ idea.sh

Saturday, February 27, 2021

pyenv install error on MacOS Big Sur - sendfile invalid

There seems to be some issue going on with pyenv and MacOS Big Sur.


./Modules/posixmodule.c:8320:15: error: implicit declaration of function 'sendfile' is invalid in C99 [-Werror,-Wimplicit-function-declaration]

        ret = sendfile(in, out, offset, &sbytes, &sf, flags);


After referring multiple posts, this is what worked for me.

brew install zlib bzip2

CFLAGS="-I$(brew --prefix openssl)/include -I$(brew --prefix bzip2)/include -I$(brew --prefix readline)/include -I$(xcrun --show-sdk-path)/usr/include" LDFLAGS="-L$(brew --prefix openssl)/lib -L$(brew --prefix readline)/lib -L$(brew --prefix zlib)/lib -L$(brew --prefix bzip2)/lib" pyenv install 3.5.3 --patch < <(curl -sSL https://github.com/python/cpython/commit/8ea6353.patch\?full_index\=1)

Wednesday, February 10, 2021

Vmware Fusion 12 - Mac OS Big Sur - Could not connect Network

Was unable to get the network to work after a restart.

| vmx| I005: VNET: 'ethernet0' enable link state propagation, lsp.state = 5

| vmx| I005: DictionaryLoad: Cannot open file "/Library/Preferences/VMware Fusion/config": No such file or directory.

| vmx| I005: VNET: MACVNetMacosGetRealAdapterType: network type for adapter 0: 8

| vmx| I005: VNET: MACVNetMacosGetVnetProperties: vnet properties: vnet=vmnet8, nat=yes, dhcp=yes (ignored), subnet=172.16.82.0, mask=255.255.255.0, firstAddr=172.16.82.1, lastAddr=172.16.82.127, isIPv6=no, IPv6Prefix=fd15:4ba5:5a2b:1008::, IPv6PrefixLen=64

| vmx| I005: VNET: MACVNetPortVirtApiStartInterface: Waiting on semaphore for adapter: 0

| host-1216256| I005: VNET: MACVNetPortVirtApiStartHandler: starting interface for adapter: 0, status: 1009

| host-1216256| W003: VNET: MACVNetPortVirtApiStartHandler: unable to create virtual intrface for device: 0, status: 1009

| vmx| I005: VNET: Semaphore signalled for adapter: 0, timeoutMs=5000, waitMs=3

| vmx| W003: VNET: MACVNetPortVirtApiStartInterface: Failed to create interface for adapter: 0, handlerStatus: 1009

| vmx| I005: VNET: MACVNetPort_Connect: Ethernet0: can't start virtual interface

| vmx| I005: Msg_Post: Error

| vmx| I005: [msg.vnet.connectvnet] Could not connect 'Ethernet0' to virtual network '/dev/vmnet8'. More information can be found in the vmware.log file.

| vmx| I005: [msg.device.badconnect] Failed to connect virtual device 'Ethernet0'.

| vmx| I005: ----------------------------------------


Here's what worked for me:

  • Click on "Apple" logo in menu bar
  • Click "System Preferences"
  • Click "Sharing"
  • Disable "Internet Sharing"
  • Restart the VM.

 


Saturday, June 20, 2020

MacOS pyenv python 3.8 tkinter install


brew install pyenv

brew install tcl-tk

echo 'export PATH="$(brew --prefix tcl-tk)/bin:$PATH"' >> ~/.zshrc
export LDFLAGS="-L$(brew --prefix tcl-tk)/lib"
export CPPFLAGS="-I$(brew --prefix tcl-tk)/include"
export PKG_CONFIG_PATH="$(brew --prefix tcl-tk)/lib/pkgconfig"


PYTHON_CONFIGURE_OPTS="--with-tcltk-includes='-I$(brew --prefix tcl-tk)/include' --with-tcltk-libs='-L$(brew --prefix tcl-tk)/lib -ltcl8.6 -ltk8.6'" CFLAGS="-I$(brew --prefix tcl-tk)/include" pyenv install 3.8.3