From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: AS15169 209.85.128.0/17 X-Spam-Status: No, score=-3.0 required=3.0 tests=AWL,BAYES_00,DKIM_SIGNED, DKIM_VALID,DKIM_VALID_AU,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,HTML_MESSAGE,RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H3,RCVD_IN_MSPIKE_WL,RCVD_IN_SORBS_SPAM,SPF_PASS shortcircuit=no autolearn=no autolearn_force=no version=3.4.0 Received: from mail-ua0-f188.google.com (mail-ua0-f188.google.com [209.85.217.188]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by dcvr.yhbt.net (Postfix) with ESMTPS id CFF311FF6D for ; Thu, 15 Dec 2016 20:07:09 +0000 (UTC) Received: by mail-ua0-f188.google.com with SMTP id 20sf9464532uak.1 for ; Thu, 15 Dec 2016 12:07:09 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=googlegroups.com; s=20161025; h=sender:mime-version:in-reply-to:references:from:date:message-id :subject:to:x-original-sender:x-original-authentication-results :reply-to:precedence:mailing-list:list-id:x-spam-checked-in-group :list-post:list-help:list-archive:list-subscribe:list-unsubscribe; bh=FH+6HX/OK3xhZCfkMtCrVpM5ltNOZGgurezAx5PuIro=; b=DmL0P6mBBSr8kOeRTfNeRpgMT9edsqQTOMQj8bIznn2xLIK7LK+24g9BLQunQiGPT4 ScNrI0jwAmX7L7DhqvlijE9Vd03t99tgV9GztPZDe9ewYkJJdCeljrEC1sWZrwmv7iP6 dardfsDuIsV7D93gRLIkOPpm7+EkXgO1tKYeBV475UxfETzLYPpOhi8NuQ2lzEUILA/r 6gKCxd0CSMN8V1HVXwfFuIwdlVOfU/pl3qzWKzEwNX9TOe3jhw7WX0HSGE+jXkFzQs2H 11iDZ1PoZL/MvqvKCYaOLc6XY1nwk7ztZOGIkVWIBACLOQSgmc2ct0b4IXX/NSWGWPjm IKwA== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=mime-version:in-reply-to:references:from:date:message-id:subject:to :x-original-sender:x-original-authentication-results:reply-to :precedence:mailing-list:list-id:x-spam-checked-in-group:list-post :list-help:list-archive:list-subscribe:list-unsubscribe; bh=FH+6HX/OK3xhZCfkMtCrVpM5ltNOZGgurezAx5PuIro=; b=KE/kUPSOA1liSn8F524tlrZu7dlfiFXad5JOkdOVxy0bDfTSrsLcB+XNirygYLOADe zYEEUb5QBnd6O4GbyWvJ2gPNefiR5czu6LvaOURCHscLM9GiLOn+zvF6FBYMzYZphW8F 3QDC7go8lTUV5cKTfo/x1fDw3RB9bqiU7apw6WZlBEXhH1gHMYnypNYNSwlpHp+qgGNY sBCO9tKtQusDlhtxax+ijOFggqVYMQxpfeFKVGrOdE32Dvya3Vhh5q55ZUXzGGfxfxfl Ul4Hyr6owYDMCn+7ihLm0zPu47p+hEp7alJJRBcpzvLAceIZULNSOB/EyB4cdeCbH6qC ftgg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=sender:x-gm-message-state:mime-version:in-reply-to:references:from :date:message-id:subject:to:x-original-sender :x-original-authentication-results:reply-to:precedence:mailing-list :list-id:x-spam-checked-in-group:list-post:list-help:list-archive :list-subscribe:list-unsubscribe; bh=FH+6HX/OK3xhZCfkMtCrVpM5ltNOZGgurezAx5PuIro=; b=W6MEqNcYbcNc3GdtfDB6pTq+RFO7GkI6sqQyAljFcJM0X1jC/GqfDSMoKqzogTdOmM uUpR7FyAeK0OLT4HOa1amnfDRy8IQHg4+wCXWZt+aQ2x7adSnVG05fw4CFuaiLMcdVD0 FcT+J1uy0yuQ5UIlhn00mh+L8ZUyYQUjX/2T8BhUMhuiCBw7nXjd9KR/3uIb2vUbfgFj FO2oHia7duUW/x6SGZ8A2f5ev8aLPJdH8mLdRP7Z6G3rf7R1g0iOR0Z5f04MBasuFHrC +IcEQ8Jm3k8XL9QofLj+siu+i0kD/Xc8g5ougJffuoBnqjh0BdW8EqBVbwm7FqIdfESO vR9g== Sender: rack-devel@googlegroups.com X-Gm-Message-State: AKaTC02MWLGU3h+3k4PxEH9I8IkIbrMKgUkyxAxHRBfqSfHb2AGrO1WL7Zi/7xcFV2pNwQ== X-Received: by 10.36.158.136 with SMTP id p130mr15680itd.2.1481832428831; Thu, 15 Dec 2016 12:07:08 -0800 (PST) X-BeenThere: rack-devel@googlegroups.com Received: by 10.107.147.7 with SMTP id v7ls2053939iod.6.gmail; Thu, 15 Dec 2016 12:07:08 -0800 (PST) X-Received: by 10.107.133.163 with SMTP id p35mr784001ioi.54.1481832428365; Thu, 15 Dec 2016 12:07:08 -0800 (PST) Received: from mail-io0-x230.google.com (mail-io0-x230.google.com. [2607:f8b0:4001:c06::230]) by gmr-mx.google.com with ESMTPS id j63si6320ita.1.2016.12.15.12.07.08 for (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Thu, 15 Dec 2016 12:07:08 -0800 (PST) Received-SPF: pass (google.com: domain of jftucker@gmail.com designates 2607:f8b0:4001:c06::230 as permitted sender) client-ip=2607:f8b0:4001:c06::230; Received: by mail-io0-x230.google.com with SMTP id d9so79160889ioe.0 for ; Thu, 15 Dec 2016 12:07:08 -0800 (PST) X-Received: by 10.107.34.74 with SMTP id i71mr3636276ioi.24.1481832427817; Thu, 15 Dec 2016 12:07:07 -0800 (PST) MIME-Version: 1.0 Received: by 10.107.30.14 with HTTP; Thu, 15 Dec 2016 12:07:07 -0800 (PST) In-Reply-To: <20161115-slow-clients-rack-vs-psgi@80x24.org> References: <20161115-slow-clients-rack-vs-psgi@80x24.org> From: James Tucker Date: Thu, 15 Dec 2016 12:07:07 -0800 Message-ID: Subject: Re: big responses to slow clients: Rack vs PSGI To: Rack Development Content-Type: multipart/alternative; boundary=001a1140f2547a34b90543b7ffaa X-Original-Sender: jftucker@gmail.com X-Original-Authentication-Results: gmr-mx.google.com; dkim=pass header.i=@gmail.com; spf=pass (google.com: domain of jftucker@gmail.com designates 2607:f8b0:4001:c06::230 as permitted sender) smtp.mailfrom=jftucker@gmail.com; dmarc=pass (p=NONE dis=NONE) header.from=gmail.com Reply-To: rack-devel@googlegroups.com Precedence: list Mailing-list: list rack-devel@googlegroups.com; contact rack-devel+owners@googlegroups.com List-ID: X-Google-Group-Id: 486215384060 List-Post: , List-Help: , List-Archive: , List-Unsubscribe: , --001a1140f2547a34b90543b7ffaa Content-Type: text/plain; charset=UTF-8 I agree with you, with a caveat. I think that the model is better for some specific use cases. _ry and I had this debate at reasonable length back before node, when he was writing flow and I prepped the first thin async patch/rack hack. The fundamental problem here is a competition of use cases and ideals. It would be ideal for server authors if Ruby had lightweight coroutines that could be scheduled across threads and relinquish control on IO. You can emulate some of this with Fibers, and Neverblock went really far down this path, as did Goliath. There's really no substitute for real user schedulable goroutines and a decent IO subsystem though - we can build the latter, but the prior is squarely in MRI's control - and they're now headed further down the path of thread isolation. Skipping a very large background and diatribe of these choices in MRI, I will say that the chosen path is really not suitable for very high scale processes. To be clear, I'm not saying "ruby doesn't scale" in the sense that we can't use it for services that handle Mqps and up. You can - it's not cheap, but it's entirely doable, and the engineering cost is on par with other choices, assuming reasonable problem solving. What I am saying is that Ruby, as designed and where it is headed, will not be ideal for tens of thousands of open sockets, or more. It does not have enough lightweight datastructures. It doesn't have the right IO primitives, and it doesn't have the right concurrency primitives. The newer concurrency primitives that are being discussed also make solving the general case of this problem harder. The general case assumes that task latencies are unpredictable, and in that space, you need a scheduler that can perform memory and cpu offloading. MRI will not deliver either of those capabilities. The reason I describe the above, is that if we accept that Ruby is only good for limited scale and cost per process, then we can view the problem a little differently. Instead of trying to force it, or constantly work around those limitations in library level systems, we can work around the problem in flow control and traffic management systems. Indeed this is what we do today, though as you note, often by accident/side effect. The kinds of catastrophic load balancing challenges such as the rap genius saga will remain, and slow client challenges will also. We can deal with them. At scale you never really get to escape these anyway, except by engineering systems to specifically combat those challenges. Either you're so fast and efficient that attacks are impractical, or you start needing to put appropriate limitations in place in upstream defenses. Ultimately the prior is brittle and as such the latter eventually gets deployed by SRE/sysadmins/ops folks. I also don't mean to say that it would be bad to explore some more of these ideas. I think it would, particularly if it could lead to more examples of what servers need in order to be efficient, as strong cases for MRI/Ruby designers to consider. I also think there are good cases to be made for alternative servers for specific use cases - I know you specifically Eric have done great work in this area. I would encourage you, absolutely, to freely depart from Rack for those use cases. I'd also be really happy to eat my words if you find some way of taking on elixir style scalability with MRI, though on a personal level I don't know if it's worth the time. As always, thank you for everything you've done, and for the discussion. On Nov 15, 2016 3:10 PM, "Eric Wong" wrote: > I've been poking around in Plack/PSGI for Perl5 some months, > and am liking it in some ways more than Rack. > > This only covers server-agnostic web applications; IMHO exposing > applications to server-specific stuff defeats the purpose of > these common specs. > > In Rack, one major problem I have is streaming large responses > requires calling body.each synchronously. > > For handling writing large responses to slow clients, this means > a Rack web server has 2 choices: > > > 1) Block the calling Thread, Fiber, or process until the > slow client can consume the input. This hurts if you have > many slow clients blocking all your threads. > > body.each { |buf| client.write(buf) } > body.close > > Simple, but your app is at the mercy of how fast the client > chooses to read the response. > > > 2) Detect :wait_writable/:wait_readable (EAGAIN) when writing to > the slow client and start buffering the response to memory or > filesystem. > > This may lead to out-of-memory or out-of-storage conditions. > > nginx does this by default when proxying, so Rubyists are > often unaware of this as it's common to use nginx in front > of Rack servers for this purpose. > > Something like the following should handle slow clients > without relying on nginx for buffering: > > tmp = nil > body.each do |buf| > if tmp > tmp.write(buf) > else > # the optimistic case: > case ret = client.write_nonblock(buf, exception: false) > when :wait_writable, :wait_writable # EAGAIN :< > tmp = Tempfile.new(ret.to_s) > tmp.write(buf) > when Integer > exp = buf.bytesize > if exp > ret # partial write :< > tmp = Tempfile.new('partial') > tmp.write(buf.byteslice(ret, exp - ret)) > end > end > end > end > > if tmp > server_specific_finish(client, tmp, body) > else > body.close if body.respond_to?(:close) > end > > Gross; but smaller responses never get buffered this way. > Any server-specific logic is still contained within the > server itself, the Rack app may remain completely unaware > of how a server handles slow clients. > > > > PSGI allows at least two methods for streaming large responses. > I will only cover the "pull" method of getline+close below. > > Naively, getline+close is usable like the Rack method 1) for > body.each: > > # Note: "getline" in Plack/PSGI is not required to return > # a "line", so it can behave like "readpartial" in Ruby. > while (defined(my $buf = $body->getline)) { > $client->write($buf); > } > $body->close; > > ...With all the problems of blocking on the $client->write call. > > On the surface, the difference between Rack and PSGI here is > minor. > > > However, "getline" yielding control to the server entirely has a > significant advantage over the Rack app calling a Proc provided > by the server: The server can stop calling $body->getline once > it detects a client is slow. > > # For the non-Perl-literate, it's pretty similar to Ruby. > # Scalar variables are prefixed with $, and method. > # calls are "$foo->METHOD" instead of "foo.METHOD" in Ruby > # if/else/elsif/while all work the same as in Ruby > # I will over-comment here assuming readers here are not > # familiar with Perl. > > # Make client socking non-blocking, equivalent to > # "IO#nonblock = true" in Ruby; normal servers would only > # call this once after accept()-ing a connection. > $client->blocking(0); > > my $blocked; # "my" declares a locally-scoped variable > > # "undef" in Perl are the equivalent of "nil" in Ruby, > # so "defined" checks here are equivalent to Ruby nil checks > while (defined(my $buf = $body->getline)) { > # length($buf) is roughly buf.bytesize in Ruby; > # I'll assume all data is binary since Perl's Unicode > # handling confuses me no matter how many times I RTFM. > my $exp = length($buf); > > # Behaves like Ruby IO#write_nonblock after the > # $client->blocking(0) call above: > my $ret = $client->syswrite($buf); > > # $ret is the number of bytes written on success: > if (defined $ret) { > if ($exp > $ret) { # partial write :< > > # similar to String#byteslice in Ruby: > $blocked = substr($buf, $ret, $exp - $ret); > > last; # break out of the while loop > } # else { continue looping on while } > > # $! is the system errno from syswrite (see perlvar manpage > # for details), $!{E****} just checks for $! matching the > # particular error number. > } elsif ($!{EAGAIN} || $!{EWOULDBLOCK}) { > # A minor detail in this example: > # this assignment is a copy, so equivalent to > # "blocked = buf.dup" in Ruby, NOT merely > # "blocked = buf". > $blocked = $buf; > > last; # break out of the while loop > } else { > # Perl does not raise exceptions by default on > # syscall errors, "die" is the standard exception > # throwing mechanism: > die "syswrite failed: $!\n"; > } > } > if (defined $blocked) { > server_specific_finish($client, $blocked, $body); > } else { > $body->close; > } > > In both my Rack and PSGI examples, I have a reference to a > server_specific_finish call. In the Rack example, this method > will stream the entire contents of tmp (a Tempfile) to the > client. > > The problem is tmp in the Rack example may be as large as > the entire response. This sucks for big responses. > > In the PSGI example, the server_specific_finish call will only > have the contents of one buffer from $body->getline in memory at > a time. The server will make further calls to $body->getline > when (and only when) the previous buffer is fully-written to the > client socket. There is only one (app-provided) buffer in > server memory at once, not entire response. > > Both server_specific_finish calls will call the "close" method > on the body when the entire response is written to the client > socket. Delaying the "close" call may make sense for logging > purposes in Rack, even if body.each is long done running, and is > obviously required in the PSGI case since further "getline" > calls need to be made before "close". > > > The key difference is that in Rack, the data is "pushed" to the > server by the Rack app. In PSGI, the app may instead ask the > server to "pull" that data. > > > Anyways, thanks for reading this far. I just felt like writing > something down for future Rack/Ruby-related projects. I'm not > sure if Rack can change without breaking all existing apps > and middlewares. > > -- > > --- > You received this message because you are subscribed to the Google Groups > "Rack Development" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to rack-devel+unsubscribe@googlegroups.com. > For more options, visit https://groups.google.com/d/optout. > -- --- You received this message because you are subscribed to the Google Groups "Rack Development" group. To unsubscribe from this group and stop receiving emails from it, send an email to rack-devel+unsubscribe@googlegroups.com. For more options, visit https://groups.google.com/d/optout. --001a1140f2547a34b90543b7ffaa Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable

