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

Small JRuby cleanups and optimizations #2012

Merged
merged 5 commits into from Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion History.md
@@ -1,7 +1,7 @@
## Master

* Features
* Your feature goes here (#Github Number)
* Optimize HTTP parser for JRuby (#2012)

* Bugfixes
* Fix socket activation of systemd (pre-existing) unix binder files (#1842, #1988)
Expand Down
58 changes: 21 additions & 37 deletions ext/puma_http11/http11_parser.java.rl
@@ -1,5 +1,7 @@
package org.jruby.puma;

import org.jruby.Ruby;
import org.jruby.RubyHash;
import org.jruby.util.ByteList;

public class Http11Parser {
Expand All @@ -19,44 +21,35 @@ public class Http11Parser {
}

action start_value { parser.mark = fpc; }
action write_value {
if(parser.http_field != null) {
parser.http_field.call(parser.data, parser.field_start, parser.field_len, parser.mark, fpc-parser.mark);
}
action write_value {
Http11.http_field(runtime, parser.data, parser.buffer, parser.field_start, parser.field_len, parser.mark, fpc-parser.mark);
}
action request_method {
if(parser.request_method != null)
parser.request_method.call(parser.data, parser.mark, fpc-parser.mark);
action request_method {
Http11.request_method(runtime, parser.data, parser.buffer, parser.mark, fpc-parser.mark);
}
action request_uri {
if(parser.request_uri != null)
parser.request_uri.call(parser.data, parser.mark, fpc-parser.mark);
action request_uri {
Http11.request_uri(runtime, parser.data, parser.buffer, parser.mark, fpc-parser.mark);
}
action fragment {
if(parser.fragment != null)
parser.fragment.call(parser.data, parser.mark, fpc-parser.mark);
action fragment {
Http11.fragment(runtime, parser.data, parser.buffer, parser.mark, fpc-parser.mark);
}

action start_query {parser.query_start = fpc; }
action query_string {
if(parser.query_string != null)
parser.query_string.call(parser.data, parser.query_start, fpc-parser.query_start);
action query_string {
Http11.query_string(runtime, parser.data, parser.buffer, parser.query_start, fpc-parser.query_start);
}

action http_version {
if(parser.http_version != null)
parser.http_version.call(parser.data, parser.mark, fpc-parser.mark);
action http_version {
Http11.http_version(runtime, parser.data, parser.buffer, parser.mark, fpc-parser.mark);
}

action request_path {
if(parser.request_path != null)
parser.request_path.call(parser.data, parser.mark, fpc-parser.mark);
Http11.request_path(runtime, parser.data, parser.buffer, parser.mark, fpc-parser.mark);
}

action done {
parser.body_start = fpc + 1;
if(parser.header_done != null)
parser.header_done.call(parser.data, fpc + 1, pe - fpc - 1);
parser.body_start = fpc + 1;
http.header_done(runtime, parser.data, parser.buffer, fpc + 1, pe - fpc - 1);
fbreak;
}

Expand All @@ -68,11 +61,11 @@ public class Http11Parser {
%% write data;

public static interface ElementCB {
public void call(Object data, int at, int length);
public void call(Ruby runtime, RubyHash data, ByteList buffer, int at, int length);
}

public static interface FieldCB {
public void call(Object data, int field, int flen, int value, int vlen);
public void call(Ruby runtime, RubyHash data, ByteList buffer, int field, int flen, int value, int vlen);
}

public static class HttpParser {
Expand All @@ -85,18 +78,9 @@ public class Http11Parser {
int field_len;
int query_start;

Object data;
RubyHash data;
ByteList buffer;

public FieldCB http_field;
public ElementCB request_method;
public ElementCB request_uri;
public ElementCB fragment;
public ElementCB request_path;
public ElementCB query_string;
public ElementCB http_version;
public ElementCB header_done;

public void init() {
cs = 0;

Expand All @@ -113,7 +97,7 @@ public class Http11Parser {

public final HttpParser parser = new HttpParser();

public int execute(ByteList buffer, int off) {
public int execute(Ruby runtime, Http11 http, ByteList buffer, int off) {
int p, pe;
int cs = parser.cs;
int len = buffer.length();
Expand Down
216 changes: 103 additions & 113 deletions ext/puma_http11/org/jruby/puma/Http11.java
Expand Up @@ -11,7 +11,6 @@
import org.jruby.anno.JRubyMethod;

import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

import org.jruby.exceptions.RaiseException;
Expand All @@ -20,6 +19,7 @@

/**
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
* @author <a href="mailto:headius@headius.com">Charles Oliver Nutter</a>
*/
public class Http11 extends RubyObject {
public final static int MAX_FIELD_NAME_LENGTH = 256;
Expand All @@ -37,6 +37,16 @@ public class Http11 extends RubyObject {
public final static int MAX_HEADER_LENGTH = 1024 * (80 + 32);
public final static String MAX_HEADER_LENGTH_ERR = "HTTP element HEADER is longer than the 114688 allowed length.";

public static final ByteList CONTENT_TYPE_BYTELIST = new ByteList(ByteList.plain("CONTENT_TYPE"));
public static final ByteList CONTENT_LENGTH_BYTELIST = new ByteList(ByteList.plain("CONTENT_LENGTH"));
public static final ByteList HTTP_PREFIX_BYTELIST = new ByteList(ByteList.plain("HTTP_"));
public static final ByteList COMMA_SPACE_BYTELIST = new ByteList(ByteList.plain(", "));
public static final ByteList REQUEST_METHOD_BYTELIST = new ByteList(ByteList.plain("REQUEST_METHOD"));
public static final ByteList REQUEST_URI_BYTELIST = new ByteList(ByteList.plain("REQUEST_URI"));
public static final ByteList FRAGMENT_BYTELIST = new ByteList(ByteList.plain("FRAGMENT"));
public static final ByteList REQUEST_PATH_BYTELIST = new ByteList(ByteList.plain("REQUEST_PATH"));
public static final ByteList QUERY_STRING_BYTELIST = new ByteList(ByteList.plain("QUERY_STRING"));
public static final ByteList HTTP_VERSION_BYTELIST = new ByteList(ByteList.plain("HTTP_VERSION"));

private static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
public IRubyObject allocate(Ruby runtime, RubyClass klass) {
Expand All @@ -53,127 +63,102 @@ public static void createHttp11(Ruby runtime) {
}

private Ruby runtime;
private RubyClass eHttpParserError;
private Http11Parser hp;
private RubyString body;

public Http11(Ruby runtime, RubyClass clazz) {
super(runtime,clazz);
this.runtime = runtime;
this.eHttpParserError = (RubyClass)runtime.getModule("Puma").getConstant("HttpParserError");
this.hp = new Http11Parser();
this.hp.parser.http_field = http_field;
this.hp.parser.request_method = request_method;
this.hp.parser.request_uri = request_uri;
this.hp.parser.fragment = fragment;
this.hp.parser.request_path = request_path;
this.hp.parser.query_string = query_string;
this.hp.parser.http_version = http_version;
this.hp.parser.header_done = header_done;
this.hp.parser.init();
}

public void validateMaxLength(int len, int max, String msg) {
public static void validateMaxLength(Ruby runtime, int len, int max, String msg) {
if(len>max) {
throw new RaiseException(runtime, eHttpParserError, msg, true);
throw newHTTPParserError(runtime, msg);
}
}

private Http11Parser.FieldCB http_field = new Http11Parser.FieldCB() {
public void call(Object data, int field, int flen, int value, int vlen) {
RubyHash req = (RubyHash)data;
RubyString f;
IRubyObject v;
validateMaxLength(flen, MAX_FIELD_NAME_LENGTH, MAX_FIELD_NAME_LENGTH_ERR);
validateMaxLength(vlen, MAX_FIELD_VALUE_LENGTH, MAX_FIELD_VALUE_LENGTH_ERR);

ByteList b = new ByteList(Http11.this.hp.parser.buffer,field,flen);
for(int i = 0,j = b.length();i<j;i++) {
if((b.get(i) & 0xFF) == '-') {
b.set(i, (byte)'_');
} else {
b.set(i, (byte)Character.toUpperCase((char)b.get(i)));
}
}

String as = b.toString();

if(as.equals("CONTENT_LENGTH") || as.equals("CONTENT_TYPE")) {
f = RubyString.newString(runtime, b);
} else {
f = RubyString.newString(runtime, "HTTP_");
f.cat(b);
}

b = new ByteList(Http11.this.hp.parser.buffer, value, vlen);
v = req.op_aref(req.getRuntime().getCurrentContext(), f);
if (v.isNil()) {
req.op_aset(req.getRuntime().getCurrentContext(), f, RubyString.newString(runtime, b));
} else {
RubyString vs = v.convertToString();
vs.cat(RubyString.newString(runtime, ", "));
vs.cat(b);
}
}
};
private static RaiseException newHTTPParserError(Ruby runtime, String msg) {
return runtime.newRaiseException(getHTTPParserError(runtime), msg);
}

private Http11Parser.ElementCB request_method = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
RubyHash req = (RubyHash)data;
RubyString val = RubyString.newString(runtime,new ByteList(hp.parser.buffer,at,length));
req.op_aset(req.getRuntime().getCurrentContext(), runtime.newString("REQUEST_METHOD"),val);
}
};

private Http11Parser.ElementCB request_uri = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
RubyHash req = (RubyHash)data;
validateMaxLength(length, MAX_REQUEST_URI_LENGTH, MAX_REQUEST_URI_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(hp.parser.buffer,at,length));
req.op_aset(req.getRuntime().getCurrentContext(), runtime.newString("REQUEST_URI"),val);
}
};

private Http11Parser.ElementCB fragment = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
RubyHash req = (RubyHash)data;
validateMaxLength(length, MAX_FRAGMENT_LENGTH, MAX_FRAGMENT_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(hp.parser.buffer,at,length));
req.op_aset(req.getRuntime().getCurrentContext(), runtime.newString("FRAGMENT"),val);
}
};

private Http11Parser.ElementCB request_path = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
RubyHash req = (RubyHash)data;
validateMaxLength(length, MAX_REQUEST_PATH_LENGTH, MAX_REQUEST_PATH_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(hp.parser.buffer,at,length));
req.op_aset(req.getRuntime().getCurrentContext(), runtime.newString("REQUEST_PATH"),val);
}
};

private Http11Parser.ElementCB query_string = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
RubyHash req = (RubyHash)data;
validateMaxLength(length, MAX_QUERY_STRING_LENGTH, MAX_QUERY_STRING_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(hp.parser.buffer,at,length));
req.op_aset(req.getRuntime().getCurrentContext(), runtime.newString("QUERY_STRING"),val);
}
};
private static RubyClass getHTTPParserError(Ruby runtime) {
// Cheaper to look this up lazily than cache eagerly and consume a field, since it's rarely encountered
return (RubyClass)runtime.getModule("Puma").getConstant("HttpParserError");
}

private Http11Parser.ElementCB http_version = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
RubyHash req = (RubyHash)data;
RubyString val = RubyString.newString(runtime,new ByteList(hp.parser.buffer,at,length));
req.op_aset(req.getRuntime().getCurrentContext(), runtime.newString("HTTP_VERSION"),val);
public static void http_field(Ruby runtime, RubyHash req, ByteList buffer, int field, int flen, int value, int vlen) {
RubyString f;
IRubyObject v;
validateMaxLength(runtime, flen, MAX_FIELD_NAME_LENGTH, MAX_FIELD_NAME_LENGTH_ERR);
validateMaxLength(runtime, vlen, MAX_FIELD_VALUE_LENGTH, MAX_FIELD_VALUE_LENGTH_ERR);

ByteList b = new ByteList(buffer,field,flen);
for(int i = 0,j = b.length();i<j;i++) {
int bite = b.get(i) & 0xFF;
if(bite == '-') {
b.set(i, (byte)'_');
} else {
b.set(i, (byte)Character.toUpperCase(bite));
}
};
}

private Http11Parser.ElementCB header_done = new Http11Parser.ElementCB() {
public void call(Object data, int at, int length) {
body = RubyString.newString(runtime, new ByteList(hp.parser.buffer, at, length));
}
};
if (b.equals(CONTENT_LENGTH_BYTELIST) || b.equals(CONTENT_TYPE_BYTELIST)) {
f = RubyString.newString(runtime, b);
} else {
f = RubyString.newStringShared(runtime, HTTP_PREFIX_BYTELIST);
f.cat(b);
}

b = new ByteList(buffer, value, vlen);
v = req.fastARef(f);
if (v == null || v.isNil()) {
req.fastASet(f, RubyString.newString(runtime, b));
} else {
RubyString vs = v.convertToString();
vs.cat(COMMA_SPACE_BYTELIST);
vs.cat(b);
}
}

public static void request_method(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
RubyString val = RubyString.newString(runtime,new ByteList(buffer,at,length));
req.fastASet(RubyString.newStringShared(runtime, REQUEST_METHOD_BYTELIST),val);
}

public static void request_uri(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
validateMaxLength(runtime, length, MAX_REQUEST_URI_LENGTH, MAX_REQUEST_URI_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(buffer,at,length));
req.fastASet(RubyString.newStringShared(runtime, REQUEST_URI_BYTELIST),val);
}

public static void fragment(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
validateMaxLength(runtime, length, MAX_FRAGMENT_LENGTH, MAX_FRAGMENT_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(buffer,at,length));
req.fastASet(RubyString.newStringShared(runtime, FRAGMENT_BYTELIST),val);
}

public static void request_path(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
validateMaxLength(runtime, length, MAX_REQUEST_PATH_LENGTH, MAX_REQUEST_PATH_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(buffer,at,length));
req.fastASet(RubyString.newStringShared(runtime, REQUEST_PATH_BYTELIST),val);
}

public static void query_string(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
validateMaxLength(runtime, length, MAX_QUERY_STRING_LENGTH, MAX_QUERY_STRING_LENGTH_ERR);
RubyString val = RubyString.newString(runtime,new ByteList(buffer,at,length));
req.fastASet(RubyString.newStringShared(runtime, QUERY_STRING_BYTELIST),val);
}

public static void http_version(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
RubyString val = RubyString.newString(runtime,new ByteList(buffer,at,length));
req.fastASet(RubyString.newStringShared(runtime, HTTP_VERSION_BYTELIST),val);
}

public void header_done(Ruby runtime, RubyHash req, ByteList buffer, int at, int length) {
body = RubyString.newStringShared(runtime, new ByteList(buffer, at, length));
}

@JRubyMethod
public IRubyObject initialize() {
Expand All @@ -195,19 +180,24 @@ public IRubyObject finish() {

@JRubyMethod
public IRubyObject execute(IRubyObject req_hash, IRubyObject data, IRubyObject start) {
int from = 0;
from = RubyNumeric.fix2int(start);
int from = RubyNumeric.fix2int(start);
ByteList d = ((RubyString)data).getByteList();
if(from >= d.length()) {
throw new RaiseException(runtime, eHttpParserError, "Requested start is after data buffer end.", true);
throw newHTTPParserError(runtime, "Requested start is after data buffer end.");
} else {
this.hp.parser.data = req_hash;
this.hp.execute(d,from);
validateMaxLength(this.hp.parser.nread,MAX_HEADER_LENGTH, MAX_HEADER_LENGTH_ERR);
if(this.hp.has_error()) {
throw new RaiseException(runtime, eHttpParserError, "Invalid HTTP format, parsing fails.", true);
Http11Parser hp = this.hp;
Http11Parser.HttpParser parser = hp.parser;

parser.data = (RubyHash) req_hash;

hp.execute(runtime, this, d,from);

validateMaxLength(runtime, parser.nread,MAX_HEADER_LENGTH, MAX_HEADER_LENGTH_ERR);

if(hp.has_error()) {
throw newHTTPParserError(runtime, "Invalid HTTP format, parsing fails.");
} else {
return runtime.newFixnum(this.hp.parser.nread);
return runtime.newFixnum(parser.nread);
}
}
}
Expand Down