Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sa gunicorn v2 #822

Merged
merged 17 commits into from
Oct 29, 2023
208 changes: 169 additions & 39 deletions py4web/server_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
wsservers_list = []

__all__ = [
"gunicorn",
"gunicornGevent",
"gevent",
"geventWebSocketServer",
"geventWs", # short_name
Expand All @@ -22,12 +24,14 @@
# ---------------------- utils -----------------------------------------------

# export PY4WEB_LOGS=/tmp # export PY4WEB_LOGS=
def get_log_file():
def get_log_file(out_banner=True):
log_dir = os.environ.get("PY4WEB_LOGS", None)
log_file = os.path.join(log_dir, "server-py4web.log") if log_dir else None
if log_file:
print(f"log_file: {log_file}")
return log_file
if log_dir and os.path.isdir(log_dir):
log_file = os.path.join(log_dir, "server-py4web.log")
if out_banner:
print(f"log_file: {log_file}")
return log_file
return None


def check_level(level):
Expand Down Expand Up @@ -57,19 +61,23 @@ def check_level(level):
)


def logging_conf(level=logging.WARN, logger_name=__name__, test_log=False):
def logging_conf(level=logging.WARN, logger_name=__name__, fmode="w", test_log=False):

log_file = get_log_file()
log_to = dict()

if log_file:
if sys.version_info >= (3, 9):
log_to["filename"] = log_file
log_to["filemode"] = "w"
log_to["filemode"] = fmode
log_to["encoding"] = "utf-8"
else:
h = logging.FileHandler(log_file, mode="w", encoding="utf-8")
log_to.update({"handlers": [h]})
try:
h = logging.FileHandler(log_file, mode=fmode, encoding="utf-8")
log_to.update({"handlers": [h]})
except (LookupError, KeyError, ValueError) as ex:
print(f"{ex}, bad encoding {__file__}")
pass

short_msg = "%(message)s > %(threadName)s > %(asctime)s.%(msecs)03d"
# long_msg = short_msg + " > %(funcName)s > %(filename)s:%(lineno)d > %(levelname)s"
Expand All @@ -79,17 +87,13 @@ def logging_conf(level=logging.WARN, logger_name=__name__, test_log=False):

try:
logging.basicConfig(
format=short_msg,
datefmt=time_msg,
level=check_level(level),
**log_to,
format=short_msg, datefmt=time_msg, level=check_level(level), **log_to,
)
except (OSError, LookupError, KeyError, ValueError) as ex:
print(f"{ex}, {__file__}")
print(f"cannot open {log_file}")
logging.basicConfig(
format="%(message)s",
level=check_level(level),
format="%(message)s", level=check_level(level),
)

if logger_name is None:
Expand Down Expand Up @@ -119,6 +123,145 @@ def get_workers(opts, default=10):


# ---------------------- servers -----------------------------------------------
def gunicorn():

from gevent import local # pip install gevent gunicorn
import threading

# To use gevent monkey.patch_all()
# run ./py4web.py run apps -s gunicornGevent ......
if isinstance(threading.local(), local.local):
print("gunicorn: monkey.patch_all() applied")

class GunicornServer(ServerAdapter):
""" https://docs.gunicorn.org/en/stable/settings.html """

# https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/
# ./py4web.py run apps -s gunicorn --watch=off --port=8000 --ssl_cert=cert.pem --ssl_key=key.pem -w 6 -L 20
# ./py4web.py run apps -s gunicornGevent --watch=off --port=8000 --ssl_cert=cert.pem --ssl_key=key.pem -w 6 -L 20

def run(self, app_handler):
from gunicorn.app.base import BaseApplication

config = {
"bind": f"{self.host}:{self.port}",
"workers": get_workers(self.options),
"certfile": self.options.get("certfile", None),
"keyfile": self.options.get("keyfile", None),
}

if not self.quiet:

level = check_level(self.options["logging_level"])
log_file = get_log_file(out_banner=False)

logger = logging_conf(level)
log_to = "-" if log_file is None else log_file

config.update(
{
"loglevel": logging.getLevelName(level),
"access_log_format": '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"',
"accesslog": log_to,
"errorlog": log_to,
}
)

class GunicornApplication(BaseApplication):
def get_gunicorn_vars(self, env_file="gunicorn.saenv"):
result = dict()
if os.path.isfile(env_file):
try:
with open(env_file, "r") as f:
lines = f.read().splitlines()
for line in lines:
line = line.strip()
if not line or line.startswith(("#",'[')):
continue
for k in ("export ", "GUNICORN_"):
line = line.replace(k, "",1)
k, v = None, None
try:
k, v = line.split("=", 1)
except ValueError:
continue
v = v.strip()
result[k.strip().lower()] = None if v == 'None' else v
if result:
print(f"gunicorn: read {env_file}")
return result
except OSError as ex:
print(f"{ex} gunicorn: cannot read {env_file}")
for k, v in os.environ.items():
if k.startswith("GUNICORN_") and v:
key = k.split("_", 1)[1].lower()
result[key] = v
return result