I agree with you, with a caveat. I think that the model= is better for some specific use cases.

_ry and I had this debate at = reasonable length back before node, when he was writing flow and I prepped = the first thin async patch/rack hack.

The fundamental problem here is= a competition of use cases and ideals. It would be ideal for server author= s if Ruby had lightweight coroutines that could be scheduled across threads= and relinquish control on IO. You can emulate some of this with Fibers, an= d Neverblock went really far down this path, as did Goliath. There's re= ally no substitute for real user schedulable goroutines and a decent IO sub= system though - we can build the latter, but the prior is squarely in MRI&#= 39;s control - and they're now headed further down the path of thread i= solation.

Skipping a very large background and diatribe of these choi= ces in MRI, I will say that the chosen path is really not suitable for very= high scale processes. To be clear, I'm not saying "ruby doesn'= ;t scale" in the sense that we can't use it for services that hand= le Mqps and up. You can - it's not cheap, but it's entirely doable,= and the engineering cost is on par with other choices, assuming reasonable= problem solving. What I am saying is that Ruby, as designed and where it i= s headed, will not be ideal for tens of thousands of open sockets, or more.= It does not have enough lightweight datastructures. It doesn't have th= e right IO primitives, and it doesn't have the right concurrency primit= ives. The newer concurrency primitives that are being discussed also make s= olving the general case of this problem harder. The general case assumes th= at task latencies are unpredictable, and in that space, you need a schedule= r that can perform memory and cpu offloading. MRI will not deliver either o= f those capabilities.

