1<?php
2
3/**
4 * Copyright 2015-2017 DataStax, Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19namespace Cassandra;
20
21/**
22 * Retry policy integration tests.
23 */
24class RetryPolicyIntegrationTest extends BasicIntegrationTest {
25    /**
26     * The number of inserts and asserts to perform during testing.
27     */
28    const NUMBER_OF_INSERTS = 25;
29    /**
30     * Maximum number of TimeoutExceptions to bound the insert/assert functions
31     * recursive calls/
32     */
33    const NUMBER_OF_TIMEOUT_EXCEPTIONS = 5;
34    /**
35     * Batch statement type
36     */
37    const BATCH_STATEMENT = 1;
38    /**
39     * Prepared statement type
40     */
41    const PREPARED_STATEMENT = 2;
42    /**
43     * Simple statement type
44     */
45    const SIMPLE_STATEMENT = 3;
46
47    /**
48     * Insert query generated for a retry policy test.
49     *
50     * @var string
51     */
52    private $insertQuery;
53
54    /**
55     * Setup the retry policy for multiple nodes.
56     */
57    public function setUp() {
58        // Ensure RF = 3 (anything greater than 1 is good)
59        $this->replicationFactor = 3;
60
61        // Process parent setup steps
62        parent::setUp();
63
64        // Create the table
65        $query = "CREATE TABLE {$this->tableNamePrefix} (key int, value_int int, PRIMARY KEY(key, value_int))";
66        $this->session->execute($query);
67
68        // Create the insert query
69        $this->insertQuery = "INSERT INTO {$this->tableNamePrefix} (key, value_int) VALUES (?, ?)";
70    }
71
72    /**
73     * Insert n values into the table for a given key.
74     *
75     * @param $statementType Type of statement to create for inserts
76     * @param RetryPolicy $policy RetryPolicy to use when executing statements
77     * @param $key Key value
78     * @param $numberOfInserts Number of inserts to perform
79     * @param $consistency Consistency level to execute statement
80     * @param int $retries Number of TimeoutException retries
81     *                     (DEFAULT: self::NUMBER_OF_TIMEOUT_EXCEPTIONS)
82     */
83    private function insert($statementType, RetryPolicy $policy, $key, $numberOfInserts, $consistency, $retries = self::NUMBER_OF_TIMEOUT_EXCEPTIONS) {
84        try {
85            // Create all statement types
86            $batch = new BatchStatement(\Cassandra::BATCH_UNLOGGED);
87            $prepare = $this->session->prepare($this->insertQuery);
88
89            // Create the default execution options
90            $options = array(
91                "consistency" => $consistency,
92                "retry_policy" => $policy
93            );
94
95            // Create the inserts
96            foreach (range(1, $numberOfInserts) as $i) {
97                $values = array(
98                    $key,
99                    $i
100                );
101                if ($statementType == self::BATCH_STATEMENT) {
102                    if ($i % 2 == 0) {
103                        $batch->add($prepare, $values);
104                    } else {
105                        $batch->add($this->insertQuery, $values);
106                    }
107                } else {
108                    // Execute either the prepare or simple statment
109                    $statement = $prepare;
110                    if ($statementType == self::SIMPLE_STATEMENT) {
111                        $statement = $this->insertQuery;
112                    }
113                    $options["arguments"] = $values;
114                    $this->session->execute($statement, $options);
115                }
116            }
117
118            // Execute the batched insert
119            if ($statementType == self::BATCH_STATEMENT) {
120                $this->session->execute($batch,  $options);
121            }
122        } catch (Exception\TimeoutException $te) {
123            if (Integration::isDebug()) {
124                fprintf(STDOUT, "Insert TimeoutException: %s (%s:%d)" . PHP_EOL,
125                    $te->getMessage(), $te->getFile(), $te->getLine());
126            }
127            if ($retries > 0) {
128                $this->insert($policy, $key, $numberOfInserts, $consistency, ($retries - 1));
129            } else {
130                throw $te;
131            }
132        }
133    }
134
135    /**
136     * Assert n values in the table for a given key.
137     *
138     * @param RetryPolicy $policy RetryPolicy to use when executing statements
139     * @param $key Key value
140     * @param $numberOfAsserts Number of inserts to perform
141     * @param $consistency Consistency level to execute statement
142     * @param int $retries Number of TimeoutException retries
143     *                     (DEFAULT: self::NUMBER_OF_TIMEOUT_EXCEPTIONS)
144     */
145    private function assert(RetryPolicy $policy, $key, $numberOfAsserts, $consistency, $retries = self::NUMBER_OF_TIMEOUT_EXCEPTIONS) {
146        try {
147            // Select the values
148            $options = array(
149                "consistency" => $consistency,
150                "retry_policy" => $policy
151            );
152            $rows = $this->session->execute(
153                "SELECT value_int FROM {$this->tableNamePrefix} WHERE key = {$key}",
154                $options
155            );
156
157            // Assert the values
158            $this->assertCount($numberOfAsserts, $rows);
159            foreach ($rows as $i => $row) {
160                $this->assertEquals(($i + 1), $row["value_int"]);
161            }
162        } catch (Exception\TimeoutException $te) {
163            if (Integration::isDebug()) {
164                fprintf(STDOUT, "Assert TimeoutException: %s (%s:%d)" . PHP_EOL,
165                    $te->getMessage(), $te->getFile(), $te->getLine());
166            }
167            if ($retries > 0) {
168                $this->assert($policy, $key, $numberOfAsserts, $consistency, ($retries - 1));
169            } else {
170                throw $te;
171            }
172        }
173    }
174
175    /**
176     * Statement execution supports downgrading consistency retry policy.
177     *
178     * This test will ensure that the PHP driver supports the downgrading
179     * retry policy when executing statements.
180     *
181     * @test
182     * @ticket PHP-60
183     *
184     * @cassandra-version-2.0
185     */
186    public function testDowngradingPolicy() {
187        // Create the retry policy (RF = 3 with 1 node)
188        $policy = new RetryPolicy\DowngradingConsistency();
189
190        // Iterate over each statement type
191        foreach (range(1, 3) as $statementType) {
192            // Determine if the statement type should be skipped
193            if ($statementType == self::BATCH_STATEMENT
194                && version_compare(\Cassandra::CPP_DRIVER_VERSION, "2.2.3") < 0) {
195                if (Integration::isDebug()) {
196                    fprintf(STDOUT, "Skipping Batch Statements in %s: Issue fixed in DataStax C/C++ v2.2.3" . PHP_EOL,
197                        $this->getName());
198                }
199            } else {
200                // Insert and assert values with CONSISTENCY_ALL
201                $this->insert($statementType, $policy, 0, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_ALL);
202                $this->assert($policy, 0, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_ALL);
203
204                // Insert and assert values with CONSISTENCY_QUORUM
205                $this->insert($statementType, $policy, 1, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_QUORUM);
206                $this->assert($policy, 1, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_QUORUM);
207
208                // Insert and assert values with CONSISTENCY_TWO
209                $this->insert($statementType, $policy, 2, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_TWO);
210                $this->assert($policy, 2, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_TWO);
211            }
212        }
213    }
214
215    /**
216     * Statement execution supports fallthrough retry policy (write exception).
217     *
218     * This test will ensure that the PHP driver supports the ability to
219     * provide any exception that occurs  when executing statements. This test
220     * will ensure that a WriteTimeoutException occurs when a consistency level
221     * cannot be achieved.
222     *
223     * @test
224     * @ticket PHP-60
225     *
226     * @cassandra-version-2.0
227     *
228     * @expectedException \Cassandra\Exception\UnavailableException
229     * @expectedExceptionMessageRegExp |Cannot achieve consistency level .*|
230     */
231    public function testFallThroughPolicyWrite() {
232        // Create the retry policy (RF = 3 with 1 node)
233        $policy = new RetryPolicy\Fallthrough();
234
235        // Iterate over each statement type
236        foreach (range(1, 3) as $statementType) {
237            // Determine if the statement type should be skipped
238            if ($statementType == self::BATCH_STATEMENT
239                && version_compare(\Cassandra::CPP_DRIVER_VERSION, "2.2.3") < 0) {
240                if (Integration::isDebug()) {
241                    fprintf(STDOUT, "Skipping Batch Statements in %s: Issue fixed in DataStax C/C++ v2.2.3" . PHP_EOL,
242                        $this->getName());
243                }
244            } else {
245                // Create an exception during write
246                $this->insert($statementType, $policy, 0, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_ALL);
247            }
248        }
249    }
250
251    /**
252     * Statement execution supports fallthrough retry policy (read exception).
253     *
254     * This test will ensure that the PHP driver supports the ability to
255     * provide any exception that occurs  when executing statements. This test
256     * will ensure that a ReadTimeoutException occurs when a consistency level
257     * cannot be achieved.
258     *
259     * @test
260     * @ticket PHP-60
261     *
262     * @cassandra-version-2.0
263     *
264     * @expectedException \Cassandra\Exception\UnavailableException
265     * @expectedExceptionMessageRegExp |Cannot achieve consistency level .*|
266     */
267    public function testFallThroughPolicyRead() {
268        // Create the retry policy (RF = 3 with 1 node)
269        $policy = new RetryPolicy\Fallthrough();
270
271        // Iterate over each statement type
272        foreach (range(1, 3) as $statementType) {
273            // Determine if the statement type should be skipped
274            if ($statementType == self::BATCH_STATEMENT
275                && version_compare(\Cassandra::CPP_DRIVER_VERSION, "2.2.3") < 0) {
276                if (Integration::isDebug()) {
277                    fprintf(STDOUT, "Skipping Batch Statements in %s: Issue fixed in DataStax C/C++ v2.2.3" . PHP_EOL,
278                        $this->getName());
279                }
280            } else {
281                // Create an exception during read
282                $this->insert($statementType, $policy, 0, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_ONE);
283                $this->assert($policy, 0, self::NUMBER_OF_INSERTS, \Cassandra::CONSISTENCY_ALL);
284            }
285        }
286    }
287}
288