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

Support protocol packages larger than 16 MiB #47 #166

Closed
wants to merge 12 commits into from
Closed

Support protocol packages larger than 16 MiB #47 #166

wants to merge 12 commits into from

Conversation

dmarkic
Copy link

@dmarkic dmarkic commented Nov 17, 2022

This PR adds support for protocol packages larger than 16MiB, both receiving and sending such packets.

@SimonFrings
Copy link
Contributor

Hey @dmarkic thanks for opening this pull request 👍

Can you also add additional unit tests so we can assure that your code works as expected. This is also a great way to test against different edge cases and see if we thought of everything important.

@dmarkic
Copy link
Author

dmarkic commented Nov 24, 2022

Hey @SimonFrings ,
indeed I forgot about both client and server cases, where if last split packet is 0xffffff, client should and server will end split packet sending with an empty packet.
Tests are written in ResultQueryTest as SelectStaticText.
In Mysql max_allowed_packet is by defaultset to 16MB (which is 32MB for client), so maybe the new tests might not pass if max_allowed_packets is not increased when testing.

@SimonFrings
Copy link
Contributor

@dmarkic Thanks for adding tests 👍

I took a look at the results from GitHub actions and saw that it shows risky tests. It seems like your assertions are never executed. I think what's happening here is that the query() method you use in your tests throws an exception. If an exception is thrown inside a promise, you can only see it if you explicitly catch it or define a behavior if the promise gets rejected, for example:

$connection->query('select \'' . $text . '\'')->then(
    function (QueryResult $command) use ($text) {
        $this->assertCount(1, $command->resultRows);
        $this->assertCount(1, $command->resultRows[0]);
        $this->assertSame($text, reset($command->resultRows[0]));

        $this->assertInstanceOf('React\MySQL\Connection', $conn);
},
    function (Exception $error) {
        echo 'Error: ' . $error->getMessage() . PHP_EOL;
    }
);

You can try this out and look for yourself if any exception is thrown, but I don't recommend adding this to your tests and then commit it to this PR. This way we stay consistent with the other tests in the ResultQueryTest class, but you can still use for debugging purposes.

I know the current behavior when it comes to exception handling is confusing, but we're going to change that with the upcoming v3 for ReactPHP, so that all errors should be shown by default unless explicitly hidden. For more information about our plans for ReactPHP v3, take a look at our v3 announcement.


I really like that you added tests here, in particular you tested the functional side which is totally fine in itself and the right thing to do, but you also have to take a look at the unit side. Can be kind of confusing that we have to types of tests, so I'll quickly break it down:

  1. We use functional tests, like you added, to test the overall functionality of the project. With functional tests we don't have to go much into detail, we only have to show the right behavior of the project in each case. In your case this means that you don't have to use the exact limit as a package size, you just have to show that you can use packages larger than 16 MiB (I'd recommend something like 20 MB). The 16 MiB limit is more of a implementation detail and has to be covered by the unit logic. This way we keep the functional tests easier to understand.

  2. The unit side of our tests goes more into detail. You changed some logic inside the Parser class which means that you have to add new unit tests to the Io/ParserTest class. In here you test against the specific lines of code you added, to assure that every line of code works as expected. You can check the code coverage output from GitHub actions to see how many lines of code in the Parser class are covered. Before your changes we had Lines: 98.12% (157/160) covered, with you changes we now have Lines: 89.56% (163/182). You don't have to achieve 100% here, there were already 3 lines of code not covered, so I would expect the result to land on (179/182) lines.

I'm sorry for the big wall of text here, I tried to give you as much information as possible to help you keep going. I'm happy to help more if you have any further questions 👍

@dmarkic
Copy link
Author

dmarkic commented Nov 28, 2022

Thank you for your thorough explanation.

Regarding the failed testSelectStaticTextSplittedPacketsExactly16MiBResponse test. Indeed, error is not caught. I've added ->done() at the end and removed max_allowed_packet setting in my testing MariaDb (so default is used) and I receive this error: React\MySQL\Exception: Got a packet bigger than 'max_allowed_packet' bytes. (as I have predicted in my previous comment) And that's why Parser class is left with untested code, since this test is testing packet that comes from server in packet split mode (in Parser::handleData).

I can write additional Parser tests that would test split packet functionality. I was first going with this functional part first, which is where the real server is tested against the code.

Is there a way to increase max_allowed_packet to maybe 17MB on testing MySql/MariaDb server so functional tests would pass? It's also why I didn't write functional tests that would use packets with 20MB size, etc. Just enough so the protocol it self is tested with the real server. I can do larger packet tests in ParserTest.php directly.

I would suggest however, to change the tests in a way like testSelectStaticTextSplittedPacketsExactly16MiBResponse is now, where, if returned promise is not used, it should be ended with ->done(). That way at least the inner exception is received by unittest.

I've updated the tests so the testSelectStaticTextSplittedPacketsExactly16MiBResponse will fail with correct (if max_allowed_packet is not increased) exception. If max_allowed_packet cannot be set in testing environment, I'll remove that test and write test in ParserTest.php.

Let me know your thoughts. I'll push the changes in few minutes, so we can see the exception.

Thanks!

…ption. Related to Support protocol packages larger than 16 MiB #47 #166
@dmarkic
Copy link
Author

dmarkic commented Nov 28, 2022

It seems that all tests fail because of max_allowed_packet. Don't know what's the current setting. On my default installation only the last one fails (since client would receive double the max_allowed_packet).

@SimonFrings
Copy link
Contributor

@dmarkic So sounds like max_allowed_packet needs to be increased on the server side, so it depends on your configuration, or am I wrong here?

@dmarkic
Copy link
Author

dmarkic commented Dec 7, 2022

@SimonFrings , for tests to work, the server on which tests are performed, needs to have max_allowed_packet increased.

See here: https://github.com/friends-of-reactphp/mysql/actions/runs/3565935698/jobs/5991738760#step:7:62

In this case, the mysql docker server needs max_allowed_packets to 17MB. Otherwise this functional test is not possible (as server will not allow large packets, thus not using packet-split).

If max_allowed_packets cannot be increased on docker mysql, I'll remove those tests and just test the Parser.php. Although I would recomment testing packet-split also against real mysql server.

Thanks!

@SimonFrings
Copy link
Contributor

@dmarkic So I guess you're using the docker example from our README.md, right? If so, I quickly eyeballed the mySQL docker image and it seems like you can change configuration by using a custom configuration file (there's also a way to change configuration without a config file): https://hub.docker.com/_/mysql/

