From 8163dbce7e25effb74f6b22bc84f20fa009c6ed0 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Wed, 13 Oct 2021 22:32:12 +0100 Subject: [PATCH] Add socket sharing --- Cargo.lock | 2 + Cargo.toml | 2 + robyn/__init__.py | 36 +++++++- src/lib.rs | 3 + src/server.rs | 28 +++++- src/shared_socket.rs | 37 ++++++++ test_python/app.py | 7 ++ test_python/mysite/manage.py | 22 +++++ test_python/mysite/mysite/__init__.py | 0 test_python/mysite/mysite/asgi.py | 16 ++++ test_python/mysite/mysite/settings.py | 125 ++++++++++++++++++++++++++ test_python/mysite/mysite/urls.py | 21 +++++ test_python/mysite/mysite/wsgi.py | 16 ++++ 13 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 src/shared_socket.rs create mode 100644 test_python/app.py create mode 100755 test_python/mysite/manage.py create mode 100644 test_python/mysite/mysite/__init__.py create mode 100644 test_python/mysite/mysite/asgi.py create mode 100644 test_python/mysite/mysite/settings.py create mode 100644 test_python/mysite/mysite/urls.py create mode 100644 test_python/mysite/mysite/wsgi.py diff --git a/Cargo.lock b/Cargo.lock index 0751b51d7..54822c400 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,6 +1183,7 @@ name = "robyn" version = "0.7.0" dependencies = [ "actix-files", + "actix-http", "actix-web", "anyhow", "dashmap", @@ -1190,6 +1191,7 @@ dependencies = [ "matchit", "pyo3", "pyo3-asyncio", + "socket2", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 4847b4461..566a190a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ actix-web = "4.0.0-beta.8" actix-files = "0.6.0-beta.4" futures-util = "0.3.15" matchit = "0.4.3" +actix-http = "3.0.0-beta.8" +socket2 = { version = "0.4.1", features = ["all"] } [package.metadata.maturin] name = "robyn" diff --git a/robyn/__init__.py b/robyn/__init__.py index 11c6ee35c..d04420d97 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -3,15 +3,33 @@ import argparse import asyncio -from .robyn import Server +from .robyn import Server, SocketHeld from .responses import static_file, jsonify from .dev_event_handler import EventHandler from .log_colors import Colors +from multiprocessing import Process from watchdog.observers import Observer +def spawned_process(handlers, socket, name): + import asyncio + import uvloop + + uvloop.install() + loop = uvloop.new_event_loop() + asyncio.set_event_loop(loop) + + # create a server + server = Server() + + for i in handlers: + server.add_route(i[0], i[1], i[2], i[3]) + + server.start(socket, name) + asyncio.get_event_loop().run_forever() + class Robyn: """This is the python wrapper for the Robyn binaries. @@ -22,6 +40,8 @@ def __init__(self, file_object): self.directory_path = directory_path self.server = Server(directory_path) self.dev = self._is_dev() + self.routes = [] + self.headers = [] def _is_dev(self): parser = argparse.ArgumentParser() @@ -40,8 +60,8 @@ def add_route(self, route_type, endpoint, handler): """ We will add the status code here only """ - self.server.add_route( - route_type, endpoint, handler, asyncio.iscoroutinefunction(handler) + self.routes.append( + ( route_type, endpoint, handler, asyncio.iscoroutinefunction(handler) ) ) def add_directory(self, route, directory_path, index_file=None, show_files_listing=False): @@ -59,7 +79,17 @@ def start(self, url="127.0.0.1", port="5000"): :param port [int]: [reperesents the port number at which the server is listening] """ + socket = SocketHeld(f"0.0.0.0:{port}", port) if not self.dev: + for i in range(2): + copied = socket.try_clone() + p = Process( + target=spawned_process, + args=(self.routes, copied, f"Process {i}"), + ) + p.start() + + input("Press Cntrl + C to stop \n") self.server.start(url, port) else: event_handler = EventHandler(self.file_path) diff --git a/src/lib.rs b/src/lib.rs index a07698593..8b68e660a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,11 @@ mod processor; mod router; mod server; +mod shared_socket; mod types; use server::Server; +use shared_socket::SocketHeld; // pyO3 module use pyo3::prelude::*; @@ -12,6 +14,7 @@ use pyo3::prelude::*; pub fn robyn(_py: Python<'_>, m: &PyModule) -> PyResult<()> { // the pymodule class to make the rustPyFunctions available m.add_class::()?; + m.add_class::()?; pyo3::prepare_freethreaded_python(); Ok(()) } diff --git a/src/server.rs b/src/server.rs index 0be087cd2..2b161649d 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,18 +1,23 @@ use crate::processor::{apply_headers, handle_request}; use crate::router::Router; +use crate::shared_socket::SocketHeld; use crate::types::Headers; use actix_files::Files; +use std::convert::TryInto; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering::{Relaxed, SeqCst}; use std::sync::{Arc, RwLock}; use std::thread; // pyO3 module +use actix_http::KeepAlive; use actix_web::*; use dashmap::DashMap; use pyo3::prelude::*; use pyo3::types::PyAny; // hyper modules +use socket2::{Domain, Protocol, Socket, Type}; + static STARTED: AtomicBool = AtomicBool::new(false); #[derive(Clone)] @@ -41,15 +46,28 @@ impl Server { } } - pub fn start(&mut self, py: Python, url: String, port: u16) { + pub fn start( + &mut self, + py: Python, + url: String, + port: u16, + socket: &PyCell, + name: String, + ) -> PyResult<()> { if STARTED .compare_exchange(false, true, SeqCst, Relaxed) .is_err() { println!("Already running..."); - return; + return Ok(()); } + let borrow = socket.try_borrow_mut()?; + let held_socket: &SocketHeld = &*borrow; + + let raw_socket = held_socket.get_socket(); + println!("Got our socket {:?}", raw_socket); + let router = self.router.clone(); let headers = self.headers.clone(); let directories = self.directories.clone(); @@ -103,7 +121,10 @@ impl Server { }) })) }) - .bind(addr) + .keep_alive(KeepAlive::Os) + .workers(1) + .client_timeout(0) + .listen(raw_socket.try_into().unwrap()) .unwrap() .run() .await @@ -112,6 +133,7 @@ impl Server { }); event_loop.call_method0("run_forever").unwrap(); + Ok(()) } pub fn add_directory( diff --git a/src/shared_socket.rs b/src/shared_socket.rs new file mode 100644 index 000000000..98941e857 --- /dev/null +++ b/src/shared_socket.rs @@ -0,0 +1,37 @@ +use pyo3::prelude::*; + +use socket2::{Domain, Protocol, Socket, Type}; +use std::net::SocketAddr; + +#[pyclass] +#[derive(Debug)] +pub struct SocketHeld { + pub socket: Socket, +} + +#[pymethods] +impl SocketHeld { + #[new] + pub fn new(address: String, port: i32) -> PyResult { + let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))?; + + let address: SocketAddr = address.parse()?; + socket.set_reuse_address(true)?; + //socket.set_reuse_port(true)?; + socket.bind(&address.into())?; + socket.listen(1024)?; + + Ok(SocketHeld { socket }) + } + + pub fn try_clone(&self) -> PyResult { + let copied = self.socket.try_clone()?; + Ok(SocketHeld { socket: copied }) + } +} + +impl SocketHeld { + pub fn get_socket(&self) -> Socket { + self.socket.try_clone().unwrap() + } +} diff --git a/test_python/app.py b/test_python/app.py new file mode 100644 index 000000000..f5961e68b --- /dev/null +++ b/test_python/app.py @@ -0,0 +1,7 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return {"Hello": "World"} diff --git a/test_python/mysite/manage.py b/test_python/mysite/manage.py new file mode 100755 index 000000000..a7da6671a --- /dev/null +++ b/test_python/mysite/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/test_python/mysite/mysite/__init__.py b/test_python/mysite/mysite/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test_python/mysite/mysite/asgi.py b/test_python/mysite/mysite/asgi.py new file mode 100644 index 000000000..fcc892069 --- /dev/null +++ b/test_python/mysite/mysite/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for mysite project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_asgi_application() diff --git a/test_python/mysite/mysite/settings.py b/test_python/mysite/mysite/settings.py new file mode 100644 index 000000000..1ee302879 --- /dev/null +++ b/test_python/mysite/mysite/settings.py @@ -0,0 +1,125 @@ +""" +Django settings for mysite project. + +Generated by 'django-admin startproject' using Django 3.2.8. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-o(eq891a$p#il6x=w-zez%=1l$#6p^2^ctlkbbm6kt*0q-b=3)' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'mysite.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'mysite.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/test_python/mysite/mysite/urls.py b/test_python/mysite/mysite/urls.py new file mode 100644 index 000000000..455210898 --- /dev/null +++ b/test_python/mysite/mysite/urls.py @@ -0,0 +1,21 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/test_python/mysite/mysite/wsgi.py b/test_python/mysite/mysite/wsgi.py new file mode 100644 index 000000000..fa2e052a9 --- /dev/null +++ b/test_python/mysite/mysite/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for mysite project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + +application = get_wsgi_application()