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

Add streaming proxy feature #1373

Merged
merged 1 commit into from
Jun 28, 2019
Merged

Add streaming proxy feature #1373

merged 1 commit into from
Jun 28, 2019

Conversation

kraih
Copy link
Member

@kraih kraih commented Jun 27, 2019

This is an implementation of the streaming proxy described in #1295.

@kraih
Copy link
Member Author

kraih commented Jun 27, 2019

The core functionality seems pretty solid, and so do the new low level methods and attributes (naming could use some bikeshedding probably).

What i do not quite like yet is the API of the helper, but i can't think of a better alternative. I originally planned to use the returned promise for response rewriting (adding headers and so on). Unfortunately the promise would have to be resolved right in between the body and read events of the forwarded response, which is impossible because resolving a promise requires another reactor tick. So instead i had to use a callback passed as an argument to start_p, which looks rather silly.

The helper API might be a blocker for this feature.

@kraih
Copy link
Member Author

kraih commented Jun 27, 2019

This script illustrates the problem.

use Mojo::Base -strict;

use Mojo::EventEmitter;
use Mojo::IOLoop;
use Mojo::Promise;
use Mojo::Util 'dumper';

my $promise = Mojo::Promise->new;
my $events  = Mojo::EventEmitter->new;

# We want to use a promise to change the "foo" value
my $response = {foo => 'before'};
$promise->then(sub {
  $response->{foo} = 'after';
});

# The first event resolves the promise, and the second prints the "foo" value
$events->on(
  one => sub {
    $promise->resolve();
  }
);
$events->on(
  two => sub {
    say dumper $response;
  }
);

# Events happen in the right order, but the promise only resolves after the
# second event because it requires a reactor tick
$events->emit('one')->emit('two');
Mojo::IOLoop->one_tick;

@kraih
Copy link
Member Author

kraih commented Jun 27, 2019

Having mentioned the negative, on the plus side the streaming proxy has been up to 30 times faster than previously used proxy implementations for Mojolicious.

use Mojolicious::Lite;

my $UPSTREAM = 'http://127.0.0.1:3000';

# Old way (caching the whole response)
get '/proxy1' => sub {
  my $c = shift;

  $c->ua->get_p($UPSTREAM)->then(sub {
    my $tx = shift;
    $c->tx->res($tx->res);
    $c->rendered;
  });
};

# New way (streaming the response as chunks of data arrive)
get '/proxy2' => sub {
  my $c = shift;

  $c->proxy->get_p($UPSTREAM);
};

app->start;

And response size does not matter at all, it can even be an infinite EventSource stream. Throttling works automatically, so if one direction is faster than the other the proxy will not cache more than 1MB of data (this is configurable).

$ wrk -c 100 -d 10 http://127.0.0.1:8080/proxy2
Running 10s test @ http://127.0.0.1:8080/proxy2
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    46.31ms   31.32ms 402.74ms   92.33%
    Req/Sec   309.83     67.12   434.00     78.50%
  6178 requests in 10.01s, 6.04GB read
  Socket errors: connect 0, read 24, write 0, timeout 0
Requests/sec:    617.04
Transfer/sec:    617.52MB

$ wrk -c 100 -d 10 http://127.0.0.1:8080/proxy1
Running 10s test @ http://127.0.0.1:8080/proxy1
  2 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.23s   258.57ms   1.96s    72.62%
    Req/Sec    22.03     25.85   141.00     85.07%
  200 requests in 10.03s, 204.65MB read
  Socket errors: connect 0, read 0, write 0, timeout 116
Requests/sec:     19.95
Transfer/sec:     20.41MB

This test was performed with 1MB responses from another Mojolicious application on a Linux machine with the page cache disabled, to simulate a worst case scenario.

@kraih
Copy link
Member Author

kraih commented Jun 27, 2019

I'm calling for a vote on this @mojolicious/core.

@kraih kraih marked this pull request as ready for review June 27, 2019 16:56
@kraih
Copy link
Member Author

kraih commented Jun 27, 2019

This PR also contains some small optimizations for streaming responses in general. Found those bottlenecks while benchmarking the proxy helper.

Copy link
Member

@jhthorsen jhthorsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I apologise in advance if my questions are idiotic. Please ignore me if that is the case.

I'm very much positive to the change in general and I more than welcome a simple proxy helper.

lib/Mojo/Headers.pm Show resolved Hide resolved
lib/Mojolicious/Plugin/DefaultHelpers.pm Outdated Show resolved Hide resolved
lib/Mojolicious/Plugin/DefaultHelpers.pm Show resolved Hide resolved
lib/Mojolicious/Plugin/DefaultHelpers.pm Outdated Show resolved Hide resolved
lib/Mojolicious/Plugin/DefaultHelpers.pm Outdated Show resolved Hide resolved
@kraih
Copy link
Member Author

kraih commented Jun 27, 2019

Ok, the problem with the API has been resolved, this should be fine now. 😀

@kraih kraih force-pushed the stream_proxy branch 2 times, most recently from ed9db29 to add9d83 Compare June 27, 2019 21:42
@kraih
Copy link
Member Author

kraih commented Jun 28, 2019

The solution we settled on for the API problem was to just reuse the body event after the start_p call.

my $tx = $c->ua->build_tx(GET => 'http://mojolicious.org');
$c->proxy->start_p($tx);
$tx->res->content->once(body => sub {
  $c->res->headers->header('X-Proxy' => 'Mojo');
});

This gives us all the flexibility we need for rewriting HTTP responses.

@kraih
Copy link
Member Author

kraih commented Jun 28, 2019

Since there is a lot of potential for bugs, i suggest we keep the feature experimental for some time.

Copy link
Member

@jhthorsen jhthorsen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code makes sense and on a high level I'm giving the PR 👍

@kraih kraih force-pushed the stream_proxy branch 2 times, most recently from 331d0b7 to a11a493 Compare June 28, 2019 14:05
@jberger
Copy link
Member

jberger commented Jun 28, 2019

Looks good to me 👍 .

For posterity I want to leave a note that reminds us to document the timing (and values if any) of the resolution/rejection of the promise. For now, that can be deferred.

@kraih kraih merged commit b3232bb into master Jun 28, 2019
@kraih kraih added the vote label Jun 28, 2019
@kraih kraih deleted the stream_proxy branch September 10, 2019 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants