From f1ad18a07fb65df6de62b7e33d7e7ce3a1ccd31e Mon Sep 17 00:00:00 2001 From: Michael Penick Date: Thu, 7 Jul 2016 11:20:45 -0700 Subject: [PATCH 1/4] Fix: Potential leak when paging results --- ext/php_cassandra_types.h | 6 +- ext/src/Cassandra/DefaultSession.c | 8 +- ext/src/Cassandra/FutureRows.c | 89 +++++++------- ext/src/Cassandra/FutureRows.h | 23 ++++ ext/src/Cassandra/Rows.c | 180 ++++++++++++++--------------- 5 files changed, 169 insertions(+), 137 deletions(-) create mode 100644 ext/src/Cassandra/FutureRows.h diff --git a/ext/php_cassandra_types.h b/ext/php_cassandra_types.h index 00fc8d9b3..323672ff8 100644 --- a/ext/php_cassandra_types.h +++ b/ext/php_cassandra_types.h @@ -247,8 +247,9 @@ PHP_CASSANDRA_BEGIN_OBJECT_TYPE(rows) cassandra_ref *statement; php5to7_zval session; php5to7_zval rows; - const CassResult *result; - php5to7_zval next_page; + php5to7_zval next_rows; + cassandra_ref *result; + cassandra_ref *next_result; php5to7_zval future_next_page; PHP_CASSANDRA_END_OBJECT_TYPE(rows) @@ -256,6 +257,7 @@ PHP_CASSANDRA_BEGIN_OBJECT_TYPE(future_rows) cassandra_ref *statement; php5to7_zval session; php5to7_zval rows; + cassandra_ref *result; CassFuture *future; PHP_CASSANDRA_END_OBJECT_TYPE(future_rows) diff --git a/ext/src/Cassandra/DefaultSession.c b/ext/src/Cassandra/DefaultSession.c index f34e4aa38..09d50636b 100644 --- a/ext/src/Cassandra/DefaultSession.c +++ b/ext/src/Cassandra/DefaultSession.c @@ -30,6 +30,12 @@ zend_class_entry *cassandra_default_session_ce = NULL; return SUCCESS; \ } +static void +free_result(void *result) +{ + cass_result_free((CassResult *) result); +} + static int bind_argument_by_index(CassStatement *statement, size_t index, zval *value TSRMLS_DC) { @@ -584,8 +590,8 @@ PHP_METHOD(DefaultSession, execute) if (single && cass_result_has_more_pages(result)) { rows->statement = php_cassandra_new_ref(single, free_statement); + rows->result = php_cassandra_new_ref((void *)result, free_result); PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->session), getThis()); - rows->result = result; return; } diff --git a/ext/src/Cassandra/FutureRows.c b/ext/src/Cassandra/FutureRows.c index 68f101851..b47c2f7f4 100644 --- a/ext/src/Cassandra/FutureRows.c +++ b/ext/src/Cassandra/FutureRows.c @@ -24,75 +24,73 @@ zend_class_entry *cassandra_future_rows_ce = NULL; ZEND_EXTERN_MODULE_GLOBALS(cassandra) static void -php_cassandra_future_clear(cassandra_future_rows *self) +free_result(void *result) { - if (self->statement) { - php_cassandra_del_ref(&self->statement); - self->statement = NULL; - } + cass_result_free((CassResult *) result); +} - PHP5TO7_ZVAL_MAYBE_DESTROY(self->session); - if (self->future) { - cass_future_free(self->future); - self->future = NULL; +int +php_cassandra_future_rows_get_result(cassandra_future_rows *future_rows, zval *timeout TSRMLS_DC) +{ + if (!future_rows->result) { + const CassResult *result = NULL; + + if (php_cassandra_future_wait_timed(future_rows->future, timeout TSRMLS_CC) == FAILURE) { + return FAILURE; + } + + if (php_cassandra_future_is_error(future_rows->future TSRMLS_CC) == FAILURE) { + return FAILURE; + } + + result = cass_future_get_result(future_rows->future); + if (!result) { + zend_throw_exception_ex(cassandra_runtime_exception_ce, 0 TSRMLS_CC, + "Future doesn't contain a result."); + return FAILURE; + } + + future_rows->result = php_cassandra_new_ref((void *)result, free_result); } + + return SUCCESS; } PHP_METHOD(FutureRows, get) { zval *timeout = NULL; cassandra_rows *rows = NULL; - const CassResult *result = NULL; cassandra_future_rows *self = PHP_CASSANDRA_GET_FUTURE_ROWS(getThis()); - if (!PHP5TO7_ZVAL_IS_UNDEF(self->rows)) { - RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->rows), 1, 0); - } - if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|z", &timeout) == FAILURE) { return; } - if (php_cassandra_future_wait_timed(self->future, timeout TSRMLS_CC) == FAILURE) { + if (php_cassandra_future_rows_get_result(self, timeout TSRMLS_CC) == FAILURE) { return; } - if (php_cassandra_future_is_error(self->future TSRMLS_CC) == FAILURE) { - return; + if (PHP5TO7_ZVAL_IS_UNDEF(self->rows)) { + if (php_cassandra_get_result((const CassResult *) self->result->data, + &self->rows TSRMLS_CC) == FAILURE) { + PHP5TO7_ZVAL_MAYBE_DESTROY(self->rows); + return; + } } - result = cass_future_get_result(self->future); - - if (!result) { - zend_throw_exception_ex(cassandra_runtime_exception_ce, 0 TSRMLS_CC, - "Future doesn't contain a result."); - return; - } + object_init_ex(return_value, cassandra_rows_ce); + rows = PHP_CASSANDRA_GET_ROWS(return_value); - PHP5TO7_ZVAL_MAYBE_MAKE(self->rows); - object_init_ex(PHP5TO7_ZVAL_MAYBE_P(self->rows), cassandra_rows_ce); - rows = PHP_CASSANDRA_GET_ROWS(PHP5TO7_ZVAL_MAYBE_P(self->rows)); + PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->rows), + PHP5TO7_ZVAL_MAYBE_P(self->rows)); - if (php_cassandra_get_result(result, &rows->rows TSRMLS_CC) == FAILURE) { - cass_result_free(result); - zval_ptr_dtor(&self->rows); - PHP5TO7_ZVAL_UNDEF(self->rows); - return; - } - - if (cass_result_has_more_pages(result)) { + if (cass_result_has_more_pages((const CassResult *)self->result->data)) { PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->session), PHP5TO7_ZVAL_MAYBE_P(self->session)); rows->statement = php_cassandra_add_ref(self->statement); - rows->result = result; - } else { - cass_result_free(result); + rows->result = php_cassandra_add_ref(self->result); } - - php_cassandra_future_clear(self); - - RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->rows), 1, 0); } ZEND_BEGIN_ARG_INFO_EX(arginfo_timeout, 0, ZEND_RETURN_VALUE, 0) @@ -129,8 +127,12 @@ php_cassandra_future_rows_free(php5to7_zend_object_free *object TSRMLS_DC) cassandra_future_rows *self = PHP5TO7_ZEND_OBJECT_GET(future_rows, object); PHP5TO7_ZVAL_MAYBE_DESTROY(self->rows); + PHP5TO7_ZVAL_MAYBE_DESTROY(self->session); + + php_cassandra_del_ref(&self->statement); + php_cassandra_del_ref(&self->result); - php_cassandra_future_clear(self); + cass_future_free(self->future); zend_object_std_dtor(&self->zval TSRMLS_CC); PHP5TO7_MAYBE_EFREE(self); @@ -144,6 +146,7 @@ php_cassandra_future_rows_new(zend_class_entry *ce TSRMLS_DC) self->future = NULL; self->statement = NULL; + self->result = NULL; PHP5TO7_ZVAL_UNDEF(self->rows); PHP5TO7_ZVAL_UNDEF(self->session); diff --git a/ext/src/Cassandra/FutureRows.h b/ext/src/Cassandra/FutureRows.h new file mode 100644 index 000000000..187a0635a --- /dev/null +++ b/ext/src/Cassandra/FutureRows.h @@ -0,0 +1,23 @@ +/** + * Copyright 2015-2016 DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PHP_CASSANDRA_FUTURE_ROWS_H +#define PHP_CASSANDRA_FUTURE_ROWS_H + +int +php_cassandra_future_rows_get_result(cassandra_future_rows *future_rows, zval *timeout TSRMLS_DC); + +#endif /* PHP_CASSANDRA_FUTURE_ROWS_H */ diff --git a/ext/src/Cassandra/Rows.c b/ext/src/Cassandra/Rows.c index 09ed9343e..472b04db9 100644 --- a/ext/src/Cassandra/Rows.c +++ b/ext/src/Cassandra/Rows.c @@ -19,22 +19,40 @@ #include "util/ref.h" #include "util/result.h" +#include "FutureRows.h" + zend_class_entry *cassandra_rows_ce = NULL; static void -php_cassandra_rows_clear(cassandra_rows *self) +free_result(void *result) { - if (self->result) { - cass_result_free(self->result); - self->result = NULL; - } + cass_result_free((CassResult *) result); +} + +static void +php_cassandra_rows_create(cassandra_rows *current, zval *result TSRMLS_DC) { + cassandra_rows *rows; - if (self->statement) { - php_cassandra_del_ref(&self->statement); - self->statement = NULL; + if (PHP5TO7_ZVAL_IS_UNDEF(current->next_rows)) { + if (php_cassandra_get_result((const CassResult *) current->next_result->data, + ¤t->next_rows TSRMLS_CC) == FAILURE) { + PHP5TO7_ZVAL_MAYBE_DESTROY(current->next_rows); + return; + } } - PHP5TO7_ZVAL_MAYBE_DESTROY(self->session); + object_init_ex(result, cassandra_rows_ce); + rows = PHP_CASSANDRA_GET_ROWS(PHP5TO7_ZVAL_MAYBE_P(result)); + + PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->rows), + PHP5TO7_ZVAL_MAYBE_P(current->next_rows)); + + if (cass_result_has_more_pages((const CassResult *) current->next_result->data)) { + rows->statement = php_cassandra_add_ref(current->statement); + rows->result = php_cassandra_add_ref(current->next_result); + PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->session), + PHP5TO7_ZVAL_MAYBE_P(current->session)); + } } PHP_METHOD(Rows, __construct) @@ -196,7 +214,7 @@ PHP_METHOD(Rows, isLastPage) self = PHP_CASSANDRA_GET_ROWS(getThis()); if (self->result == NULL && - PHP5TO7_ZVAL_IS_UNDEF(self->next_page) && + PHP5TO7_ZVAL_IS_UNDEF(self->next_rows) && PHP5TO7_ZVAL_IS_UNDEF(self->future_next_page)) { RETURN_TRUE; } @@ -207,100 +225,77 @@ PHP_METHOD(Rows, isLastPage) PHP_METHOD(Rows, nextPage) { zval *timeout = NULL; - cassandra_session *session = NULL; - CassFuture *future = NULL; - const CassResult *result = NULL; - cassandra_rows *rows = NULL; - cassandra_future_rows *future_rows = NULL; - cassandra_rows *self = PHP_CASSANDRA_GET_ROWS(getThis()); - if (!PHP5TO7_ZVAL_IS_UNDEF(self->next_page)) { - RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->next_page), 1, 0); - } - if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|z", &timeout) == FAILURE) { return; } - if (!PHP5TO7_ZVAL_IS_UNDEF(self->future_next_page)) { - if (!instanceof_function(PHP5TO7_Z_OBJCE_MAYBE_P(self->future_next_page), - cassandra_future_rows_ce TSRMLS_CC)) { - zend_throw_exception_ex(cassandra_runtime_exception_ce, 0 TSRMLS_CC, - "Unexpected future instance."); - return; - } - - future_rows = PHP_CASSANDRA_GET_FUTURE_ROWS(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page)); + if (!self->next_result) { + if (!PHP5TO7_ZVAL_IS_UNDEF(self->future_next_page)) { + cassandra_future_rows *future_rows = NULL; - if (php_cassandra_future_wait_timed(future_rows->future, timeout TSRMLS_CC) == FAILURE) { - return; - } - - if (php_cassandra_future_is_error(future_rows->future TSRMLS_CC) == FAILURE) { - return; - } + if (!instanceof_function(PHP5TO7_Z_OBJCE_MAYBE_P(self->future_next_page), + cassandra_future_rows_ce TSRMLS_CC)) { + zend_throw_exception_ex(cassandra_runtime_exception_ce, 0 TSRMLS_CC, + "Unexpected future instance."); + return; + } - result = cass_future_get_result(future_rows->future); - } else { - if (self->result == NULL) { - return; - } + future_rows = PHP_CASSANDRA_GET_FUTURE_ROWS(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page)); - ASSERT_SUCCESS(cass_statement_set_paging_state((CassStatement *) self->statement->data, self->result)); + if (php_cassandra_future_rows_get_result(future_rows, timeout TSRMLS_CC) == FAILURE) { + return; + } - session = PHP_CASSANDRA_GET_SESSION(PHP5TO7_ZVAL_MAYBE_P(self->session)); - future = cass_session_execute(session->session, (CassStatement *) self->statement->data); + self->next_result = php_cassandra_add_ref(future_rows->result); + } else { + cassandra_session *session = NULL; + const CassResult *result = NULL; + CassFuture *future = NULL; - if (php_cassandra_future_wait_timed(future, timeout TSRMLS_CC) == FAILURE) { - return; - } + if (self->result == NULL) { + return; + } - if (php_cassandra_future_is_error(future TSRMLS_CC) == FAILURE) { - return; - } + ASSERT_SUCCESS(cass_statement_set_paging_state((CassStatement *) self->statement->data, + (const CassResult *) self->result->data)); - result = cass_future_get_result(future); - cass_future_free(future); - } + session = PHP_CASSANDRA_GET_SESSION(PHP5TO7_ZVAL_MAYBE_P(self->session)); + future = cass_session_execute(session->session, (CassStatement *) self->statement->data); - if (!result) { - zend_throw_exception_ex(cassandra_runtime_exception_ce, 0 TSRMLS_CC, - "Future doesn't contain a result."); - return; - } + if (php_cassandra_future_wait_timed(future, timeout TSRMLS_CC) == FAILURE) { + return; + } - PHP5TO7_ZVAL_MAYBE_MAKE(self->next_page); - object_init_ex(PHP5TO7_ZVAL_MAYBE_P(self->next_page), cassandra_rows_ce); - rows = PHP_CASSANDRA_GET_ROWS(PHP5TO7_ZVAL_MAYBE_P(self->next_page)); + if (php_cassandra_future_is_error(future TSRMLS_CC) == FAILURE) { + return; + } - if (php_cassandra_get_result(result, &rows->rows TSRMLS_CC) == FAILURE) { - cass_result_free(result); - zval_dtor(PHP5TO7_ZVAL_MAYBE_P(self->next_page)); - PHP5TO7_ZVAL_UNDEF(self->next_page); - return; - } + result = cass_future_get_result(future); + if (!result) { + cass_future_free(future); + zend_throw_exception_ex(cassandra_runtime_exception_ce, 0 TSRMLS_CC, + "Future doesn't contain a result."); + return; + } - PHP5TO7_ZVAL_MAYBE_DESTROY(self->future_next_page); + self->next_result = php_cassandra_new_ref((void *)result , free_result); - if (cass_result_has_more_pages(result)) { - rows->statement = php_cassandra_add_ref(self->statement); - rows->result = result; - PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->session), - PHP5TO7_ZVAL_MAYBE_P(self->session)); - } else { - cass_result_free(result); + cass_future_free(future); + } } - php_cassandra_rows_clear(self); - RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->next_page), 1, 0); + /* Always create a new rows object to avoid creating a linked list of + * objects. + */ + php_cassandra_rows_create(self, return_value TSRMLS_CC); } PHP_METHOD(Rows, nextPageAsync) { cassandra_rows *self = NULL; cassandra_session *session = NULL; - CassFuture *future = NULL; cassandra_future_rows *future_rows = NULL; if (zend_parse_parameters_none() == FAILURE) @@ -312,13 +307,13 @@ PHP_METHOD(Rows, nextPageAsync) RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page), 1, 0); } - if (!PHP5TO7_ZVAL_IS_UNDEF(self->next_page)) { + if (self->next_result) { cassandra_future_value *future_value; PHP5TO7_ZVAL_MAYBE_MAKE(self->future_next_page); object_init_ex(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page), cassandra_future_value_ce); future_value = PHP_CASSANDRA_GET_FUTURE_VALUE(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page)); - PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(future_value->value), - PHP5TO7_ZVAL_MAYBE_P(self->next_page)); + PHP5TO7_ZVAL_MAYBE_MAKE(future_value->value); + php_cassandra_rows_create(self, PHP5TO7_ZVAL_MAYBE_P(future_value->value) TSRMLS_CC); RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page), 1, 0); } @@ -327,21 +322,21 @@ PHP_METHOD(Rows, nextPageAsync) return; } - ASSERT_SUCCESS(cass_statement_set_paging_state((CassStatement *) self->statement->data, self->result)); + ASSERT_SUCCESS(cass_statement_set_paging_state((CassStatement *) self->statement->data, + (const CassResult *) self->result->data)); session = PHP_CASSANDRA_GET_SESSION(PHP5TO7_ZVAL_MAYBE_P(self->session)); - future = cass_session_execute(session->session, (CassStatement *) self->statement->data); PHP5TO7_ZVAL_MAYBE_MAKE(self->future_next_page); object_init_ex(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page), cassandra_future_rows_ce); future_rows = PHP_CASSANDRA_GET_FUTURE_ROWS(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page)); future_rows->statement = php_cassandra_add_ref(self->statement); - future_rows->future = future; + future_rows->future = cass_session_execute(session->session, + (CassStatement *) self->statement->data); PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(future_rows->session), PHP5TO7_ZVAL_MAYBE_P(self->session)); - php_cassandra_rows_clear(self); RETURN_ZVAL(PHP5TO7_ZVAL_MAYBE_P(self->future_next_page), 1, 0); } @@ -359,7 +354,7 @@ PHP_METHOD(Rows, pagingStateToken) if (self->result == NULL) return; - ASSERT_SUCCESS(cass_result_paging_state_token(self->result, + ASSERT_SUCCESS(cass_result_paging_state_token((const CassResult *) self->result->data, &paging_state, &paging_state_size)); PHP5TO7_RETURN_STRINGL(paging_state, paging_state_size); @@ -443,11 +438,13 @@ php_cassandra_rows_free(php5to7_zend_object_free *object TSRMLS_DC) { cassandra_rows *self = PHP5TO7_ZEND_OBJECT_GET(rows, object); - php_cassandra_rows_clear(self); + php_cassandra_del_ref(&self->result); + php_cassandra_del_ref(&self->statement); + php_cassandra_del_ref(&self->next_result); PHP5TO7_ZVAL_MAYBE_DESTROY(self->session); PHP5TO7_ZVAL_MAYBE_DESTROY(self->rows); - PHP5TO7_ZVAL_MAYBE_DESTROY(self->next_page); + PHP5TO7_ZVAL_MAYBE_DESTROY(self->next_rows); PHP5TO7_ZVAL_MAYBE_DESTROY(self->future_next_page); zend_object_std_dtor(&self->zval TSRMLS_CC); @@ -460,11 +457,12 @@ php_cassandra_rows_new(zend_class_entry *ce TSRMLS_DC) cassandra_rows *self = PHP5TO7_ZEND_OBJECT_ECALLOC(rows, ce); - self->statement = NULL; - self->result = NULL; + self->statement = NULL; + self->result = NULL; + self->next_result = NULL; PHP5TO7_ZVAL_UNDEF(self->session); PHP5TO7_ZVAL_UNDEF(self->rows); - PHP5TO7_ZVAL_UNDEF(self->next_page); + PHP5TO7_ZVAL_UNDEF(self->next_rows); PHP5TO7_ZVAL_UNDEF(self->future_next_page); PHP5TO7_ZEND_OBJECT_INIT(rows, self, ce); From 2ac03c20abf48b55f1b9f358da7f2c24666922f8 Mon Sep 17 00:00:00 2001 From: Michael Fero Date: Tue, 12 Jul 2016 12:34:50 -0400 Subject: [PATCH 2/4] fix: Correcting build for PHP 7 --- ext/src/Cassandra/Rows.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/src/Cassandra/Rows.c b/ext/src/Cassandra/Rows.c index 472b04db9..1ea8c05b6 100644 --- a/ext/src/Cassandra/Rows.c +++ b/ext/src/Cassandra/Rows.c @@ -42,7 +42,7 @@ php_cassandra_rows_create(cassandra_rows *current, zval *result TSRMLS_DC) { } object_init_ex(result, cassandra_rows_ce); - rows = PHP_CASSANDRA_GET_ROWS(PHP5TO7_ZVAL_MAYBE_P(result)); + rows = PHP_CASSANDRA_GET_ROWS(result); PHP5TO7_ZVAL_COPY(PHP5TO7_ZVAL_MAYBE_P(rows->rows), PHP5TO7_ZVAL_MAYBE_P(current->next_rows)); From 59da2a2c6203c4f234c2b92c3ce4476045d31a89 Mon Sep 17 00:00:00 2001 From: Michael Fero Date: Tue, 12 Jul 2016 20:11:26 -0400 Subject: [PATCH 3/4] test: Adding test for PHP-101 (Memory leak when paging enabled) --- .../Cassandra/PagingIntegrationTest.php | 131 +++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/tests/integration/Cassandra/PagingIntegrationTest.php b/tests/integration/Cassandra/PagingIntegrationTest.php index 48499d17c..0b7b52b18 100644 --- a/tests/integration/Cassandra/PagingIntegrationTest.php +++ b/tests/integration/Cassandra/PagingIntegrationTest.php @@ -18,8 +18,7 @@ namespace Cassandra; -class PagingIntegrationTest extends BasicIntegrationTest -{ +class PagingIntegrationTest extends BasicIntegrationTest { public function setUp() { parent::setUp(); @@ -40,6 +39,66 @@ public function setUp() { } } + /** + * Generate a random string + * + * @param int $length Length of string to generate (DEFAULT: random length + * from 1 - 1024 characters) + * @return string Randomly genreated text + */ + private function randomString($length = -1) { + // Determine if the length should be random + if ($length < 0) { + $length = mt_rand(1, 1024); + } + + // Generate the random string from the below character set + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ '; + $charactersLength = strlen($characters); + $randomString = ''; + foreach (range(1, $length) as $i) { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + return $randomString; + } + + /** + * Page through the results while validating no memory leaks exists + * + * @param $start Starting memory value + * @return int Number of rows visited + */ + private function validatePageResults($rows) { + // Get the starting memory usage + $start = memory_get_usage() / 1024; + if (Integration::isDebug() && Integration::isVerbose()) { + fprintf(STDOUT, "Start Usage: %dkb" . PHP_EOL, $start); + } + + // Page over each result set and count the number of rows visited + $count = $rows->count(); + while ($rows = $rows->nextPage()) { + if ($rows->count() != 0) { + $count += $rows->count(); + if (Integration::isDebug() && Integration::isVerbose()) { + fprintf(STDOUT, "Page %d: Current memory usage is %dkb" . PHP_EOL, + ($count / 2), ((memory_get_usage() / 1024) - $start)); + } + } + } + + // Get the final memory usage (and apply a tolerance to compensate for GC) + $end = memory_get_usage() / 1024; + if (Integration::isDebug() && Integration::isVerbose()) { + fprintf(STDOUT, "End Usage: %dkb [%dkb]" . PHP_EOL, $end, ($end - $start)); + } + $difference = ($end - $start) - 20; // 20KB tolerance + $this->assertLessThanOrEqual(0, $difference); + + // Return the number of rows visited + return $count; + } + /** * Use paging state token * @@ -119,4 +178,72 @@ public function testNullToken() { $result = $this->session->execute($statement, $options); } + + /** + * Paging advancement does not create memory leak + * + * This test will ensure that the driver does not create memory leaks + * associated advancing to the next page of results. + * + * @test + * @ticket PHP-101 + */ + public function testNoPagingMemoryLeak() { + // Create the user types and table for the test + $this->session->execute(new SimpleStatement( + "DROP TABLE {$this->tableNamePrefix}" + )); + $this->session->execute(new SimpleStatement( + "CREATE TYPE price_history (time timestamp, price float)" + )); + $priceHistory = Type::userType( + "time", Type::timestamp(), + "price", Type::float()); + $this->session->execute(new SimpleStatement( + "CREATE TYPE purchase_stats (day_of_week int, total_purchases int)" + )); + $purchaseStats = Type::userType( + "day_of_week", Type::int(), + "total_purchases", Type::int()); + $this->session->execute(new SimpleStatement( + "CREATE TABLE {$this->tableNamePrefix} (id uuid PRIMARY KEY, + history frozen, stats frozen, + comments text)" + )); + + // Populate the table with some random data + $totalInserts = 500; + $statement = $this->session->prepare("INSERT INTO {$this->tableNamePrefix} + (id, history, stats, comments) VALUES (?, ?, ?, ?)"); + foreach (range(1, $totalInserts) as $i) { + // Create the values for the insert + $history = $priceHistory->create( + "time", new Timestamp(mt_rand(1270094400000, 1459483200000)), // 04-01-2010 - 04-01-2016 + "price", new Float((mt_rand(1, 1000) / 100)) + ); + $stats = $purchaseStats->create( + "day_of_week", mt_rand(0, 6), + "total_purchases", mt_rand(0, 1000) + ); + $values = array( + new Uuid(), + $history, + $stats, + $this->randomString() + ); + + $options = new ExecutionOptions(array("arguments" => $values)); + $this->session->execute($statement, $options); + } + + // Select all the rows in the table using paging + $statement = new SimpleStatement("SELECT * FROM {$this->tableNamePrefix}"); + $options = new ExecutionOptions(array("page_size" => 2)); + $rows = $this->session->execute($statement, $options); + + + // Validate paging and ensure all the rows were read + $count = $this->validatePageResults($rows); + $this->assertEquals($totalInserts, $count); + } } From 41f10b43b41b9535b6c75a08bd01121076ff3567 Mon Sep 17 00:00:00 2001 From: Michael Penick Date: Thu, 14 Jul 2016 14:50:39 -0700 Subject: [PATCH 4/4] Added next page caching tests for Cassandra\Rows --- .../Cassandra/PagingIntegrationTest.php | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/integration/Cassandra/PagingIntegrationTest.php b/tests/integration/Cassandra/PagingIntegrationTest.php index 0b7b52b18..457ca8f2b 100644 --- a/tests/integration/Cassandra/PagingIntegrationTest.php +++ b/tests/integration/Cassandra/PagingIntegrationTest.php @@ -39,6 +39,22 @@ public function setUp() { } } + /** + * Convert a single column of a collection of rows into an array + * + * @param Cassandra\Rows + * @param string Column name to consolidate + * + * @return array Array of column values using the provided column name + */ + private static function convertRowsToArray($rows, $columnName) { + $values = array(); + foreach ($rows as $row) { + $values []= $row[$columnName]; + } + return $values; + } + /** * Generate a random string * @@ -179,6 +195,96 @@ public function testNullToken() { $result = $this->session->execute($statement, $options); } + /** + * Verify next page caching in `Cassandra\Rows` + * + * @test + * @ticket PHP-101 + */ + public function testNextPageCaching() { + $results = array(); + $pageSize = 2; + + $options = array("page_size" => $pageSize); + $statement = new SimpleStatement( + "SELECT * FROM {$this->tableNamePrefix}" + ); + + // Get first page + $rows = $this->session->execute($statement, new ExecutionOptions($options)); + $this->assertEquals($rows->count(), $pageSize); + $values = self::convertRowsToArray($rows, "value"); + + // Get next page (verify that it's a different page) + $nextRows = $rows->nextPage(); + $nextValues = self::convertRowsToArray($nextRows, "value"); + $this->assertEquals($nextRows->count(), $pageSize); + $this->assertNotEquals($values, $nextValues); + + // Get next page again (verify that it's the same) + $nextRowsAgain = $rows->nextPage(); + $this->assertEquals($nextRowsAgain->count(), $pageSize); + $nextValuesAgain = self::convertRowsToArray($nextRowsAgain, "value"); + $this->assertEquals($nextValues, $nextValuesAgain); + + // Get next page asynchonously (verify that it's the same) + $nextRowsAsync = $rows->nextPageAsync()->get(); + $this->assertEquals($nextRowsAsync->count(), $pageSize); + $nextValuesAsync = self::convertRowsToArray($nextRowsAsync, "value"); + $this->assertEquals($nextValues, $nextValuesAsync); + + // Get the next page's page (verify that it's a different page) + $lastRows = $nextRows->nextPage(); + $this->assertEquals($lastRows->count(), $pageSize); + $lastValues = self::convertRowsToArray($lastRows, "value"); + $this->assertNotEquals($nextValues, $lastValues); + } + + /** + * Verify next page asynchronous caching in `Cassandra\Rows` + * + * @test + * @ticket PHP-101 + */ + public function testNextPageAsyncCaching() { + $results = array(); + $pageSize = 2; + + $options = array("page_size" => $pageSize); + $statement = new SimpleStatement( + "SELECT * FROM {$this->tableNamePrefix}" + ); + + // Get first page + $rows = $this->session->execute($statement, new ExecutionOptions($options)); + $this->assertEquals($rows->count(), $pageSize); + $values = self::convertRowsToArray($rows, "value"); + + // Get next page asynchronously (verify that it's a different page) + $nextRowsAsync = $rows->nextPageAsync()->get(); + $this->assertEquals($nextRowsAsync->count(), $pageSize); + $nextValuesAsync = self::convertRowsToArray($nextRowsAsync, "value"); + $this->assertNotEquals($values, $nextValuesAsync); + + // Get next page asynchronously again (verify that it's the same) + $nextRowsAgainAsync = $rows->nextPageAsync()->get(); + $this->assertEquals($nextRowsAgainAsync->count(), $pageSize); + $nextValuesAgainAsync = self::convertRowsToArray($nextRowsAgainAsync, "value"); + $this->assertEquals($nextValuesAsync, $nextValuesAgainAsync); + + // Get the next page again synchonously (verify that it's the same) + $nextRows = $rows->nextPage(); + $nextValues = self::convertRowsToArray($nextRows, "value"); + $this->assertEquals($nextRows->count(), $pageSize); + $this->assertEquals($nextValuesAsync, $nextValues); + + // Get the next page's page asynchronously (verify that it's a different page) + $lastRowsAsync = $nextRowsAsync->nextPageAsync()->get(); + $this->assertEquals($lastRowsAsync->count(), $pageSize); + $lastValuesAsync = self::convertRowsToArray($lastRowsAsync, "value"); + $this->assertNotEquals($nextValuesAsync, $lastValuesAsync); + } + /** * Paging advancement does not create memory leak *