Another thing I want to mention here is that we can test this on a unit level so this project behaves the right way, but this doesn't mean that every server will accept these packet sizes and that's why this is "difficult" to test the functional side.

For example, if you test the functional side on your local machine with a changed server configuration, it is likely that all tests will pass, but on GitHub actions the same tests will fail, because we never adjusted the configuration there. One answer could be that we just raise the max_allowed_packets there, but I'm don't think this is something we want to recommend the user to do. This also includes a bunch of new questions like:

  • What would be an optimal size?
  • Do we change the currently used sever setting or do we use two servers (one allowing small packages and one allowing big ones) and if so, do we now have to test both across all PHP versions?
  • ...

Another way could be to write a test that will check the max_allowed_packets size on the server side and will just skip the test if the server doesn't allow bigger packages. We'll still have to test this on the unit level and the big plus I see here is, we don't have to adjust the used server configuration for our test matrix. The downside is that we didn't really test the case, but I'm not sure how big of a problem this really is.

If you ask me, I would always suggest to split your big packet into smaller ones if possible, maybe this could also be an answer to your problem. I don't know if there are cases where it is impossible to use smaller packets. If there are it is a good idea that this project support bigger packages.But like I said above, how do we test this the right way?

A lot of questions, I'm interested in your opinion on this 👍

@dmarkic
Copy link
Author

dmarkic commented Dec 8, 2022

The only question here is: can/will you adjust the max_allowed_packets on Github actions. It's only about those tests now.

Sure, if someone would want to run tests locally, we can skip tests that would need max_allowed_packets > 16MB.

To answer your questions:

  • What would be an optimal size?

    • For tests 17MB is totally enough
  • Do we change the currently used sever setting or do we use two servers (one allowing small packages and one allowing big ones) and if so, do we now have to test both across all PHP versions?

    • Just change the current servers configuration. There is no other test that would interfere with max_allowed_packets.

Regarding the split of big packets - it's just not feasible (it is possible though). If you have a use case, where something larger than 16MB is stored in MySQL, you will need support for MySQL split packet functionality to fetch/update it without changing your application logic.
Also, what's rather bad about current implementation is, it silently discards such packet. You think you've written something to MySQL, but you didn't. It takes some time to figure out that this is not supported by this package.

If this packet_splitting merges, I'd love to contribute also:

@SimonFrings
Copy link
Contributor

Or is it an option to raise the server configuration inside this functional test only, so we don't have to change the whole configuration?

@dmarkic
Copy link
Author

dmarkic commented Dec 13, 2022

Hello @SimonFrings ,