def load_config(self):

"""
gunicorn.saenv

# example
# export GUNICORN_max_requests=1200
export GUNICORN_worker_tmp_dir=/dev/shm

# run as -s gunicornGevent
#worker_class=gevent
#worker_class=eventlet

# run as -s gunicorn
#worker_class=sync
[hello any text]
worker_class=gthread
workers=4
threads=8

"""


# export GUNICORN_BACKLOG=4096
# export GUNICORN_worker_connections=100

# export GUNICORN_worker_class=sync
# export GUNICORN_worker_class=gthread
# export GUNICORN_worker_tmp_dir=/dev/shm
# export GUNICORN_threads=8
# export GUNICORN_timeout=10
# export GUNICORN_max_requests=1200

#
# tested with ssep4w https://github.com/ali96343/lvsio
#
# To use gevent monkey.patch_all()
# run ./py4web.py run apps -s gunicornGevent ......
# export GUNICORN_worker_class=gevent
# export GUNICORN_worker_class=gunicorn.workers.ggevent.GeventWorker
#
# pip install gunicorn[eventlet]
# export GUNICORN_worker_class=eventlet
#
# time seq 1 5000 | xargs -I % -P 0 curl http://localhost:8000/todo &>/dev/null

gunicorn_vars = self.get_gunicorn_vars()

if gunicorn_vars:
config.update(gunicorn_vars)
print("gunicorn config:", config)

for key, value in config.items():
self.cfg.set(key, value)

def load(self):
return app_handler

GunicornApplication().run()

return GunicornServer


gunicornGevent = gunicorn


def gevent():
Expand All @@ -139,13 +282,10 @@ def gevent():

class GeventServer(ServerAdapter):
def run(self, app_handler):
logger = "default"
logger = None # "default"

if not self.quiet:
logger = logging_conf(
self.options["logging_level"],
"gevent",
)
logger = logging_conf(self.options["logging_level"], "gevent",)
# logger.addHandler(logging.StreamHandler())

certfile = self.options.get("certfile", None)
Expand Down Expand Up @@ -177,6 +317,7 @@ def run(self, app_handler):

def geventWebSocketServer():
from gevent import pywsgi

# from geventwebsocket.handler import WebSocketHandler # pip install gevent-websocket
from gevent_ws import WebSocketHandler # pip install gevent gevent-ws

Expand All @@ -193,21 +334,15 @@ def geventWebSocketServer():

class GeventWebSocketServer(ServerAdapter):
def run(self, app_handler):
logger = "default"
logger = None # "default"

if not self.quiet:
logger = logging_conf(
self.options["logging_level"],
"gevent-ws",
)
logger = logging_conf(self.options["logging_level"], "gevent-ws",)

certfile = self.options.get("certfile", None)

ssl_args = (
dict(
certfile=certfile,
keyfile=self.options.get("keyfile", None),
)
dict(certfile=certfile, keyfile=self.options.get("keyfile", None),)
if certfile
else dict()
)
Expand Down Expand Up @@ -235,19 +370,15 @@ def wsgirefThreadingServer():
import socket
from concurrent.futures import ThreadPoolExecutor # pip install futures
from socketserver import ThreadingMixIn
from wsgiref.simple_server import (WSGIRequestHandler, WSGIServer,
make_server)
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer, make_server

class WSGIRefThreadingServer(ServerAdapter):
def run(self, app_handler):

self.log = None

if not self.quiet:
self.log = logging_conf(
self.options["logging_level"],
"wsgiref",
)
self.log = logging_conf(self.options["logging_level"], "wsgiref",)

self_run = self # used in internal classes to access options and logger

Expand Down Expand Up @@ -355,9 +486,7 @@ def run(self, app_handler):

if not self.quiet:

logging_conf(
self.options["logging_level"],
)
logging_conf(self.options["logging_level"],)

interface = (
(
Expand Down Expand Up @@ -406,7 +535,8 @@ def salog(pat='SA:'):

return _srv_log

dbg and salog().info(str(mess))
caller = f" > {APP_NAME} > {sys._getframe().f_back.f_code.co_name}"
dbg and salog().info(mess + caller)

log_warn=log_info
log_debug=log_info
Expand Down