]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/NNTPdeflate.pm
352d4842834816a0df1523575aa4a6ea7f3326b3
[public-inbox.git] / lib / PublicInbox / NNTPdeflate.pm
1 # Copyright (C) all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
4 # RFC 8054 NNTP COMPRESS DEFLATE implementation
5 #
6 # RSS usage for 10K idle-but-did-something NNTP clients on 64-bit:
7 #   TLS + DEFLATE[a] :  1.8 GB  (MemLevel=9, 1.2 GB with MemLevel=8)
8 #   TLS + DEFLATE[b] :  ~300MB
9 #   TLS only         :  <200MB
10 #   plain            :   <50MB
11 #
12 # [a] - initial implementation using per-client Deflate contexts and buffer
13 #
14 # [b] - memory-optimized implementation using a global deflate context.
15 #       It's less efficient in terms of compression, but way more
16 #       efficient in terms of server memory usage.
17 package PublicInbox::NNTPdeflate;
18 use strict;
19 use 5.010_001;
20 use parent qw(PublicInbox::NNTP);
21 use Compress::Raw::Zlib;
22
23 my %IN_OPT = (
24         -Bufsize => PublicInbox::NNTP::LINE_MAX,
25         -WindowBits => -15, # RFC 1951
26         -AppendOutput => 1,
27 );
28
29 # global deflate context and buffer
30 my $zbuf = \(my $buf = '');
31 my $zout;
32 {
33         my $err;
34         ($zout, $err) = Compress::Raw::Zlib::Deflate->new(
35                 # nnrpd (INN) and Compress::Raw::Zlib favor MemLevel=9,
36                 # the zlib C library and git use MemLevel=8 as the default
37                 # -MemLevel => 9,
38                 -Bufsize => 65536, # same as nnrpd
39                 -WindowBits => -15, # RFC 1951
40                 -AppendOutput => 1,
41         );
42         $err == Z_OK or die "Failed to initialize zlib deflate stream: $err";
43 }
44
45
46 sub enable {
47         my ($class, $self) = @_;
48         my ($in, $err) = Compress::Raw::Zlib::Inflate->new(%IN_OPT);
49         if ($err != Z_OK) {
50                 $self->err("Inflate->new failed: $err");
51                 $self->write(\"403 Unable to activate compression\r\n");
52                 return;
53         }
54         $self->write(\"206 Compression active\r\n");
55         bless $self, $class;
56         $self->{zin} = $in;
57 }
58
59 # overrides PublicInbox::NNTP::compressed
60 sub compressed { 1 }
61
62 sub do_read ($$$$) {
63         my ($self, $rbuf, $len, $off) = @_;
64
65         my $zin = $self->{zin} or return; # closed
66         my $doff;
67         my $dbuf = delete($self->{dbuf}) // '';
68         $doff = length($dbuf);
69         my $r = PublicInbox::DS::do_read($self, \$dbuf, $len, $doff) or return;
70
71         # Workaround inflate bug appending to OOK scalars:
72         # <https://rt.cpan.org/Ticket/Display.html?id=132734>
73         # We only have $off if the client is pipelining, and pipelining
74         # is where our substr() OOK optimization in event_step makes sense.
75         if ($off) {
76                 my $copy = $$rbuf;
77                 undef $$rbuf;
78                 $$rbuf = $copy;
79         }
80
81         # assert(length($$rbuf) == $off) as far as NNTP.pm is concerned
82         # -ConsumeInput is true, so $dbuf is automatically emptied
83         my $err = $zin->inflate($dbuf, $rbuf);
84         if ($err == Z_OK) {
85                 $self->{dbuf} = $dbuf if $dbuf ne '';
86                 $r = length($$rbuf) and return $r;
87                 # nothing ready, yet, get more, later
88                 $self->requeue;
89         } else {
90                 delete $self->{zin};
91                 $self->close;
92         }
93         0;
94 }
95
96 # override PublicInbox::DS::msg_more
97 sub msg_more ($$) {
98         my $self = $_[0];
99
100         # $_[1] may be a reference or not for ->deflate
101         my $err = $zout->deflate($_[1], $zbuf);
102         $err == Z_OK or die "->deflate failed $err";
103         1;
104 }
105
106 sub zflush ($) {
107         my ($self) = @_;
108
109         my $deflated = $zbuf;
110         $zbuf = \(my $next = '');
111
112         my $err = $zout->flush($deflated, Z_FULL_FLUSH);
113         $err == Z_OK or die "->flush failed $err";
114
115         # We can still let the lower socket layer do buffering:
116         PublicInbox::DS::msg_more($self, $$deflated);
117 }
118
119 # compatible with PublicInbox::DS::write, so $_[1] may be a reference or not
120 sub write ($$) {
121         my $self = $_[0];
122         return PublicInbox::DS::write($self, $_[1]) if ref($_[1]) eq 'CODE';
123
124         my $deflated = $zbuf;
125         $zbuf = \(my $next = '');
126
127         # $_[1] may be a reference or not for ->deflate
128         my $err = $zout->deflate($_[1], $deflated);
129         $err == Z_OK or die "->deflate failed $err";
130         $err = $zout->flush($deflated, Z_FULL_FLUSH);
131         $err == Z_OK or die "->flush failed $err";
132
133         # We can still let the socket layer do buffering:
134         PublicInbox::DS::write($self, $deflated);
135 }
136
137 1;