Yes, that's a great idea. We could use SET GLOBAL max_allowed_packet = ... to set the max_allowed_packet to a different value. But the user running this query should have SUPER privilege. Don't know if that's the case on Github actions.

It is also possible to skip those tests if current max_allowed_packet is too low. I am in favor of testing this with increasing max_allowed_packet. The easiest is for sure to start mysqld with max-allowed-packet=17M argument.

Let me know what you think.

@SimonFrings
Copy link
Contributor

Yes, that's a great idea. We could use SET GLOBAL max_allowed_packet = ... to set the max_allowed_packet to a different value. But the user running this query should have SUPER privilege. Don't know if that's the case on Github actions.

Maybe let's try this first, seems to be the best solution so far, we'll see if it works out. If not we can always come back to our other suggestions. 👍

@dmarkic
Copy link
Author

dmarkic commented Dec 16, 2022

Hello @SimonFrings ,

Test is now successful, but packet split is not tested as it does not have permission to increase the max_allowed_packet.

To increase max_allowed_packet, MySQL docker would have to be started with --max-allowed-packet=17825792 at the end.

Example from ci.yml line 32:

- run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5 --max-allowed-packet=17825792

Don't know if I can change the ci.yml file to test?

@SimonFrings
Copy link
Contributor

@dmarkic Seems a bit random as it would increase max_allowed_packet for every test and not only for the ones you need. Is there no way we can increase max_allowed_packet from the outside?

@dmarkic
Copy link
Author

dmarkic commented Jan 2, 2023

@SimonFrings , not really. I tried, but user does not have permission to change max_allowed_packet. It really doesn't change anything for the other tests, except it enables us to test packet split functionality. There is no other way of testing this against real server except with this small change.

@SimonFrings
Copy link
Contributor

@dmarkic ok, to keep this going let's change the server settings for now to make this work, maybe we can find a better solution afterwards.

@dmarkic
Copy link
Author

dmarkic commented Jan 4, 2023

@SimonFrings , I guess it's all working now with this small change.

@@ -29,7 +29,7 @@ jobs:
php-version: ${{ matrix.php }}
coverage: xdebug
- run: composer install
- run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5
- run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5 --max-allowed-packet=17825792
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like a good idea, but I think it makes more sense to use something like this:

Suggested change
- run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5 --max-allowed-packet=17825792
- run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5 --max-allowed-packet=20M

Using 17825792 could seem a bit random for others ^^

Copy link
Author

Choose a reason for hiding this comment

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

Sure, I'll make this change

* If last part was exactly 0xffffff in size, we need to send an empty packet to signal end
* of packet splitting.
*/
if (\strlen($packet) == 0 && $part_len == 0xffffff) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to use === instead of == here?

Copy link
Author

Choose a reason for hiding this comment

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

I'll review my code and make changes

@dmarkic
Copy link
Author

dmarkic commented Jan 10, 2023

@SimonFrings , I guess that solves the whole PR with packet split support?