The reason I describe the above, is that if we = accept that Ruby is only good for limited scale and cost per process, then = we can view the problem a little differently. Instead of trying to force it= , or constantly work around those limitations in library level systems, we = can work around the problem in flow control and traffic management systems.= Indeed this is what we do today, though as you note, often by accident/sid= e effect. The kinds of catastrophic load balancing challenges such as the r= ap genius saga will remain, and slow client challenges will also. We can de= al with them. At scale you never really get to escape these anyway, except = by engineering systems to specifically combat those challenges. Either you&= #39;re so fast and efficient that attacks are impractical, or you start nee= ding to put appropriate limitations in place in upstream defenses. Ultimate= ly the prior is brittle and as such the latter eventually gets deployed by = SRE/sysadmins/ops folks.

I also don't mean to say that it would b= e bad to explore some more of these ideas. I think it would, particularly i= f it could lead to more examples of what servers need in order to be effici= ent, as strong cases for MRI/Ruby designers to consider. I also think there= are good cases to be made for alternative servers for specific use cases -= I know you specifically Eric have done great work in this area. I would en= courage you, absolutely, to freely depart from Rack for those use cases. I&= #39;d also be really happy to eat my words if you find some way of taking o= n elixir style scalability with MRI, though on a personal level I don't= know if it's worth the time. As always, thank you for everything you&#= 39;ve done, and for the discussion.


