Hi,

I have a couple of comments regarding the last patch, mostly these are minor 
issues.

In src/backend/libpq/pqcomm.c, starting from the line 1114:

int
pq_getbyte_if_available(unsigned char *c)
{
        int                     r;

        Assert(PqCommReadingMsg);

        if (PqRecvPointer < PqRecvLength || (0) > 0)   // not easy to 
understand optimization (maybe add a comment?)
        {
                *c = PqRecvBuffer[PqRecvPointer++];
                return 1;
        }
        return r;             // returned value is not initialized
}

In src/interfaces/libpq/fe-connect.c, starting from the line 3255:

pqGetc(&algorithm, conn);
impl = zpq_get_algorithm_impl(algorithm);
{                                                                               
 // I believe that if (impl < 0) condition is missing here, otherwise there is 
always an error
   appendPQExpBuffer(&conn->errorMessage,
                 libpq_gettext(
                                                "server is not supported 
requested compression algorithm %c\n"), algorithm);
   goto error_return;
}

In configure, starting from the line 1587:

--without-zlib          do not use Zlib
--with-zstd             do not use zstd // is this correct?

Thanks

--
Daniil Zakhlystov

> On Nov 1, 2020, at 12:08 AM, Konstantin Knizhnik <k.knizh...@postgrespro.ru> 
> wrote:
> 
> Hi
> 
> On 31.10.2020 00:03, Andres Freund wrote:
>> Hi,
>> 
>> On 2020-10-29 16:45:58 +0300, Konstantin Knizhnik wrote:
>>>> - What does " and it is up to the client whether to continue work
>>>>    without compression or report error" actually mean for a libpq 
>>>> parameter?
>>> It can not happen.
>>> The client request from server use of compressed protocol only if
>>> "compression=XXX" was specified in connection string.
>>> But XXX should be supported by client, otherwise this request will be
>>> rejected.
>>> So supported protocol string sent by client can never be empty.
>> I think it's pretty important for features like this to be able to fail
>> softly when the feature is not available on the other side. Otherwise a
>> lot of applications need to have unnecessary version dependencies coded
>> into them.
> Sorry, may be I do not completely understand your suggestion.
> Right now user jut specify that he wants to use compression.
> Libpq client sends to the server list of supported algorithms
> and server choose among them the best one is supported.
> It sends it chose to the client and them are both using this algorithm.
> 
> Sorry, that in previous mail I have used incorrect samples: client is not 
> explicitly specifying compression algorithm -
> it just request compression. And server choose the most efficient algorithm 
> which is supported both by client and server.
> So client should not know names of the particular algorithms (i.e. zlib, 
> zstd) and
> choice is based on the assumption that server (or better say programmer)
> knows better than user which algorithms is more efficient.
> Last assumption me be contested because user better know which content will 
> be send and which
> algorithm is more efficient for this content. But right know when the choice 
> is between zstd and zlib,
> the first one is always better: faster and provides better quality.
> 
>> 
>>>> - What is the point of the "streaming mode" reference?
>>> There are ways of performing compression:
>>> - block mode when each block is individually compressed (compressor stores
>>> dictionary in each compressed blocked)
>>> - stream mode
>>> Block mode allows to independently decompress each page. It is good for
>>> implementing page or field compression (as pglz is used to compress toast
>>> values).
>>> But it is not efficient for compressing client-server protocol commands.
>>> It seems to me to be important to explain that libpq is using stream mode
>>> and why there is no pglz compressor
>> To me that seems like unnecessary detail in the user oriented parts of
>> the docs at least.
>> 
> Ok, I will remove this phrase.
> 
>>>> Why are compression methods identified by one byte identifiers? That
>>>> seems unnecessarily small, given this is commonly a once-per-connection
>>>> action?
>>> It is mostly for simplicity of implementation: it is always simple to work
>>> with fixed size messages (or with array of chars rather than array of
>>> strings).
>>> And I do not think that it can somehow decrease flexibility: this one-letter
>>> algorihth codes are not visible for user. And I do not think that we
>>> sometime will support more than 127 (or even 64 different compression
>>> algorithms).
>> It's pretty darn trivial to have a variable width protocol message,
>> given that we have all the tooling for that. Having a variable length
>> descriptor allows us to extend the compression identifier with e.g. the
>> level without needing to change the protocol. E.g. zstd:9 or
>> zstd:level=9 or such.
>> 
>> I suggest using a variable length string as the identifier, and split it
>> at the first : for the lookup, and pass the rest to the compression
>> method.
> Yes, I agree that it provides more flexibility.
> And it is not a big problem to handle arbitrary strings instead of chars.
> But right now my intention was to prevent user from choosing compression 
> algorithm and especially specifying compression level
> (which are algorithm-specific). Its matter of server-client handshake to 
> choose best compression algorithm supported by both of them.
> I can definitely rewrite it, but IMHO given to much flexibility for user will 
> just complicate things without any positive effect.
> 
> 
>> 
>>>> The protocol sounds to me like there's no way to enable/disable
>>>> compression in an existing connection. To me it seems better to have an
>>>> explicit, client initiated, request to use a specific method of
>>>> compression (including none). That allows to enable compression for bulk
>>>> work, and disable it in other cases (e.g. for security sensitive
>>>> content, or for unlikely to compress well content).
>>> It will significantly complicate implementation (because of buffering at
>>> different levels).
>> Really? We need to be able to ensure a message is flushed out to the
>> network at precise moments - otherwise a lot of stuff will break.
> Definitely stream is flushed after writing each message.
> The problem is at receiver side: several messages can be sent without waiting 
> response and will be read into the buffer. If first is compressed
> and subsequent - not, then it will be not so trivial to handle it. I have 
> already faced with this problem when compression is switched on:
> backend may send some message right after acknowledging compression protocol. 
>  This message is already compressed but is delivered together with 
> uncompressed compression acknowledgement message. I have solved this problem 
> in switching on compression at client side,
> but really do not want to solve it at arbitrary moments. And once again: my 
> opinion is that too much flexibility is not so good here:
> there is no sense to switch off compression for short messages (overhead of 
> switching can be larger than compression itself).
>> 
>>> Also it is not clear to me who and how will control enabling/disabling
>>> compression in this case?
>>> I can imagine that "\copy" should trigger compression. But what about
>>> (server side) "copy"  command?
>> The client could still trigger that. I don't think it should ever be
>> server controlled.
> Sorry, but I still think that possibility to turn compression on/off on the 
> fly is very doubtful idea.
> And it will require extension of protocol (adding some extra functions to 
> libpq library to control it).
>>> And concerning security risks... In most cases such problem is not relevant
>>> at all because both client and server are located within single reliable
>>> network.
>> The trend is towards never trusting the network, even if internal, not
>> the opposite.
>> 
>> 
>>> It if security of communication really matters, you should not switch
>>> compression in all cases (including COPY and other bulk data transfer).
>>> It is very strange idea to let client to decide which data is "security
>>> sensitive" and which not.
>> Huh?
> Sorry, if I was unclear. I am not specialist in security.
> But it seems to be very dangerous when client (and not server) decides which 
> data is "security sensitive"
> and which not. Just an example: you have VerySecreteTable. And when you 
> perform selects on this table,
> you switch compression off. But then client want to execute COPY command for 
> this table and as far as it requires bulk data transfer,
> user decides to switch on compression.
> 
> Actually specking about security risks has no sense: if you want to provide 
> security, then you use TLS. And it provides its own compression.
> So there is no need to perform compression at libpq level. libpq comression 
> is for the cases when you do not need SSL at all.
> 
>>>> I think that would also make cross-version handling easier, because a
>>>> newer client driver can send the compression request and handle the
>>>> error, without needing to reconnect or such.
>>>> 
>>>> Most importantly, I think such a design is basically a necessity to make
>>>> connection poolers to work in a sensible way.
>>> I do not completely understand the problem with connection pooler.
>>> Right now developers of Yandex Odyssey are trying to support libpq
>>> compression in their pooler.
>>> If them will be faced with some problems, I will definitely address
>>> them.
>> It makes poolers a lot more expensive if they have to decompress and
>> then recompress again. It'd be awesome if even the decompression could
>> be avoided in at least some cases, but compression is the really
>> expensive part. So if a client connects to the pooler with
>> compression=zlib and an existing server connection is used, the pooler
>> should be able to switch the existing connection to zlib.
> My expectation was that pooler is installed in the same network as database 
> and there is no need
> to compress traffic between pooler and backends.
> 
> But even if it is really needed I do not see problems here.
> Definitely we need zpq library to support different compression algorithms in 
> the same application.
> But it is done.
> 
> To avoid pooler perform decompression+compression ist is necessary to send 
> command header in raw format.
> But it will greatly reduce effect of stream compression.
> 
> 
>> 
>> 
>>> But if you think that it is so important, I will try to implement it. Many
>>> questions arise in this case: which side should control compression level?
>>> Should client affect compression level both at client side and at server
>>> side? Or it should be possible to specify separately compression level for
>>> client and for server?
>> I don't think the server needs to know about what the client does,
>> compression level wise.
> Sorry, I do not understand you.
> Compression takes place at  sender side.
> So both client compressing its messages before sending to server
> and server compresses responses sent to the client.
> In both cases compression level may be specified.
> And it is not clear whether client should control compression level of the 
> server.
> 
> 
>> 
>>>>> +<para>
>>>>> +        Used compression algorithm. Right now the following streaming 
>>>>> compression algorithms are supported: 'f' - Facebook zstd, 'z' - zlib, 
>>>>> 'n' - no compression.
>>>>> +</para>
>>>> I would prefer this just be referenced as zstd or zstandard, not
>>>> facebook zstd. There's an RFC (albeit only "informational"), and it
>>>> doesn't name facebook, except as an employer:
>>>> https://tools.ietf.org/html/rfc8478
>>> Please notice that it is internal encoding, user will specify
>>> psql -d "dbname=postgres compression=zstd"
>>> 
>>> If name "zstd" is not good, I can choose any other.
>> All I was saying is that I think you should not name it ""Facebook zstd",
>> just "zstd".
> 
> Sorry, it was my error.
> Right now compression name is not specified by user at all: just boolean 
> value on/off.
> 
>> I think you should just use something like
>> 
>> pq_beginmessage(buf, 'z');
>> pq_sendbyte(buf, compression_method_byte);
>> pq_endmessage(buf);
>> 
>> like most of the backend does? And if you, as I suggest, use a variable
>> length compression identifier, use
>> pq_sendcountedtext(buf, compression_method, strlen(compression_method));
>> or such.
> 
> Sorry, I can not do it in this way because pq connection with client is not 
> yet initialized.
> So I can not use pq_endmessage here and have to call secure_write manually.
> 
>> I am not saying it's necessarily the best approach, but it doesn't seem
>> like it'd be hard to have zpq not send / receive the data from the
>> network, but just transform the data actually to-be-sent/received from
>> the network.
>> 
>> E.g. in pq_recvbuf() you could do something roughly like the following,
>> after a successful secure_read()
>> 
>>     if (compression_enabled)
>>     {
>>         zpq_decompress(network_data, network_data_length,
>>                        &input_processed, &output_data, &output_data_length);
>>         copy_data_into_recv_buffer(output_data, output_data_length);
>>         network_data += input_processed;
>>         network_data_length -= input_processed;
>>     }
> 
> Sorry, it is not possible because to produce some output, compressor may need 
> to perform multiple read.
> And doing it in two different places, i.e. in pqsecure_read and secure_read 
> is worse than doing it on once place - s it is done now.
> 
> 
>> and the inverse on the network receive side. The advantage would be that
>> we can then more easily use zpq stuff in other places.
>> 
>> If we do go with the current callback model, I think we should at least
>> extend it so that a 'context' parameter is shuffled through zpq, so that
>> other code can work without global state.
> 
> Callback is has void* arg where arbitrary daat can be passed to callback.
> 
>> 
>> 
>>>>> +         r = PqStream
>>>>> +                 ? zpq_read(PqStream, PqRecvBuffer + PqRecvLength,
>>>>> +                                    PQ_RECV_BUFFER_SIZE - PqRecvLength, 
>>>>> &processed)
>>>>> +                 : secure_read(MyProcPort, PqRecvBuffer + PqRecvLength,
>>>>> +                                           PQ_RECV_BUFFER_SIZE - 
>>>>> PqRecvLength);
>>>>> +         PqRecvLength += processed;
>>>> ? : doesn't make sense to me in this case. This should be an if/else.
>>>> 
>>> Isn't it a matter of style preference?
>>> Why  if/else is principle better than ?:
>> ?: makes sense when it's either much shorter, or when it allows to avoid
>> repeating a bunch of code. E.g. if it's just a conditional argument to a
>> function with a lot of other arguments.
>> 
>> But when, as here, it just leads to weirder formatting, it really just
>> makes the code harder to read.
> 
> From my point of view the main role of ?: construction is not just saving 
> some bytes/lines of code.
> It emphasizes that both parts of conditional construction produce the same 
> result.
> As in this case it is result of read operation. If you replace it with 
> if-else, this relation between actions done in two branches is missed
> (or at least less clear).
> 
>> 
>>> I agree that sometimes ?: leads to more complex and obscure expressions.
>>> But to you think that if-else in this case will lead to more clear or
>>> readable code?
>> Yes, pretty clearly for me.
>> 
>>     r = PqStream
>>         ? zpq_read(PqStream, PqRecvBuffer + PqRecvLength,
>>                    PQ_RECV_BUFFER_SIZE - PqRecvLength, &processed)
>>         : secure_read(MyProcPort, PqRecvBuffer + PqRecvLength,
>>                    PQ_RECV_BUFFER_SIZE - PqRecvLength);
>> vs
>> 
>>     if (PqStream)
>>         r = zpq_read(PqStream, PqRecvBuffer + PqRecvLength,
>>                      PQ_RECV_BUFFER_SIZE - PqRecvLength, &processed);
>>     else
>>         r = secure_read(MyProcPort, PqRecvBuffer + PqRecvLength,
>>                         PQ_RECV_BUFFER_SIZE - PqRecvLength);
>> 
>> the if / else are visually more clearly distinct, the sole added
>> repetition is r =. And most importantly if / else are more common.
> 
> I agree that ?: is less clear than if/else. In most of modern programming 
> languages there is no construction ?:
> but if-else can be used as expression. And repetition is always bad, isn't 
> it? Not just because of writing extra code,
> but because of possible inconsistencies and errors (just like normal forms in 
> databases).
> 
> In any case - I do not think that it is principle: if you prefer - I can 
> replace it with if-else
>>> Another question is whether conditional expression here is really good idea.
>>> I prefer to replace with indirect function call...
>> A branch is cheaper than indirect calls, FWIW>
> 
> Really? Please notice that we do not compare just branch with indirect call, 
> but branch+direct call vs. indirect call.
> I didn't measure it. But in any case IMHO performance aspect is less 
> important here than clearance and maintainability of code.
> And indirect call is definitely better in this sense... Unfortunately I can 
> not use it (certainly it is not absolutely impossible, but it requires much
> more changes).
> 
>>>>> +#define ZSTD_BUFFER_SIZE (8*1024)
>>>>> +#define ZSTD_COMPRESSION_LEVEL 1
>>>> Add some arguments for choosing these parameters.
>>>> 
>>> What are the suggested way to specify them?
>>> I can not put them in GUCs (because them are also needed at client side).
>> I don't mean arguments as in configurable, I mean that you should add a
>> comment explaining why 8KB / level 1 was chosen.
> 
> Sorry, will do.
>>>>> +/*
>>>>> + * Get list of the supported algorithms.
>>>>> + * Each algorithm is identified by one letter: 'f' - Facebook zstd, 'z' 
>>>>> - zlib.
>>>>> + * Algorithm identifies are appended to the provided buffer and 
>>>>> terminated by '\0'.
>>>>> + */
>>>>> +void
>>>>> +zpq_get_supported_algorithms(char algorithms[ZPQ_MAX_ALGORITHMS])
>>>>> +{
>>>>> + int i;
>>>>> + for (i = 0; zpq_algorithms[i].name != NULL; i++)
>>>>> + {
>>>>> +         Assert(i < ZPQ_MAX_ALGORITHMS);
>>>>> +         algorithms[i] = zpq_algorithms[i].name();
>>>>> + }
>>>>> + Assert(i < ZPQ_MAX_ALGORITHMS);
>>>>> + algorithms[i] = '\0';
>>>>> +}
>>>> Uh, doesn't this bake ZPQ_MAX_ALGORITHMS into the ABI? That seems
>>>> entirely unnecessary?
>>> I tried to avoid use of dynamic memory allocation because zpq is used both
>>> in client and server environments with different memory allocation
>>> policies.
>> We support palloc in both(). But even without that, you can just have
>> one function to get the number of algorithms, and then have the caller
>> pass in a large enough array.
> 
> Certainly it is possible.
> But why complicate implementation if it is internal function used only in 
> single place in the code?
>> 
>> 
>>> And I afraid that using the _pq_ parameter stuff makes enabling of
>>> compression even less user friendly.
>> I didn't intend to suggest that the _pq_ stuff should be used in a
>> client facing manner. What I suggesting is that if the connection string
>> contains compression=, the driver would internally translate that to
>> _pq_.compression to pass that on to the server, which would allow the
>> connection establishment to succeed, even if the server is a bit older.
> 
> New client can establish connection with old server if it is not using 
> compression.
> And if it wants to use compression, then _pq_.compression will not help much.
> Old server will ignore it (unlike compression= option) but then client will 
> wait for acknowledgement from
> server for used  compression algorithm and didn't receive one. Certainly it 
> is possible to handle this situation (if we didn't
> receive expected message) but why it is better than adding compression option 
> to startup package?
> But if you think that adding new options to startup package should be avoided 
> as much as possible,
> then I will replace it with _pq_.compression.
> 
>> Greetings,
>> 
>> Andres Freund
> Thank you very much for review and explanations.

Reply via email to