Comment on lines 592 to 614
protected function checkMaxAllowedPacket($connection, $min = 0x1100000)
{
return $connection->query('SHOW VARIABLES LIKE \'max_allowed_packet\'')->then(
function ($res) use ($min, $connection) {
$current = $res->resultRows[0]['Value'];
if ($current < $min) {
return $connection->query('SET GLOBAL max_allowed_packet = ?', [$min])->otherwise(
function (\Throwable $e) use ($min, $current) {
throw new \Exception(
'Cannot set max_allowed_packet to: ' . $min . ' ' .
'current: ' . $current . ' error: ' . $e->getMessage()
);
}
);
}
return \React\Promise\resolve();
}
)->then(
function () {
return true;
}
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You said in one of your last comments:

I tried, but user does not have permission to change max_allowed_packet

In here you're trying to overwrite max_allowed_packet, so I'm a little confused if it works or not. Or maybe this is a leftover? It seems like your tests will never overwrite max_allowed_packet in this case because $min never surpasses $current, right?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, it will try. Maybe useful for someone with smaller max_allowed_packet but permission to change it. If it does not succeed it will mark test as incomplete.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that this is important for someone who wants to execute the tests and has a smaller max_allowed_packet. We also recommend a temporary mysql docker image to execute tests inside our documentation which doesn't involve raising max_allowed_packet, so it will use the standard 16MiB :

For example, to create an empty test database, you can also use a temporary mysql Docker image like this:

docker run -it --rm --net=host \
   -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test \
    -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5

Over all I am not sure if we should even be able to SET GLOBAL max_allowed_packet as it changes the configuration of a database, which could be used by more than just one person. Changing someone else's gloabl configuration just feels wrong and I can imagine why it shouldn't be allowed. If we want to alter the max_allowed_packet, we should do this temporary on the connection if possible.

As an alternative we could also update our temporary Docker image inside the Readme.md like you already did inside the ci.yml (same docker images btw) and if someone would use a database with a smaller max_allowed_packet we should skip the necessary tests (not mark them incomplete)

What do you think?

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I agree, we should probably not set GLOBAL configuration. I'll rewrite tests to simply check if max_allowed_packet is set to large enough value for tests, otherwise tests will be marked as incomplete.
I will update the Readme.md file to include max_allowed_packet setting.

…cket is too low. Add --max-allowed-packet=20M to README.md docker instructions
@dmarkic
Copy link
Author

dmarkic commented Jan 19, 2023

Sorry for late response. I've fixed the discussed issues.

  • packet split test are marked as incomplete if max_allowed_packet is too low
  • README.md docker arguments were updated

Copy link
Contributor

@SimonFrings SimonFrings left a comment

Choose a reason for hiding this comment

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

Thanks for updating the PR, we're getting closer and closer 👍

I added a few remarks below, interested what you think about them.

Comment on lines 156 to 157
$this->debug('Waiting for complete packet with ' . $len . '/' . $this->pctSize . ' bytes');

return;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should leave this blank line here, improves the readability and keeps the changelog a bit smaller

Comment on lines +633 to +635
function (\Throwable $e) {
$this->markTestIncomplete('checkMaxAllowedPacket: ' . $e->getMessage());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should skip these tests here instead of marking them as incomplete. As the documentation of PHPUnit says to mark tests as incomplete:

PHPUnit\Framework\IncompleteTest is a marker interface for marking an exception that is raised by a test method as the result of the test being incomplete or currently not implemented.

The problem here isn't that some implementation is missing, it's more of a "Your configuration doesn't fit our test" case here. This fits the example for skipping tests with PHPUnit as described inside the docs:

In the test case class’ setUp() template method we check whether the MySQLi extension is available and use the markTestSkipped() method to skip the test if it is not.

Makes more sense, what do you think?

Comment on lines +652 to +654

$promise = $this->checkMaxAllowedPacket($connection);

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this test behaves the way you intended to. As I see it, this test will be marked as skipped if someone uses a max_allowed_packet size of 16 MiB which should be allowed in this case. This is caused by the $min default of your checkMaxAllowedPacket() function, as it will set the minimum to 17MiB and throws an exception for configurations with 16 MiB.

Comment on lines +669 to +675
)->otherwise(
function (\Throwable $e) {
$this->markTestIncomplete('checkMaxAllowedPacket: ' . $e->getMessage());
}
)->always(
function () use ($connection) {
$connection->quit();
Copy link
Contributor

Choose a reason for hiding this comment

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

The PromiseInterface::always() and PromiseInterface::otherwise() functions are marked as deprecated as described in reactphp/promise, I don't think it's a good idea to use them in this context as we want to avoid using deprecated functions.

Copy link
Author

Choose a reason for hiding this comment

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

This only works on react-promise:3.x ... not react-promise:2.7.

Will only react-promise:3.x be supported? In current package.json both are supported.

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right that both versions are supported and we already know that v3 will be the way forward. I agree that it should be compatible with both versions at this point, but looking into the future here. If we already know that this functionality will be removed at a later point in time, it makes sense to avoid using these functions, so we don't have to remove them again.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, but 3.x introduced that, 2.7 does not work with catch and finally. So, when 2.7 version is removed from this package, it will be time to also change those methods.

@SimonFrings
Copy link
Contributor

Hey @dmarkic, this PR is open for a long time now and I wanted to see how it's going 👍

I still think this is a valuable addition to the project, so what can we do to get this to a mergable state. A lot has happened since February 2023 and the first thing to be done here is to rebase and resolve the latest conflicts.

Also taken from my last response, there are still some changes that should be done in here (e.g. avoid using the deprecated otherwise() and always() functions as they were removed in Promised v3).

I want to avoid having pull requests and issues open for too long, so it's also okay to close this if there's currently no time to work on this topic and revisit this at a later point again.

Interested in your thoughts!

@dmarkic
Copy link
Author

dmarkic commented Apr 17, 2024

Hello @SimonFrings,

I'll create new PR for 0.7.x version, since a lot has changed. You can close this one and I'll create a new one also with promise v3 support.

I'll do this within few days.

@SimonFrings
Copy link
Contributor

Alright, then I'll close this one 👍

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.

2 participants