On Nov 15, 2016 3= :10 PM, "Eric Wong" <e@80x24.org> wrote:
I've been poking around in Plack/PSGI for Perl5 some mon= ths,
and am liking it in some ways more than Rack.

This only covers server-agnostic web applications; IMHO exposing
applications to server-specific stuff defeats the purpose of
these common specs.

In Rack, one major problem I have is streaming large responses
requires calling body.each synchronously.

For handling writing large responses to slow clients, this means
a Rack web server has 2 choices:


1) Block the calling Thread, Fiber, or process until the
=C2=A0 =C2=A0slow client can consume the input.=C2=A0 This hurts if you hav= e
=C2=A0 =C2=A0many slow clients blocking all your threads.

=C2=A0 =C2=A0 =C2=A0 body.each { |buf| client.write(buf) }
=C2=A0 =C2=A0 =C2=A0 body.close

=C2=A0 =C2=A0Simple, but your app is at the mercy of how fast the client =C2=A0 =C2=A0chooses to read the response.


2) Detect :wait_writable/:wait_readable (EAGAIN) when writing to
=C2=A0 =C2=A0the slow client and start buffering the response to memory or<= br> =C2=A0 =C2=A0filesystem.

=C2=A0 =C2=A0This may lead to out-of-memory or out-of-storage conditions.
=C2=A0 =C2=A0nginx does this by default when proxying, so Rubyists are
=C2=A0 =C2=A0often unaware of this as it's common to use nginx in front=
=C2=A0 =C2=A0of Rack servers for this purpose.

