Serverless function using Frappe Framework

Using no dependency

Just serve the current gunicorn ... frappe.app:application setup and write a whitelisted function. It will serve ALL ERPNext ReST API. Function and response cannot be isolated.

Using FastAPI

FastAPI using Frappe Framework

Use example to build serverless function using frappe framework.

Use any framework or library of your choice instead of FastAPI.

Prerequisites

  • Core development happens with standard bench start.
  • Already running frappe-bench like setup.
  • Connection to same mariadb, redis that the frappe-bench connects to.
  • Access to sites directory of the frappe-bench
  • Valid site to set it as SITE_NAME environment variable.

Installation

Install as python app in frappe-bench python env

cd ~/frappe-bench
git clone https://github.com/castlecraft/fast_frappe apps/fast_frappe
./env/bin/pip install -e apps/fast_frappe

Serve

Using uvicorn

cd ~/frappe-bench
. ./env/bin/activate

# Execute app from sites directory
cd ~/frappe-bench/sites
# Set SITE_NAME to use for the function
SITE_NAME=function.local uvicorn fast_frappe.main:app --port 3000

Check Response

curl -s http://localhost:3000 | jq .

Containerized

Build

cd ~/frappe-bench/apps/fast_frappe
docker build -t fast_frappe:latest .

Run

docker run -v /path/to/sites:/home/frappe/frappe-bench/sites --publish 3000:3000 fast_frappe:latest

Description

The app consists of 2 files:

main.py:

import frappe
from typing import Optional

from fastapi import FastAPI
from fast_frappe.ctrl import init_frappe, destroy_frappe

app = FastAPI()


@app.get("/")
def read_root():
    init_frappe()
    available_doctypes = frappe.get_list("DocType")
    settings = frappe.get_single("System Settings")
    destroy_frappe()
    return {
        "available_doctypes": available_doctypes,
        "settings": settings.as_dict(),
    }

and ctrl.py:

import os

import frappe


def init_frappe():
    site = os.environ.get("SITE_NAME", "test.localhost")
    frappe.init(site=site)
    frappe.connect()


def destroy_frappe():
    frappe.destroy()
20 Likes

Hi Sir, any performance comparison tested yet vs Werkzeug?
Thanks

frappe whitelisted function
@frappe.whitelist(allow_guest=True)
def get_status():
	return {
		"dt": frappe.get_all("DocType"),
		"settings": frappe.get_single("System Settings"),
	}

ab output

ab -n 1000 -c 100 http://test.localhost:8000/api/method/frappe.client.get_status
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test.localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Werkzeug/0.16.1
Server Hostname:        test.localhost
Server Port:            8000

Document Path:          /api/method/frappe.client.get_status
Document Length:        8187 bytes

Concurrency Level:      100
Time taken for tests:   63.310 seconds
Complete requests:      1000
Failed requests:        33
   (Connect: 0, Receive: 0, Length: 33, Exceptions: 0)
