Hello community, here is the log from the commit of package python-bjoern for openSUSE:Factory checked in at 2020-06-24 15:49:18 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-bjoern (Old) and /work/SRC/openSUSE:Factory/.python-bjoern.new.2956 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-bjoern" Wed Jun 24 15:49:18 2020 rev:9 rq:816735 version:3.1.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-bjoern/python-bjoern.changes 2019-07-08 16:37:24.304651680 +0200 +++ /work/SRC/openSUSE:Factory/.python-bjoern.new.2956/python-bjoern.changes 2020-06-24 15:49:23.708533019 +0200 @@ -1,0 +2,8 @@ +Wed Jun 24 01:48:04 UTC 2020 - Steve Kowalik <steven.kowa...@suse.com> + +- Update to 3.1.0: + * #169 Fix blocking accept() (Ionut Negru) + * #164 Add support for statsd metrics/events (Mohammad Gufran) + * #162 Fix Expect: 100-Continue support (Tom Brennan) + +------------------------------------------------------------------- Old: ---- bjoern-3.0.1.tar.gz New: ---- bjoern-3.1.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-bjoern.spec ++++++ --- /var/tmp/diff_new_pack.gyNPFA/_old 2020-06-24 15:49:24.512536390 +0200 +++ /var/tmp/diff_new_pack.gyNPFA/_new 2020-06-24 15:49:24.516536407 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-bjoern # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2020 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,12 +18,11 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-bjoern -Version: 3.0.1 +Version: 3.1.0 Release: 0 Summary: A screamingly fast Python 2 + 3 WSGI server written in C License: BSD-2-Clause -Group: Development/Languages/Python -Url: https://github.com/jonashaag/bjoern +URL: https://github.com/jonashaag/bjoern Source: https://files.pythonhosted.org/packages/source/b/bjoern/bjoern-%{version}.tar.gz BuildRequires: %{python_module devel} BuildRequires: %{python_module setuptools} ++++++ bjoern-3.0.1.tar.gz -> bjoern-3.1.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/.gitmodules new/bjoern-3.1.0/.gitmodules --- old/bjoern-3.0.1/.gitmodules 2014-11-11 21:31:07.000000000 +0100 +++ new/bjoern-3.1.0/.gitmodules 2019-10-19 14:58:30.000000000 +0200 @@ -1,3 +1,6 @@ [submodule "http-parser"] path = http-parser url = git://github.com/joyent/http-parser +[submodule "statsd-c-client"] + path = statsd-c-client + url = https://github.com/romanbsd/statsd-c-client diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/CHANGELOG new/bjoern-3.1.0/CHANGELOG --- old/bjoern-3.0.1/CHANGELOG 2019-06-07 12:31:44.000000000 +0200 +++ new/bjoern-3.1.0/CHANGELOG 2019-11-03 10:44:04.000000000 +0100 @@ -1,3 +1,8 @@ +3.1.0 (Nov 3, 2019) + - #169 Fix blocking accept() (Ionut Negru) + - #164 Add support for statsd metrics/events (Mohammad Gufran) + - #162 Fix Expect: 100-Continue support (Tom Brennan) + 3.0.1 (June 6, 2019) - Fix #158, #160: Correct string type for WSGI environ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/Makefile new/bjoern-3.1.0/Makefile --- old/bjoern-3.0.1/Makefile 2019-06-07 12:29:11.000000000 +0200 +++ new/bjoern-3.1.0/Makefile 2019-11-03 10:43:15.000000000 +0100 @@ -9,18 +9,18 @@ HTTP_PARSER_OBJ = $(HTTP_PARSER_DIR)/http_parser.o HTTP_PARSER_SRC = $(HTTP_PARSER_DIR)/http_parser.c -objects = $(HTTP_PARSER_OBJ) \ +STATSD_CLIENT_DIR = statsd-c-client +STATSD_CLIENT_OBJ = $(STATSD_CLIENT_DIR)/statsd-client.o +STATSD_CLIENT_SRC = $(STATSD_CLIENT_DIR)/statsd-client.c + +objects = $(HTTP_PARSER_OBJ) $(STATSD_CLIENT_OBJ) \ $(patsubst $(SOURCE_DIR)/%.c, $(BUILD_DIR)/%.o, \ $(wildcard $(SOURCE_DIR)/*.c)) -CPPFLAGS += $(PYTHON_INCLUDE) -I . -I $(SOURCE_DIR) -I $(HTTP_PARSER_DIR) +CPPFLAGS += $(PYTHON_INCLUDE) -I . -I $(SOURCE_DIR) -I $(HTTP_PARSER_DIR) -I $(STATSD_CLIENT_DIR) CFLAGS += $(FEATURES) -std=c99 -fno-strict-aliasing -fcommon -fPIC -Wall LDFLAGS += $(PYTHON_LDFLAGS) -l ev -shared -fcommon -ifneq ($(WANT_SENDFILE), no) -FEATURES += -D WANT_SENDFILE -endif - ifneq ($(WANT_SIGINT_HANDLING), no) FEATURES += -D WANT_SIGINT_HANDLING endif @@ -33,6 +33,17 @@ FEATURES += -D SIGNAL_CHECK_INTERVAL=0.1 endif +ifeq ($(WANT_STATSD), yes) +FEATURES += -D WANT_STATSD +else +filter_out = $(foreach v,$(2),$(if $(findstring $(1),$(v)),,$(v))) +objects := $(call filter_out,statsd,$(objects)) +endif + +ifeq ($(WANT_STATSD_TAGS), yes) +FEATURES += -D WANT_STATSD_TAGS +endif + all: prepare-build $(objects) _bjoernmodule print-env: @@ -48,13 +59,14 @@ CFLAGS='-Os' make _bjoernmodule: + @echo ' -> ' $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(objects) -o $(BUILD_DIR)/_bjoern.so @$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $(objects) -o $(BUILD_DIR)/_bjoern.so @PYTHONPATH=$$PYTHONPATH:$(BUILD_DIR) ${PYTHON} -c "import bjoern" again: clean all debug: - CFLAGS='-D DEBUG -g' make again + CFLAGS='${CFLAGS} -D DEBUG -g' make again $(BUILD_DIR)/%.o: $(SOURCE_DIR)/%.c @echo ' -> ' $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ @@ -106,3 +118,6 @@ $(HTTP_PARSER_OBJ): $(MAKE) -C $(HTTP_PARSER_DIR) http_parser.o CFLAGS_DEBUG_EXTRA=-fPIC CFLAGS_FAST_EXTRA=-fPIC + +$(STATSD_CLIENT_OBJ): + $(MAKE) -C $(STATSD_CLIENT_DIR) statsd-client.o CFLAGS_DEBUG_EXTRA=-fPIC CFLAGS_FAST_EXTRA=-fPIC diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/PKG-INFO new/bjoern-3.1.0/PKG-INFO --- old/bjoern-3.0.1/PKG-INFO 2019-06-07 12:32:21.000000000 +0200 +++ new/bjoern-3.1.0/PKG-INFO 2019-11-03 10:45:13.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: bjoern -Version: 3.0.1 +Version: 3.1.0 Summary: A screamingly fast Python 2 + 3 WSGI server written in C. Home-page: https://github.com/jonashaag/bjoern Author: Jonas Haag diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/README.rst new/bjoern-3.1.0/README.rst --- old/bjoern-3.0.1/README.rst 2017-08-31 18:52:13.000000000 +0200 +++ new/bjoern-3.1.0/README.rst 2019-10-19 14:58:30.000000000 +0200 @@ -42,17 +42,27 @@ # Bind to abstract Unix socket: (Linux only) bjoern.run(wsgi_application, 'unix:@socket_name') + # Enable statsd metrics. See instrumentation.md for details. + bjoern.run(wsgi_application, host, port, statsd=...) + Alternatively, the mainloop can be run separately:: bjoern.listen(wsgi_application, host, port) bjoern.run() - + + # With metrics. See instrumentation.md for details. + bjoern.listen(wsgi_application, host, port) + bjoern.run(statsd=...) + You can also simply pass a Python socket(-like) object. Note that you are responsible for initializing and cleaning up the socket in that case. :: bjoern.server_run(socket_object, wsgi_application) bjoern.server_run(filedescriptor_as_integer, wsgi_application) + # This needs manual compilation with `WANT_STATSD=yes` + bjoern.server_run(socket_object, wsgi_application, enable_statsd=True) + .. _WSGI: http://www.python.org/dev/peps/pep-0333/ .. _libev: http://software.schmorp.de/pkg/libev.html .. _http-parser: https://github.com/joyent/http-parser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/_bjoernmodule.c new/bjoern-3.1.0/bjoern/_bjoernmodule.c --- old/bjoern-3.0.1/bjoern/_bjoernmodule.c 2019-06-07 12:32:00.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/_bjoernmodule.c 2019-11-03 10:44:28.000000000 +0100 @@ -3,6 +3,10 @@ #include "wsgi.h" #include "filewrapper.h" +#ifdef WANT_STATSD +#include "statsd-client.h" +#endif + static PyObject* run(PyObject* self, PyObject* args) { @@ -10,9 +14,31 @@ PyObject* socket; - if(!PyArg_ParseTuple(args, "OO:server_run", &socket, &info.wsgi_app)) { +#ifdef WANT_STATSD + info.statsd = NULL; + int statsd_enabled; + char* statsd_host; + int statsd_port; + char* statsd_ns; + char* statsd_tags = NULL; + + if(!PyArg_ParseTuple(args, "OOiziz|z:server_run", &socket, &info.wsgi_app, + &statsd_enabled, &statsd_host, &statsd_port, &statsd_ns, &statsd_tags)) { + return NULL; + } +#else + char* ignored_str = NULL; + int ignored_int = 0; + + if(!PyArg_ParseTuple(args, "OO|izizz:server_run", &socket, &info.wsgi_app, &ignored_int, + &ignored_str, &ignored_int, &ignored_str, &ignored_str)) { return NULL; } + if (ignored_str != NULL || ignored_int != 0) { + PyErr_Format(PyExc_TypeError, "Unexpected statsd_* arguments (forgot to compile with statsd support?)"); + return NULL; + } +#endif info.sockfd = PyObject_AsFileDescriptor(socket); if (info.sockfd < 0) { @@ -32,9 +58,38 @@ } } +#ifdef WANT_STATSD + if (statsd_enabled) { + if (statsd_host == NULL || *statsd_host == '\0') { + statsd_host = "127.0.0.1"; + } + + if (statsd_ns == NULL || *statsd_ns == '\0') { + info.statsd = statsd_init(statsd_host, statsd_port); + } else { + info.statsd = statsd_init_with_namespace(statsd_host, statsd_port, statsd_ns); + } +#ifdef WANT_STATSD_TAGS + info.statsd_tags = statsd_tags; + DBG("Statsd: host=%s, port=%d, ns=%s, tags=%s", statsd_host, statsd_port, statsd_ns, statsd_tags); +#else + DBG("Statsd: host=%s, port=%d, ns=%s", statsd_host, statsd_port, statsd_ns); +#endif + } else { + DBG("Statsd disabled"); + } + + +#endif + _initialize_request_module(&info); + server_run(&info); +#ifdef WANT_STATSD + statsd_finalize(info.statsd); +#endif + Py_RETURN_NONE; } @@ -73,17 +128,45 @@ Py_INCREF(&FileWrapper_Type); Py_INCREF(&StartResponse_Type); + PyObject* features = PyDict_New(); + +#ifdef WANT_SIGNAL_HANDLING + PyDict_SetItemString(features, "has_signal_handling", Py_True); +#else + PyDict_SetItemString(features, "has_signal_handling", Py_False); +#endif + +#ifdef WANT_SIGINT_HANDLING + PyDict_SetItemString(features, "has_sigint_handling", Py_True); +#else + PyDict_SetItemString(features, "has_sigint_handling", Py_False); +#endif + +#ifdef WANT_STATSD + PyDict_SetItemString(features, "has_statsd", Py_True); +#else + PyDict_SetItemString(features, "has_statsd", Py_False); +#endif + +#ifdef WANT_STATSD_TAGS + PyDict_SetItemString(features, "has_statsd_tags", Py_True); +#else + PyDict_SetItemString(features, "has_statsd_tags", Py_False); +#endif + #if PY_MAJOR_VERSION >= 3 PyObject* bjoern_module = PyModule_Create(&module); if (bjoern_module == NULL) { return NULL; } - - PyModule_AddObject(bjoern_module, "version", Py_BuildValue("(iii)", 3, 0, 1)); - return bjoern_module; #else PyObject* bjoern_module = Py_InitModule("_bjoern", Bjoern_FunctionTable); - PyModule_AddObject(bjoern_module, "version", Py_BuildValue("(iii)", 3, 0, 1)); #endif + PyModule_AddObject(bjoern_module, "features", features); + PyModule_AddObject(bjoern_module, "version", Py_BuildValue("(iii)", 3, 1, 0)); + +#if PY_MAJOR_VERSION >= 3 + return bjoern_module; +#endif } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/common.h new/bjoern-3.1.0/bjoern/common.h --- old/bjoern-3.0.1/bjoern/common.h 2018-08-04 15:14:38.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/common.h 2019-10-19 14:58:30.000000000 +0200 @@ -14,7 +14,12 @@ typedef struct { char* data; size_t len; } string; -enum http_status { HTTP_BAD_REQUEST = 1, HTTP_LENGTH_REQUIRED, HTTP_SERVER_ERROR }; +enum http_status { + HTTP_BAD_REQUEST = 1, + HTTP_LENGTH_REQUIRED, + HTTP_EXPECTATION_FAILED, + HTTP_SERVER_ERROR +}; size_t unquote_url_inplace(char* url, size_t len); void _init_common(void); @@ -52,4 +57,27 @@ #define assert(...) do{}while(0) #endif +#ifdef WANT_STATSD + #include "statsd-client.h" + + #ifdef WANT_STATSD_TAGS + #include "statsd_tags.h" + #define STATSD_INCREMENT(name) \ + do { \ + DBG("statisd.inc: %s", name); \ + statsd_inc_with_tags(((ThreadInfo*) ev_userdata(mainloop))->server_info->statsd, \ + name, \ + ((ThreadInfo*) ev_userdata(mainloop))->server_info->statsd_tags); \ + } while (0) + #else + #define STATSD_INCREMENT(name) \ + do { \ + DBG("statisd.inc: %s", name); \ + statsd_inc(((ThreadInfo*) ev_userdata(mainloop))->server_info->statsd, name, 1.0); \ + } while (0) + #endif +#else + #define STATSD_INCREMENT(name) DBG("statsd.inc: %s", name) +#endif + #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/py2py3.h new/bjoern-3.1.0/bjoern/py2py3.h --- old/bjoern-3.0.1/bjoern/py2py3.h 2019-05-27 16:04:07.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/py2py3.h 2019-09-12 09:31:24.000000000 +0200 @@ -18,6 +18,7 @@ #define _PEP3333_String_FromFormat(...) PyUnicode_FromFormat(__VA_ARGS__) #define _PEP3333_String_GET_SIZE(u) PyUnicode_GET_LENGTH(u) #define _PEP3333_String_Concat(u1, u2) PyUnicode_Concat(u1, u2) +#define _PEP3333_String_CompareWithASCIIString(o, c_str) PyUnicode_CompareWithASCIIString(o, c_str) #else @@ -53,6 +54,11 @@ return ret; } + +static int _PEP3333_String_CompareWithASCIIString(PyObject *o, const char *c_str) +{ + return memcmp(_PEP3333_Bytes_AS_DATA(o), c_str, _PEP3333_Bytes_GET_SIZE(o)); +} #endif #endif /* _PY2PY3_H */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/request.c new/bjoern-3.1.0/bjoern/request.c --- old/bjoern-3.0.1/bjoern/request.c 2019-06-07 12:29:15.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/request.c 2019-09-12 09:31:24.000000000 +0200 @@ -180,7 +180,49 @@ if(!PARSER->invalid_header) { /* Set header, or append data to header if this is not the first call */ _set_or_append_header(REQUEST->headers, PARSER->field, value, len); + + /* + ** HTTP/1.1 describes the header `Expect: 100-continue`, which provides + ** a mechanism for a client to request permission from a server to send + ** the rest of the request (i.e., the body). This restriction is entirely + ** client-side in that, regardless of this header existing, the client + ** can opt to send the body immediately (i.e., contradicting the header). + ** In that case, the header MAY be ignored (according to the RFC). It's + ** important to note that this mechanism is not intended to mitigate DoS + ** attacks, etc. It's an optional client-side courtesy only. + ** + ** Some popular clients (e.g., cURL) do in fact wait for an indefinite + ** time period before sending the rest of the request, and so it is + ** appropriate to respond with `HTTP/1.1 100 Continue` in that case. + ** + ** In the future, it may be a reasonable improvement to provide a callback + ** API to provide a mechanism for calling code (i.e., Python) to determine + ** whether continuation is appropriate based on the existing headers, but + ** currently the behavior implemented here is to respond with 100-continue + ** automatically as soon as the Expect header is detected, or + ** `417 Expectation Failed` if the header contains anything other than + ** "100-continue". + ** + ** see https://tools.ietf.org/html/rfc2616#page-48 for specifics. + */ + if (REQUEST->state.expect_continue || REQUEST->state.error_code) + return 0; + + if (parser->http_major > 0 && parser->http_minor > 0) { + bool field_is_http_expect = !_PEP3333_String_CompareWithASCIIString( + PARSER->field, "HTTP_EXPECT" + ); + + if (field_is_http_expect) { + if (!strncmp(value, "100-continue", len)) { + REQUEST->state.expect_continue = true; + } else { + REQUEST->state.error_code = HTTP_EXPECTATION_FAILED; + } + } + } } + return 0; } @@ -189,6 +231,14 @@ { PyObject *body; + /* + ** If you're reading the body, you're not waiting for it, so + ** no need to respond 100-continue. + ** See RFC 2616 https://tools.ietf.org/html/rfc2616#page-49 for details. + ** See also comment above in `on_header_value`. + */ + REQUEST->state.expect_continue = false; + body = PyDict_GetItem(REQUEST->headers, _wsgi_input); if (body == NULL) { if(!parser->content_length) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/request.h new/bjoern-3.1.0/bjoern/request.h --- old/bjoern-3.0.1/bjoern/request.h 2018-06-01 11:06:59.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/request.h 2019-09-12 09:31:24.000000000 +0200 @@ -16,6 +16,7 @@ unsigned keep_alive : 1; unsigned response_length_unknown : 1; unsigned chunked_response : 1; + unsigned expect_continue : 1; } request_state; typedef struct { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/server.c new/bjoern-3.1.0/bjoern/server.c --- old/bjoern-3.0.1/bjoern/server.c 2019-05-07 15:13:43.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/server.c 2019-11-03 10:42:57.000000000 +0100 @@ -17,25 +17,32 @@ #include "common.h" #include "wsgi.h" #include "server.h" - #include "py2py3.h" +#ifdef WANT_STATSD +#include "statsd-client.h" +#endif + #define READ_BUFFER_SIZE 64*1024 #define Py_XCLEAR(obj) do { if(obj) { Py_DECREF(obj); obj = NULL; } } while(0) #define GIL_LOCK(n) PyGILState_STATE _gilstate_##n = PyGILState_Ensure() #define GIL_UNLOCK(n) PyGILState_Release(_gilstate_##n) -static const char* http_error_messages[4] = { +static const char* http_error_messages[5] = { NULL, /* Error codes start at 1 because 0 means "no error" */ "HTTP/1.1 400 Bad Request\r\n\r\n", "HTTP/1.1 406 Length Required\r\n\r\n", + "HTTP/1.1 417 Expectation Failed\r\n\r\n", "HTTP/1.1 500 Internal Server Error\r\n\r\n" }; +static const char *CONTINUE = "HTTP/1.1 100 Continue\r\n\r\n"; + enum _rw_state { not_yet_done = 1, done, aborted, + expect_continue, }; typedef enum _rw_state read_state; typedef enum _rw_state write_state; @@ -47,12 +54,12 @@ typedef void ev_io_callback(struct ev_loop*, ev_io*, const int); -#if WANT_SIGINT_HANDLING +#ifdef WANT_SIGINT_HANDLING typedef void ev_signal_callback(struct ev_loop*, ev_signal*, const int); static ev_signal_callback ev_signal_on_sigint; #endif -#if WANT_SIGINT_HANDLING +#ifdef WANT_SIGNAL_HANDLING typedef void ev_timer_callback(struct ev_loop*, ev_timer*, const int); static ev_timer_callback ev_timer_ontick; ev_timer timeout_watcher; @@ -75,12 +82,13 @@ ThreadInfo thread_info; thread_info.server_info = server_info; + ev_set_userdata(mainloop, &thread_info); ev_io_init(&thread_info.accept_watcher, ev_io_on_request, server_info->sockfd, EV_READ); ev_io_start(mainloop, &thread_info.accept_watcher); -#if WANT_SIGINT_HANDLING +#ifdef WANT_SIGINT_HANDLING ev_signal sigint_watcher; ev_signal_init(&sigint_watcher, ev_signal_on_sigint, SIGINT); ev_signal_start(mainloop, &sigint_watcher); @@ -99,7 +107,7 @@ Py_END_ALLOW_THREADS } -#if WANT_SIGINT_HANDLING +#ifdef WANT_SIGINT_HANDLING static void pyerr_set_interrupt(struct ev_loop* mainloop, struct ev_cleanup* watcher, const int events) { @@ -124,7 +132,7 @@ } #endif -#if WANT_SIGNAL_HANDLING +#ifdef WANT_SIGNAL_HANDLING static void ev_timer_ontick(struct ev_loop* mainloop, ev_timer* watcher, const int events) { @@ -145,11 +153,13 @@ client_fd = accept(watcher->fd, (struct sockaddr*)&sockaddr, &addrlen); if(client_fd < 0) { DBG("Could not accept() client: errno %d", errno); + STATSD_INCREMENT("conn.accept.error"); return; } int flags = fcntl(client_fd, F_GETFL, 0); if(fcntl(client_fd, F_SETFL, (flags < 0 ? 0 : flags) | O_NONBLOCK) == -1) { + STATSD_INCREMENT("conn.accept.error"); DBG("Could not set_nonblocking() client %d: errno %d", client_fd, errno); return; } @@ -164,6 +174,8 @@ GIL_UNLOCK(0); + STATSD_INCREMENT("conn.accept.success"); + DBG_REQ(request, "Accepted client %s:%d on fd %d", inet_ntoa(sockaddr.sin_addr), ntohs(sockaddr.sin_port), client_fd); @@ -172,6 +184,23 @@ ev_io_start(mainloop, &request->ev_watcher); } + +static void +start_reading(struct ev_loop *mainloop, Request *request) +{ + ev_io_init(&request->ev_watcher, &ev_io_on_read, + request->client_fd, EV_READ); + ev_io_start(mainloop, &request->ev_watcher); +} + +static void +start_writing(struct ev_loop *mainloop, Request *request) +{ + ev_io_init(&request->ev_watcher, &ev_io_on_write, + request->client_fd, EV_WRITE); + ev_io_start(mainloop, &request->ev_watcher); +} + static void ev_io_on_read(struct ev_loop* mainloop, ev_io* watcher, const int events) { @@ -192,6 +221,7 @@ /* Client disconnected */ read_state = aborted; DBG_REQ(request, "Client disconnected"); + STATSD_INCREMENT("req.error.client_disconnected"); } else if (read_bytes < 0) { /* Would block or error */ if(errno == EAGAIN || errno == EWOULDBLOCK) { @@ -199,6 +229,7 @@ } else { read_state = aborted; DBG_REQ(request, "Hit errno %d while read()ing", errno); + STATSD_INCREMENT("req.error.read"); } } else { /* OK, either expect more data or done reading */ @@ -207,14 +238,18 @@ /* HTTP parse error */ read_state = done; DBG_REQ(request, "Parse error"); + STATSD_INCREMENT("req.error.parse"); request->current_chunk = _PEP3333_Bytes_FromString( http_error_messages[request->state.error_code]); assert(request->iterator == NULL); } else if(request->state.parse_finished) { - /* HTTP parse successful */ + /* HTTP parse successful, meaning we have the entire + * request (the header _and_ the body). */ read_state = done; - bool wsgi_ok = wsgi_call_application(request); - if (!wsgi_ok) { + + STATSD_INCREMENT("req.success.read"); + + if (!wsgi_call_application(request)) { /* Response is "HTTP 500 Internal Server Error" */ DBG_REQ(request, "WSGI app error"); assert(PyErr_Occurred()); @@ -223,7 +258,15 @@ Py_XCLEAR(request->iterator); request->current_chunk = _PEP3333_Bytes_FromString( http_error_messages[HTTP_SERVER_ERROR]); + STATSD_INCREMENT("req.error.internal"); } + } else if (request->state.expect_continue) { + /* + ** Handle "Expect: 100-continue" header. + ** See https://tools.ietf.org/html/rfc2616#page-48 and `on_header_value` + ** in request.c for more details. + */ + read_state = expect_continue; } else { /* Wait for more data */ read_state = not_yet_done; @@ -232,16 +275,23 @@ switch (read_state) { case not_yet_done: + STATSD_INCREMENT("req.active"); + break; + case expect_continue: + DBG_REQ(request, "pause read, write 100-continue"); + ev_io_stop(mainloop, &request->ev_watcher); + request->current_chunk = _PEP3333_Bytes_FromString(CONTINUE); + start_writing(mainloop, request); break; case done: DBG_REQ(request, "Stop read watcher, start write watcher"); ev_io_stop(mainloop, &request->ev_watcher); - ev_io_init(&request->ev_watcher, &ev_io_on_write, - request->client_fd, EV_WRITE); - ev_io_start(mainloop, &request->ev_watcher); + start_writing(mainloop, request); + STATSD_INCREMENT("req.done"); break; case aborted: close_connection(mainloop, request); + STATSD_INCREMENT("req.aborted"); break; } @@ -278,27 +328,43 @@ write_state = on_write_chunk(mainloop, request); } + write_state = request->state.expect_continue + ? expect_continue + : write_state; + switch(write_state) { case not_yet_done: + STATSD_INCREMENT("resp.active"); break; case done: + STATSD_INCREMENT("resp.done"); if(request->state.keep_alive) { DBG_REQ(request, "done, keep-alive"); + STATSD_INCREMENT("resp.done.keepalive"); ev_io_stop(mainloop, &request->ev_watcher); + Request_clean(request); Request_reset(request); - ev_io_init(&request->ev_watcher, &ev_io_on_read, - request->client_fd, EV_READ); - ev_io_start(mainloop, &request->ev_watcher); + + start_reading(mainloop, request); } else { DBG_REQ(request, "done, close"); + STATSD_INCREMENT("resp.conn.close"); close_connection(mainloop, request); } break; + case expect_continue: + DBG_REQ(request, "expect continue"); + ev_io_stop(mainloop, &request->ev_watcher); + + request->state.expect_continue = false; + start_reading(mainloop, request); + break; case aborted: /* Response was aborted due to an error. We can't do anything graceful here * because at least one chunk is already sent... just close the connection. */ close_connection(mainloop, request); + STATSD_INCREMENT("resp.aborted"); break; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/server.h new/bjoern-3.1.0/bjoern/server.h --- old/bjoern-3.0.1/bjoern/server.h 2015-08-20 12:07:54.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/server.h 2019-10-19 14:58:30.000000000 +0200 @@ -1,11 +1,21 @@ #ifndef __server_h__ #define __server_h__ +#ifdef WANT_STATSD +#include "statsd-client.h" +#endif + typedef struct { int sockfd; PyObject* wsgi_app; PyObject* host; PyObject* port; +#ifdef WANT_STATSD + statsd_link* statsd; +# ifdef WANT_STATSD_TAGS + char* statsd_tags; +# endif +#endif } ServerInfo; void server_run(ServerInfo*); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/statsd_tags.c new/bjoern-3.1.0/bjoern/statsd_tags.c --- old/bjoern-3.0.1/bjoern/statsd_tags.c 1970-01-01 01:00:00.000000000 +0100 +++ new/bjoern-3.1.0/bjoern/statsd_tags.c 2019-10-19 14:58:30.000000000 +0200 @@ -0,0 +1,21 @@ +#include <stdio.h> +#include "statsd-client.h" + +#define MAX_MSG_LEN_WITH_TAGS 1000 + +static int send_stats_with_tags(statsd_link* link, char *stat, size_t value, const char* type, const char* tags) +{ + if (link == NULL) { + // Statsd disabled + return 0; + } + + char message[MAX_MSG_LEN_WITH_TAGS]; + snprintf(message, MAX_MSG_LEN_WITH_TAGS, "%s%s:%zd|%s|#%s", link->ns ? link->ns : "", stat, value, type, tags ? tags : ""); + return statsd_send(link, message); +} + +int statsd_inc_with_tags(statsd_link* link, char* stat, const char* tags) +{ + return send_stats_with_tags(link, stat, 1, "c", tags); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/statsd_tags.h new/bjoern-3.1.0/bjoern/statsd_tags.h --- old/bjoern-3.0.1/bjoern/statsd_tags.h 1970-01-01 01:00:00.000000000 +0100 +++ new/bjoern-3.1.0/bjoern/statsd_tags.h 2019-10-19 14:58:30.000000000 +0200 @@ -0,0 +1,9 @@ +#ifndef __statsd_tags__ +#define __statsd_tags__ + +#include "statsd-client.h" + +int send_stats_with_tags(statsd_link* link, char *stat, size_t value, const char *type, const char* tags); +int statsd_inc_with_tags(statsd_link* link, char *stat, const char* tags); + +#endif \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern/wsgi.c new/bjoern-3.1.0/bjoern/wsgi.c --- old/bjoern-3.0.1/bjoern/wsgi.c 2018-08-15 10:12:21.000000000 +0200 +++ new/bjoern-3.1.0/bjoern/wsgi.c 2019-09-12 10:14:48.000000000 +0200 @@ -67,6 +67,7 @@ _PEP3333_Bytes_Check(PyList_GET_ITEM(retval, 0))) { /* Optimize the most common case, a single bytestring in a list: */ + DBG_REQ(request, "WSGI iterable is list of size 1"); PyObject* tmp = PyList_GET_ITEM(retval, 0); Py_INCREF(tmp); Py_DECREF(retval); @@ -77,6 +78,7 @@ * i.e. sending the response item for item. "item for item" means * "char for char" if you have a bytestring. -- I'm not that stupid. */ bytestring: + DBG_REQ(request, "WSGI iterable is byte string"); request->iterable = NULL; request->iterator = NULL; if(_PEP3333_Bytes_GET_SIZE(retval)) { @@ -86,12 +88,14 @@ Py_DECREF(retval); first_chunk = NULL; } - } else if(FileWrapper_CheckExact(retval) && FileWrapper_GetFd(retval) != -1) { + } else if(!request->state.response_length_unknown && FileWrapper_CheckExact(retval) && FileWrapper_GetFd(retval) != -1) { + DBG_REQ(request, "WSGI iterable is wsgi.file_wrapper instance and Content-Length is known"); request->iterable = retval; request->iterator = NULL; first_chunk = NULL; } else { /* Generic iterable (list of length != 1, generator, ...) */ + DBG_REQ(request, "WSGI iterable is some other iterable"); request->iterable = retval; request->iterator = PyObject_GetIter(retval); if(request->iterator == NULL) @@ -125,18 +129,22 @@ if(request->state.response_length_unknown) { if(request->parser.parser.http_major > 0 && request->parser.parser.http_minor > 0) { /* On HTTP 1.1, we can use Transfer-Encoding: chunked. */ + DBG_REQ(request, "Content-Length unknown, HTTP/1.1 -> Connection: will keep-alive with chunked response"); request->state.chunked_response = true; request->state.keep_alive = true; } else { /* On HTTP 1.0, we can only resort to closing the connection. */ + DBG_REQ(request, "Content-Length unknown, HTTP/1.10 -> will close"); request->state.keep_alive = false; } } else { /* We know the content-length. Can always keep-alive. */ + DBG_REQ(request, "Content-Length known -> will keep alive"); request->state.keep_alive = true; } } else { /* Explicit "Connection: close" (HTTP 1.1) or missing "Connection: keep-alive" (HTTP 1.0) */ + DBG_REQ(request, "Connection: close request by client"); request->state.keep_alive = false; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern.egg-info/PKG-INFO new/bjoern-3.1.0/bjoern.egg-info/PKG-INFO --- old/bjoern-3.0.1/bjoern.egg-info/PKG-INFO 2019-06-07 12:32:21.000000000 +0200 +++ new/bjoern-3.1.0/bjoern.egg-info/PKG-INFO 2019-11-03 10:45:13.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: bjoern -Version: 3.0.1 +Version: 3.1.0 Summary: A screamingly fast Python 2 + 3 WSGI server written in C. Home-page: https://github.com/jonashaag/bjoern Author: Jonas Haag diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern.egg-info/SOURCES.txt new/bjoern-3.1.0/bjoern.egg-info/SOURCES.txt --- old/bjoern-3.0.1/bjoern.egg-info/SOURCES.txt 2019-06-07 12:32:21.000000000 +0200 +++ new/bjoern-3.1.0/bjoern.egg-info/SOURCES.txt 2019-11-03 10:45:13.000000000 +0100 @@ -21,6 +21,8 @@ bjoern/request.h bjoern/server.c bjoern/server.h +bjoern/statsd_tags.c +bjoern/statsd_tags.h bjoern/wsgi.c bjoern/wsgi.h bjoern.egg-info/PKG-INFO diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/bjoern.py new/bjoern-3.1.0/bjoern.py --- old/bjoern-3.0.1/bjoern.py 2019-05-27 16:04:07.000000000 +0200 +++ new/bjoern-3.1.0/bjoern.py 2019-11-03 10:43:15.000000000 +0100 @@ -6,7 +6,6 @@ _default_instance = None DEFAULT_LISTEN_BACKLOG = 1024 - def bind_and_listen(host, port=None, reuse_port=False, listen_backlog=DEFAULT_LISTEN_BACKLOG): if host.startswith("unix:@"): @@ -30,16 +29,25 @@ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) sock.bind((host, port)) + sock.setblocking(False) + sock.listen(listen_backlog) return sock -def server_run(sock, wsgi_app): - _bjoern.server_run(sock, wsgi_app) +def server_run(sock, wsgi_app, statsd=None): + args = [sock, wsgi_app] + + if _bjoern.features.get('has_statsd'): + if statsd: + args.extend([int(statsd['enable']), statsd['host'], statsd['port'], statsd['ns'], statsd.get('tags')]) + else: + args.extend([0, None, 0, None, None]) + + _bjoern.server_run(*args) -# Backwards compatibility API def listen(wsgi_app, host, port=None, reuse_port=False, listen_backlog=DEFAULT_LISTEN_BACKLOG): """ @@ -56,6 +64,8 @@ listen_backlog=listen_backlog) _default_instance = (sock, wsgi_app) + return sock + def run(*args, **kwargs): """ @@ -68,6 +78,8 @@ """ global _default_instance + statsd = kwargs.pop('statsd', None) + if args or kwargs: # Called as `bjoern.run(wsgi_app, host, ...)` listen(*args, **kwargs) @@ -80,7 +92,7 @@ sock, wsgi_app = _default_instance try: - server_run(sock, wsgi_app) + server_run(sock, wsgi_app, statsd) finally: if sock.family == socket.AF_UNIX: filename = sock.getsockname() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/setup.py new/bjoern-3.1.0/setup.py --- old/bjoern-3.0.1/setup.py 2019-06-07 12:31:52.000000000 +0200 +++ new/bjoern-3.1.0/setup.py 2019-11-03 10:44:14.000000000 +0100 @@ -2,18 +2,36 @@ import glob from setuptools import setup, Extension +WANT_SIGINT_HANDLING = os.environ.get('BJOERN_WANT_SIGINT_HANDLING', True) +WANT_SIGNAL_HANDLING = os.environ.get('BJOERN_WANT_SIGNAL_HANDLING', True) +SIGNAL_CHECK_INTERVAL = os.environ.get('BJOERN_SIGNAL_CHECK_INTERVAL', '0.1') +WANT_STATSD = os.environ.get('BJOERN_WANT_STATSD', False) +WANT_STATSD_TAGS = os.environ.get('BJOERN_WANT_STATSD_TAGS', False) + +compile_flags = [('SIGNAL_CHECK_INTERVAL', SIGNAL_CHECK_INTERVAL)] +if WANT_SIGNAL_HANDLING: + compile_flags.append(('WANT_SIGNAL_HANDLING', 'yes')) +if WANT_SIGINT_HANDLING: + compile_flags.append(('WANT_SIGINT_HANDLING', 'yes')) +if WANT_STATSD: + compile_flags.append(('WANT_STATSD', 'yes')) + if WANT_STATSD_TAGS: + compile_flags.append(('WANT_STATSD_TAGS', 'yes')) + SOURCE_FILES = [os.path.join('http-parser', 'http_parser.c')] + \ + [os.path.join('statsd-c-client', 'statsd-client.c')] + \ sorted(glob.glob(os.path.join('bjoern', '*.c'))) +if not WANT_STATSD: + SOURCE_FILES.remove('statsd-c-client/statsd-client.c') + SOURCE_FILES.remove('bjoern/statsd_tags.c') + bjoern_extension = Extension( '_bjoern', sources = SOURCE_FILES, libraries = ['ev'], - include_dirs = ['http-parser', '/usr/include/libev'], - define_macros = [('WANT_SENDFILE', '1'), - ('WANT_SIGINT_HANDLING', '1'), - ('WANT_SIGNAL_HANDLING', '1'), - ('SIGNAL_CHECK_INTERVAL', '0.1')], + include_dirs = ['http-parser', 'statsd-c-client', '/usr/include/libev', '/opt/local/include'], + define_macros = compile_flags, extra_compile_args = ['-std=c99', '-fno-strict-aliasing', '-fcommon', '-fPIC', '-Wall', '-Wextra', '-Wno-unused-parameter', '-Wno-missing-field-initializers', '-g'] @@ -26,7 +44,7 @@ license = '2-clause BSD', url = 'https://github.com/jonashaag/bjoern', description = 'A screamingly fast Python 2 + 3 WSGI server written in C.', - version = '3.0.1', + version = '3.1.0', classifiers = ['Development Status :: 4 - Beta', 'License :: OSI Approved :: BSD License', 'Programming Language :: C', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/bjoern-3.0.1/tests/hello.py new/bjoern-3.1.0/tests/hello.py --- old/bjoern-3.0.1/tests/hello.py 2017-08-31 10:28:13.000000000 +0200 +++ new/bjoern-3.1.0/tests/hello.py 2019-10-19 14:58:30.000000000 +0200 @@ -29,4 +29,4 @@ return choice(apps)(env, start_response) if __name__ == '__main__': - bjoern.run(wsgi_app, '0.0.0.0', 8080) + bjoern.run(wsgi_app, '127.0.0.1', 8080)