=C2=A0 =C2=A0Something like the following should handle slow clients
=C2=A0 =C2=A0without relying on nginx for buffering:

=C2=A0 =C2=A0 =C2=A0 tmp =3D nil
=C2=A0 =C2=A0 =C2=A0 body.each do |buf|
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if tmp
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 tmp.write(buf)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 else
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # the optimistic case:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 case ret =3D client.write_nonblock(buf, = exception: false)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 when :wait_writable, :wait_writable # EA= GAIN :<
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 tmp =3D Tempfile.new(ret.to_s) =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 tmp.write(buf)
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 when Integer
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 exp =3D buf.bytesize
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if exp > ret # partial write := <
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0tmp =3D Tempfile.new= ('partial')
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0tmp.write(buf.bytesl= ice(ret, exp - ret))
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 end
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 end
=C2=A0 =C2=A0 =C2=A0 =C2=A0 end
=C2=A0 =C2=A0 =C2=A0 end

=C2=A0 =C2=A0 =C2=A0 if tmp
=C2=A0 =C2=A0 =C2=A0 =C2=A0 server_specific_finish(client, tmp, body)
=C2=A0 =C2=A0 =C2=A0 else
=C2=A0 =C2=A0 =C2=A0 =C2=A0 body.close if body.respond_to?(:close)
=C2=A0 =C2=A0 =C2=A0 end