Total transferred:      8343206 bytes
HTML transferred:       7916829 bytes
Requests per second:    15.80 [#/sec] (mean)
Time per request:       6330.953 [ms] (mean)
Time per request:       63.310 [ms] (mean, across all concurrent requests)
Transfer rate:          128.70 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   1.0      0       5
Processing:   148 6200 1616.0   6169   13117
Waiting:        0 5990 1919.7   6119   13045
Total:        148 6200 1615.5   6170   13117

Percentage of the requests served within a certain time (ms)
  50%   6170
  66%   6519
  75%   6722
  80%   6835
  90%   7261
  95%   9198
  98%  11258
  99%  11975
 100%  13117 (longest request)
fastapi function
@app.get("/")
def read_root():
    init_frappe()
    available_doctypes = frappe.get_list("DocType")
    settings = frappe.get_single("System Settings")
    return {
        "available_doctypes": available_doctypes,
        "settings": settings.as_dict(),
    }

ab output

ab -n 1000 -c 100 http://test.localhost:3000/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test.localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        uvicorn
Server Hostname:        test.localhost
Server Port:            3000

Document Path:          /
Document Length:        8270 bytes

Concurrency Level:      100
Time taken for tests:   42.326 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      8416000 bytes
HTML transferred:       8270000 bytes
Requests per second:    23.63 [#/sec] (mean)
Time per request:       4232.593 [ms] (mean)
Time per request:       42.326 [ms] (mean, across all concurrent requests)
Transfer rate:          194.18 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.6      0       9
Processing:   553 4095 2738.6   3360   13011
Waiting:      543 3810 2737.6   3169   12877
Total:        553 4096 2739.9   3360   13011

Percentage of the requests served within a certain time (ms)
  50%   3360
  66%   3448
  75%   3646
  80%   3699
  90%   7169
  95%  12632
  98%  12953
  99%  12973
 100%  13011 (longest request)

No significant difference.

4 Likes

I tried gunicorn + gevent with regular frappe.app:application and got some impressive numbers!

frappe + gunicorn + gevent
/home/revant/frappe-bench/env/bin/gunicorn -c /home/revant/frappe-bench/commands/gevent_patch.py -b 0.0.0.0:8000 -t 120 --worker-class gevent frappe.app:application --preload

ab:

ab -n 1000 -c 100 http://test.localhost:8000/api/method/frappe.client.get_status
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking test.localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        gunicorn
Server Hostname:        test.localhost
Server Port:            8000

Document Path:          /api/method/frappe.client.get_status
Document Length:        8187 bytes

Concurrency Level:      100
Time taken for tests:   34.509 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      8625000 bytes
HTML transferred:       8187000 bytes
Requests per second:    28.98 [#/sec] (mean)
Time per request:       3450.935 [ms] (mean)
Time per request:       34.509 [ms] (mean, across all concurrent requests)
Transfer rate:          244.07 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.6      0       3
Processing:   142 3376 414.5   3388    4411
Waiting:      138 3376 414.5   3388    4411
Total:        142 3377 414.3   3388    4411

Percentage of the requests served within a certain time (ms)
  50%   3388
  66%   3558
  75%   3642
  80%   3738
  90%   3902
  95%   3965
  98%   4161
  99%   4333
 100%   4411 (longest request)

May be I’m doing something wrong with ab command and config.

Has anyone tried frappe + gunicorn + gevent? It seems to be fast!

2 Likes

This is so cool. @revant_one, I am so grateful for your presence in this community. You continuously show us not only what Frappe/ERPNext is but also everything it has the potential to be.

10 Likes

Synchronous (Frappe) is theoretically faster under very light loads.
Asynchronous (Fastapi) power shows when load is heavy.

https://locust.io/ may be a good way to test.

1 Like

Wow. Gevent looks promising.

May I ask the gunicorn version (19 I presumed) ? and what is gevent_patch.py does ?

Thanks.

If you use frappe_docker, use WORKER_CLASS=gevent as environment variable to erpnext-python container, it’ll load everything

https://github.com/frappe/frappe_docker/blob/develop/build/common/commands/gevent_patch.py

1 Like

Thank you for this app.
Do you know how one can leverage frappe auth in this app?
Thanks…
you’re a blessing

1 Like

Any auth which works through request headers will work.
I think even the cookie based auth should work.

check types of auth here Introduction

Is the response payload size skewing the stats?

no idea. you can try it.

Hi @revant_one and thanks for this very usefull trick
having to init then destroying the frappe instance within each view is not a good practice i think.
instead i tried to define a middleware that init frappe before the view is called then destroy it but i am getting this error RuntimeError: no object bound to db. Same way i tried to do the same via Dependancies but i am getting the same error are you an idea of what is causing this problem and how can i solve it ?
Thanks

try removing the destroy part from middleware?

Removing the destroy part is not solving the problem
here is my middleware function

@app.middleware("http")
async def setup_frappe(request: Request, call_next):
    print("INIT FRAPPE")
    deps.init_frappe()
    response = await call_next(request)
    # deps.destroy_frappe()
    return response

I’ve not used fast api a lot.

I don’t know why db is not initialized and await function is called.

make sure init is called first and then you process the request in the event loop.

1 Like

Ok i will investigate and update here if i found something.

Second question, if i want to deploy the FastAPI app in production do i have to edit the nginx conf file in order to redirect the incoming requests to the uvicorn server ? if yes could you share what to update exactly please ? when i will run bench setup nginx this will overwrite the changes right ? is there a way to solve this too ?

Thanks

In case of serverless function the gateway is provided by the serverless provider.
No need to setup nginx. As long as uvicorn is running the ISP’s gateway should do the job of providing you an https endpoint.

check Magnum for AWS https://mangum.io

1 Like

I my case i want to use FastAPI to implement a custom API on top of my frappe app as you suggested to me here moreover my bench is deployed on a dedicated server so i want to keep it simple without having to manage other VMs/Services of cloud providers
What do you think is the simplest way to deploy it giving the above ?

thanks a lot

1 Like

Is there any intention of creating a merge for this or making it an official option for frappe/ERPNext?
Especially considering this thread (which led me here):

@rmehta