Try ERPNext Try Frappe Cloud Buy Support Partners Foundation

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()
12 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.

1 Like

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!

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.

4 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.

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