=C2=A0 =C2=A0Gross; but smaller responses never get buffered this way.
=C2=A0 =C2=A0Any server-specific logic is still contained within the
=C2=A0 =C2=A0server itself, the Rack app may remain completely unaware
=C2=A0 =C2=A0of how a server handles slow clients.



PSGI allows at least two methods for streaming large responses.
I will only cover the "pull" method of getline+close below.

Naively, getline+close is usable like the Rack method 1) for
body.each:

=C2=A0 =C2=A0 =C2=A0 # Note: "getline" in Plack/PSGI is not requi= red to return
=C2=A0 =C2=A0 =C2=A0 # a "line", so it can behave like "read= partial" in Ruby.
=C2=A0 =C2=A0 =C2=A0 while (defined(my $buf =3D $body->getline)) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 $client->write($buf);
=C2=A0 =C2=A0 =C2=A0 }
=C2=A0 =C2=A0 =C2=A0 $body->close;

...With all the problems of blocking on the $client->write call.

On the surface, the difference between Rack and PSGI here is
minor.


However, "getline" yielding control to the server entirely has a<= br> significant advantage over the Rack app calling a Proc provided
by the server: The server can stop calling $body->getline once
it detects a client is slow.

=C2=A0 =C2=A0 =C2=A0 # For the non-Perl-literate, it's pretty similar t= o Ruby.
=C2=A0 =C2=A0 =C2=A0 # Scalar variables are prefixed with $, and method. =C2=A0 =C2=A0 =C2=A0 # calls are "$foo->METHOD" instead of &qu= ot;foo.METHOD" in Ruby
=C2=A0 =C2=A0 =C2=A0 # if/else/elsif/while all work the same as in Ruby
=C2=A0 =C2=A0 =C2=A0 # I will over-comment here assuming readers here are n= ot
=C2=A0 =C2=A0 =C2=A0 # familiar with Perl.

=C2=A0 =C2=A0 =C2=A0 # Make client socking non-blocking, equivalent to
=C2=A0 =C2=A0 =C2=A0 # "IO#nonblock =3D true" in Ruby; normal ser= vers would only
=C2=A0 =C2=A0 =C2=A0 # call this once after accept()-ing a connection.
=C2=A0 =C2=A0 =C2=A0 $client->blocking(0);

