Klaus Demo bjoern / 15f2b48
Support for SO_REUSEPORT. Closes #69. Thanks to @dw for the initial patch. Jonas Haag 7 years ago
6 changed file(s) with 104 addition(s) and 19 deletion(s). Raw diff Collapse all Expand all
0 1.3.3 (Sep 2013)
1 - Support for SO_REUSEPORT (David Wilson, #69)
2
03 1.3.2 (June 2013)
14 - FreeBSD (sendfile) support (Olivier Duchateu)
25
5757 # Bind to TCP host/port pair:
5858 bjoern.run(wsgi_application, host, port)
5959
60 # TCP host/port pair, enabling SO_REUSEPORT if available.
61 bjoern.run(wsgi_application, host, port, reuseport=True)
62
6063 # Bind to Unix socket:
6164 bjoern.run(wsgi_application, 'unix:/path/to/socket')
6265
55
66
77 PyDoc_STRVAR(listen_doc,
8 "listen(application, host, port) -> None\n\n \
8 "listen(wsgi_app, host, [port, [reuse_port=False]]) -> None\n\n \
99 \
10 Makes bjoern listen to host:port and use application as WSGI callback. \
11 (This does not run the server mainloop.)");
10 Makes bjoern listen to 'host:port' and use 'wsgi_app' as WSGI application. \
11 (This does not run the server mainloop.)\n\n \
12 \
13 'reuse_port' -- whether to set SO_REUSEPORT (if available on platform)");
1214 static PyObject*
13 listen(PyObject* self, PyObject* args)
15 listen(PyObject* self, PyObject* args, PyObject* kwds)
1416 {
17 static char* keywords[] = {"wsgi_app", "host", "port", "reuse_port", NULL};
18
1519 const char* host;
16 int port = 0;
20 int port = 5;
21 int reuse_port = false; // see footnote (1)
1722
1823 if(wsgi_app) {
1924 PyErr_SetString(
2328 return NULL;
2429 }
2530
26 if(!PyArg_ParseTuple(args, "Os|i:run/listen", &wsgi_app, &host, &port))
31 if(!PyArg_ParseTupleAndKeywords(args, kwds, "Os|ii:run/listen", keywords,
32 &wsgi_app, &host, &port, &reuse_port))
2733 return NULL;
2834
2935 _initialize_request_module(host, port);
3743 host += 5;
3844 }
3945
40 if(!server_init(host, port)) {
46 if(!server_init(host, port, reuse_port)) {
4147 if(port)
4248 PyErr_Format(PyExc_RuntimeError, "Could not start server on %s:%d", host, port);
4349 else
5359 }
5460
5561 PyDoc_STRVAR(run_doc,
56 "run(application, host, port) -> None\n \
57 Calls listen(application, host, port) and starts the server mainloop.\n \
62 "run(*args, **kwargs) -> None\n \
63 Calls listen(*args, **kwargs) and starts the server mainloop.\n \
5864 \n\
5965 run() -> None\n \
6066 Starts the server mainloop. listen(...) has to be called before calling \
6167 run() without arguments.");
6268 static PyObject*
63 run(PyObject* self, PyObject* args)
69 run(PyObject* self, PyObject* args, PyObject* kwds)
6470 {
6571 if(PyTuple_GET_SIZE(args) == 0) {
6672 /* bjoern.run() */
6773 if(!wsgi_app) {
6874 PyErr_SetString(
6975 PyExc_RuntimeError,
70 "Must call bjoern.listen(app, host, port) before "
76 "Must call bjoern.listen(wsgi_app, host, ...) before "
7177 "calling bjoern.run() without arguments."
7278 );
7379 return NULL;
7480 }
7581 } else {
76 /* bjoern.run(app, host, port) */
77 if(!listen(self, args))
82 /* bjoern.run(wsgi_app, host, ...) */
83 if(!listen(self, args, kwds))
7884 return NULL;
7985 }
8086
8490 }
8591
8692 static PyMethodDef Bjoern_FunctionTable[] = {
87 {"run", run, METH_VARARGS, run_doc},
88 {"listen", listen, METH_VARARGS, listen_doc},
93 {"run", (PyCFunction) run, METH_VARARGS | METH_KEYWORDS, run_doc},
94 {"listen", (PyCFunction) listen, METH_VARARGS | METH_KEYWORDS, listen_doc},
8995 {NULL, NULL, 0, NULL}
9096 };
9197
102108 PyObject* bjoern_module = Py_InitModule("bjoern", Bjoern_FunctionTable);
103109 PyModule_AddObject(bjoern_module, "version", Py_BuildValue("(iii)", 1, 3, 2));
104110 }
111
112 /* (1) Don't use bool here because we use the "i" type specifier in the call to
113 * PyArg_ParseTuple below which always writes sizeof(int) bytes (and thus
114 * might write to memory regions we don't expect it to write to. */
7777 }
7878 #endif
7979
80 bool server_init(const char* hostaddr, const int port)
80 bool server_init(const char* hostaddr, const int port, const int reuse_port)
8181 {
8282 if(!port) {
8383 /* Unix socket */
108108 inet_pton(AF_INET, hostaddr, &sockaddr.sin_addr);
109109 sockaddr.sin_port = htons(port);
110110
111 /* Socket options */
112 int true_ref = true;
113
111114 /* Set SO_REUSEADDR t make the IP address available for reuse */
112 int optval = true;
113 setsockopt(sockinfo.fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
115 setsockopt(sockinfo.fd, SOL_SOCKET, SO_REUSEADDR, &true_ref, sizeof(true_ref));
116
117 /* Enable "receive steering" on FreeBSD and Linux >=3.9. This allows
118 * multiple independent bjoerns to bind to the same port (and ideally also
119 * set their CPU affinity), resulting in more efficient load distribution.
120 * https://lwn.net/Articles/542629/
121 */
122 if(reuse_port) {
123 #ifdef SO_REUSEPORT
124 setsockopt(sockinfo.fd, SOL_SOCKET, SO_REUSEPORT, &true_ref, sizeof(true_ref));
125 #else
126 PyErr_SetString(PyExc_RuntimeError, "SO_REUSEPORT is not available.");
127 goto err;
128 #endif
129 }
114130
115131 if(bind(sockinfo.fd, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) < 0)
116132 goto err;
00 #include "request.h"
11
2 bool server_init(const char* hostaddr, const int port);
2 bool server_init(const char* hostaddr, const int port, const int reuseport);
33 void server_run(void);
0 import os
1 import time
2 import sys
3 import httplib
4 import subprocess
5 from collections import defaultdict
6
7
8 N_PROCESSES = 3
9 N_REQUESTS_PER_PROCESS = 100
10 N_REQUESTS = N_REQUESTS_PER_PROCESS * N_PROCESSES
11
12
13 def cmd_test():
14 processes = [subprocess.Popen([sys.executable, __file__, "app"])
15 for _ in range(N_PROCESSES)]
16
17 time.sleep(0.2 * N_PROCESSES)
18
19
20 responder_count = defaultdict(int)
21
22 for i in range(N_REQUESTS):
23 conn = httplib.HTTPConnection("localhost", 8080)
24 conn.request("GET", "/")
25 response = conn.getresponse().read()
26 responder = response.split()[-1]
27 responder_count[responder] += 1
28
29 for proc in processes:
30 proc.terminate()
31
32 for responder, count in responder_count.items():
33 assert (count > N_REQUESTS_PER_PROCESS * 0.8 and
34 count < N_REQUESTS_PER_PROCESS * 1.2), \
35 "requests not correctly distributed"
36
37
38
39 def cmd_app():
40 def app(environ, start_response):
41 start_response('200 OK', [])
42 return ["Hello from process %d\n" % os.getpid()]
43
44 import bjoern
45 bjoern.run(app, "localhost", 8080, True)
46
47
48 if __name__ == '__main__':
49 if "app" in sys.argv:
50 cmd_app()
51 else:
52 cmd_test()