=C2=A0 =C2=A0 =C2=A0 my $blocked; # "my" declares a locally-scope= d variable

=C2=A0 =C2=A0 =C2=A0 # "undef" in Perl are the equivalent of &quo= t;nil" in Ruby,
=C2=A0 =C2=A0 =C2=A0 # so "defined" checks here are equivalent to= Ruby nil checks
=C2=A0 =C2=A0 =C2=A0 while (defined(my $buf =3D $body->getline)) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # length($buf) is roughly buf.bytesize i= n Ruby;
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # I'll assume all data is binary sin= ce Perl's Unicode
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # handling confuses me no matter how man= y times I RTFM.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 my $exp =3D length($buf);

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # Behaves like Ruby IO#write_nonblock af= ter the
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # $client->blocking(0) call above: =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 my $ret =3D $client->syswrite($buf);<= br>
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # $ret is the number of bytes written on= success:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if (defined $ret) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 if ($exp > $ret) { # pa= rtial write :<

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # similar to= String#byteslice in Ruby:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 $blocked =3D= substr($buf, $ret, $exp - $ret);

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 last; # brea= k out of the while loop
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 } # else { continue loopin= g on while }

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # $! is the system errno from syswrite (= see perlvar manpage
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # for details), $!{E****} just checks fo= r $! matching the
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # particular error number.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 } elsif ($!{EAGAIN} || $!{EWOULDBLOCK}) = {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # A minor detail in this e= xample:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # this assignment is a cop= y, so equivalent to
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # "blocked =3D buf.du= p" in Ruby, NOT merely
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # "blocked =3D buf&qu= ot;.
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 $blocked =3D $buf;

=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 last; # break out of the w= hile loop
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 } else {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # Perl does not raise exce= ptions by default on
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # syscall errors, "di= e" is the standard exception
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 # throwing mechanism:
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 die "syswrite failed:= $!\n";
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 }
=C2=A0 =C2=A0 =C2=A0 }
=C2=A0 =C2=A0 =C2=A0 if (defined $blocked) {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 server_specific_finish($client, $bl= ocked, $body);
=C2=A0 =C2=A0 =C2=A0 } else {
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 $body->close;
=C2=A0 =C2=A0 =C2=A0 }

In both my Rack and PSGI examples, I have a reference to a
server_specific_finish call.=C2=A0 In the Rack example, this method
will stream the entire contents of tmp (a Tempfile) to the
client.

The problem is tmp in the Rack example may be as large as
the entire response.=C2=A0 This sucks for big responses.

In the PSGI example, the server_specific_finish call will only
have the contents of one buffer from $body->getline in memory at
a time.=C2=A0 The server will make further calls to $body->getline
when (and only when) the previous buffer is fully-written to the
client socket.=C2=A0 There is only one (app-provided) buffer in
server memory at once, not entire response.

Both server_specific_finish calls will call the "close" method on the body when the entire response is written to the client
socket.=C2=A0 Delaying the "close" call may make sense for loggin= g
purposes in Rack, even if body.each is long done running, and is
obviously required in the PSGI case since further "getline"
calls need to be made before "close".


The key difference is that in Rack, the data is "pushed" to the server by the Rack app.=C2=A0 In PSGI, the app may instead ask the
server to "pull" that data.


Anyways, thanks for reading this far.=C2=A0 I just felt like writing
something down for future Rack/Ruby-related projects.=C2=A0 I'm not
sure if Rack can change without breaking all existing apps
and middlewares.

--

---
You received this message because you are subscribed to the Google Groups &= quot;Rack Development" group.
To unsubscribe from this group and stop receiving emails from it, send an e= mail to rack-devel+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

--

---
You received this message because you are subscribed to the Google Groups &= quot;Rack Development" group.
To unsubscribe from this group and stop receiving emails from it, send an e= mail to
rack-dev= el+unsubscribe@googlegroups.com.
For more options, visit http= s://groups.google.com/d/optout.
--001a1140f2547a34b90543b7ffaa--