Skip to content

Commit 2557520

Browse files
authored
Modified how drivers handle query timeout settings (#1037)
1 parent fdf029d commit 2557520

File tree

9 files changed

+371
-40
lines changed

9 files changed

+371
-40
lines changed

source/pdo_sqlsrv/pdo_dbh.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -720,13 +720,6 @@ int pdo_sqlsrv_dbh_prepare( _Inout_ pdo_dbh_t *dbh, _In_reads_(sql_len) const ch
720720
driver_stmt->buffered_query_limit = driver_dbh->client_buffer_max_size;
721721
}
722722

723-
// if the user didn't set anything in the prepare options, then set the query timeout
724-
// to the value set on the connection.
725-
if(( driver_stmt->query_timeout == QUERY_TIMEOUT_INVALID ) && ( driver_dbh->query_timeout != QUERY_TIMEOUT_INVALID )) {
726-
727-
core_sqlsrv_set_query_timeout( driver_stmt, driver_dbh->query_timeout TSRMLS_CC );
728-
}
729-
730723
// rewrite named parameters in the query to positional parameters if we aren't letting PDO do the
731724
// parameter substitution for us
732725
if( stmt->supports_placeholders != PDO_PLACEHOLDER_NONE ) {

source/pdo_sqlsrv/pdo_stmt.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,11 @@ int pdo_sqlsrv_stmt_execute( _Inout_ pdo_stmt_t *stmt TSRMLS_DC )
580580
query_len = static_cast<unsigned int>(stmt->active_query_stringlen);
581581
}
582582

583+
// The query timeout setting is inherited from the corresponding connection attribute, but
584+
// the user may have changed the query timeout setting again before this via
585+
// PDOStatement::setAttribute()
586+
driver_stmt->set_query_timeout();
587+
583588
SQLRETURN execReturn = core_sqlsrv_execute( driver_stmt TSRMLS_CC, query, query_len );
584589

585590
if ( execReturn == SQL_NO_DATA ) {
@@ -1503,3 +1508,11 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type,
15031508
return sqlsrv_phptype;
15041509
}
15051510

1511+
void pdo_sqlsrv_stmt::set_query_timeout()
1512+
{
1513+
if (query_timeout == QUERY_TIMEOUT_INVALID || query_timeout < 0) {
1514+
return;
1515+
}
1516+
1517+
core::SQLSetStmtAttr(this, SQL_ATTR_QUERY_TIMEOUT, reinterpret_cast<SQLPOINTER>((SQLLEN)query_timeout), SQL_IS_UINTEGER TSRMLS_CC);
1518+
}

source/pdo_sqlsrv/php_pdo_sqlsrv_int.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt {
246246
fetch_datetime = db->fetch_datetime;
247247
format_decimals = db->format_decimals;
248248
decimal_places = db->decimal_places;
249+
query_timeout = db->query_timeout;
249250
}
250251

251252
virtual ~pdo_sqlsrv_stmt( void );
@@ -254,6 +255,9 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt {
254255
// for PDO, everything is a string, so we return SQLSRV_PHPTYPE_STRING for all SQL types
255256
virtual sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream );
256257

258+
// driver specific way to set query timeout
259+
virtual void set_query_timeout();
260+
257261
bool direct_query; // flag set if the query should be executed directly or prepared
258262
const char* direct_query_subst_string; // if the query is direct, hold the substitution string if using named parameters
259263
size_t direct_query_subst_string_len; // length of query string used for direct queries

source/shared/core_sqlsrv.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,8 @@ struct sqlsrv_stmt : public sqlsrv_context {
15581558
// driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants
15591559
virtual sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream ) = 0;
15601560

1561+
// driver specific way to set query timeout
1562+
virtual void set_query_timeout() = 0;
15611563
};
15621564

15631565
// *** field metadata struct ***
@@ -1616,7 +1618,6 @@ bool core_sqlsrv_has_any_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC );
16161618
void core_sqlsrv_next_result( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC, _In_ bool finalize_output_params = true, _In_ bool throw_on_errors = true );
16171619
void core_sqlsrv_post_param( _Inout_ sqlsrv_stmt* stmt, _In_ zend_ulong paramno, zval* param_z TSRMLS_DC );
16181620
void core_sqlsrv_set_scrollable( _Inout_ sqlsrv_stmt* stmt, _In_ unsigned long cursor_type TSRMLS_DC );
1619-
void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _In_ long timeout TSRMLS_DC );
16201621
void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* value_z TSRMLS_DC );
16211622
void core_sqlsrv_set_send_at_exec( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z TSRMLS_DC );
16221623
bool core_sqlsrv_send_stream_packet( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC );

source/shared/core_stmt.cpp

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,11 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm
322322
} ZEND_HASH_FOREACH_END();
323323
}
324324

325+
// The query timeout setting is inherited from the corresponding connection attribute, but
326+
// the user may override that the query timeout setting using the statement option.
327+
// In any case, set query timeout using the latest value
328+
stmt->set_query_timeout();
329+
325330
return_stmt = stmt;
326331
stmt.transferred();
327332
}
@@ -1361,7 +1366,7 @@ void core_sqlsrv_set_buffered_query_limit( _Inout_ sqlsrv_stmt* stmt, _In_ SQLLE
13611366
}
13621367

13631368

1364-
// Overloaded. Extracts the long value and calls the core_sqlsrv_set_query_timeout
1369+
// Extracts the long value and calls the core_sqlsrv_set_query_timeout
13651370
// which accepts timeout parameter as a long. If the zval is not of type long
13661371
// than throws error.
13671372
void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* value_z TSRMLS_DC )
@@ -1375,37 +1380,8 @@ void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _Inout_ zval* val
13751380
THROW_CORE_ERROR( stmt, SQLSRV_ERROR_INVALID_QUERY_TIMEOUT_VALUE, Z_STRVAL_P( value_z ) );
13761381
}
13771382

1378-
core_sqlsrv_set_query_timeout( stmt, static_cast<long>( Z_LVAL_P( value_z )) TSRMLS_CC );
1379-
}
1380-
catch( core::CoreException& ) {
1381-
throw;
1382-
}
1383-
}
1384-
1385-
// Overloaded. Accepts the timeout as a long.
1386-
void core_sqlsrv_set_query_timeout( _Inout_ sqlsrv_stmt* stmt, _In_ long timeout TSRMLS_DC )
1387-
{
1388-
try {
1389-
1390-
DEBUG_SQLSRV_ASSERT( timeout >= 0 , "core_sqlsrv_set_query_timeout: The value of query timeout cannot be less than 0." );
1391-
1392-
// set the statement attribute
1393-
core::SQLSetStmtAttr( stmt, SQL_ATTR_QUERY_TIMEOUT, reinterpret_cast<SQLPOINTER>( (SQLLEN)timeout ), SQL_IS_UINTEGER TSRMLS_CC );
1394-
1395-
// a query timeout of 0 indicates "no timeout", which means that lock_timeout should also be set to "no timeout" which
1396-
// is represented by -1.
1397-
int lock_timeout = (( timeout == 0 ) ? -1 : timeout * 1000 /*convert to milliseconds*/ );
1398-
1399-
// set the LOCK_TIMEOUT on the server.
1400-
char lock_timeout_sql[32] = {'\0'};
1401-
1402-
int written = snprintf( lock_timeout_sql, sizeof( lock_timeout_sql ), "SET LOCK_TIMEOUT %d", lock_timeout );
1403-
SQLSRV_ASSERT( (written != -1 && written != sizeof( lock_timeout_sql )),
1404-
"stmt_option_query_timeout: snprintf failed. Shouldn't ever fail." );
1405-
1406-
core::SQLExecDirect( stmt, lock_timeout_sql TSRMLS_CC );
1407-
1408-
stmt->query_timeout = timeout;
1383+
// Save the query timeout setting for processing later
1384+
stmt->query_timeout = static_cast<long>(Z_LVAL_P(value_z));
14091385
}
14101386
catch( core::CoreException& ) {
14111387
throw;

source/sqlsrv/php_sqlsrv_int.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ struct ss_sqlsrv_stmt : public sqlsrv_stmt {
124124
// driver specific conversion rules from a SQL Server/ODBC type to one of the SQLSRV_PHPTYPE_* constants
125125
sqlsrv_phptype sql_type_to_php_type( _In_ SQLINTEGER sql_type, _In_ SQLUINTEGER size, _In_ bool prefer_string_to_stream );
126126

127+
// driver specific way to set query timeout
128+
virtual void set_query_timeout();
129+
127130
bool prepared; // whether the statement has been prepared yet (used for error messages)
128131
zend_ulong conn_index; // index into the connection hash that contains this statement structure
129132
zval* params_z; // hold parameters passed to sqlsrv_prepare but not used until sqlsrv_execute

source/sqlsrv/stmt.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,29 @@ sqlsrv_phptype ss_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, _
267267
return ss_phptype;
268268
}
269269

270+
void ss_sqlsrv_stmt::set_query_timeout()
271+
{
272+
if (query_timeout == QUERY_TIMEOUT_INVALID || query_timeout < 0) {
273+
return;
274+
}
275+
276+
// set the statement attribute
277+
core::SQLSetStmtAttr(this, SQL_ATTR_QUERY_TIMEOUT, reinterpret_cast<SQLPOINTER>( (SQLLEN)query_timeout ), SQL_IS_UINTEGER TSRMLS_CC );
278+
279+
// a query timeout of 0 indicates "no timeout", which means that lock_timeout should also be set to "no timeout" which
280+
// is represented by -1.
281+
int lock_timeout = (( query_timeout == 0 ) ? -1 : query_timeout * 1000 /*convert to milliseconds*/ );
282+
283+
// set the LOCK_TIMEOUT on the server.
284+
char lock_timeout_sql[32] = {'\0'};
285+
286+
int written = snprintf( lock_timeout_sql, sizeof( lock_timeout_sql ), "SET LOCK_TIMEOUT %d", lock_timeout );
287+
SQLSRV_ASSERT( (written != -1 && written != sizeof( lock_timeout_sql )),
288+
"stmt_option_query_timeout: snprintf failed. Shouldn't ever fail." );
289+
290+
core::SQLExecDirect(this, lock_timeout_sql TSRMLS_CC );
291+
}
292+
270293
// statement specific parameter proccessing. Uses the generic function specialised to return a statement
271294
// resource.
272295
#define PROCESS_PARAMS( rsrc, param_spec, calling_func, param_count, ... ) \
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
--TEST--
2+
GitHub issue 1027 - PDO::SQLSRV_ATTR_QUERY_TIMEOUT had no effect on PDO::exec()
3+
--DESCRIPTION--
4+
This test verifies that setting PDO::SQLSRV_ATTR_QUERY_TIMEOUT correctly should affect PDO::exec() as in the case for PDO::prepare() (as statement attribute or option).
5+
--ENV--
6+
PHPT_EXEC=true
7+
--SKIPIF--
8+
<?php require('skipif_mid-refactor.inc'); ?>
9+
--FILE--
10+
<?php
11+
require_once("MsSetup.inc");
12+
require_once("MsCommon_mid-refactor.inc");
13+
14+
const _DELAY = 5;
15+
16+
$message = '*Invalid value timeout specified for option PDO::SQLSRV_ATTR_QUERY_TIMEOUT.';
17+
$delay = _DELAY;
18+
$query = "WAITFOR DELAY '00:00:$delay'; SELECT 1";
19+
$error = '*Query timeout expired';
20+
21+
function testTimeoutAttribute($conn, $timeout, $statementLevel = false)
22+
{
23+
global $message;
24+
25+
$invalid = str_replace('timeout', $timeout, $message);
26+
27+
try {
28+
if ($statementLevel) {
29+
trace("statement option expects error: $invalid\n");
30+
$options = array(PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $timeout);
31+
$sql = 'SELECT 1';
32+
$stmt = $conn->prepare($sql, $options);
33+
} else {
34+
trace("connection attribute expects error: $invalid\n");
35+
$conn->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout);
36+
}
37+
} catch (PDOException $e) {
38+
if (!fnmatch($invalid, $e->getMessage())) {
39+
echo "Unexpected error returned setting invalid $timeout for SQLSRV_ATTR_QUERY_TIMEOUT\n";
40+
var_dump($e->getMessage());
41+
}
42+
}
43+
}
44+
45+
function testErrors($conn)
46+
{
47+
testTimeoutAttribute($conn, 1.8);
48+
testTimeoutAttribute($conn, 'xyz');
49+
testTimeoutAttribute($conn, -99, true);
50+
testTimeoutAttribute($conn, 'abc', true);
51+
}
52+
53+
function checkTimeElapsed($message, $t0, $t1, $expectedDelay)
54+
{
55+
$elapsed = $t1 - $t0;
56+
$diff = abs($elapsed - $expectedDelay);
57+
$leeway = 1.0;
58+
$missed = ($diff > $leeway);
59+
trace("$message $elapsed secs elapsed\n");
60+
61+
if ($missed) {
62+
echo $message;
63+
echo "Expected $expectedDelay but $elapsed secs elapsed\n";
64+
}
65+
}
66+
67+
function connectionTest($timeout, $asAttribute)
68+
{
69+
global $query, $error;
70+
$keyword = '';
71+
72+
if ($asAttribute) {
73+
$conn = connect($keyword);
74+
$conn->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout);
75+
} else {
76+
$options = array(PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $timeout);
77+
$conn = connect($keyword, $options);
78+
}
79+
80+
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
81+
82+
// if timeout is 0 it means no timeout
83+
$delay = ($timeout > 0) ? $timeout : _DELAY;
84+
85+
$result = null;
86+
$t0 = microtime(true);
87+
88+
try {
89+
$result = $conn->exec($query);
90+
if ($timeout > 0) {
91+
echo "connectionTest $timeout, $asAttribute: ";
92+
echo "this should have timed out!\n";
93+
}
94+
} catch (PDOException $e) {
95+
if (!fnmatch($error, $e->getMessage())) {
96+
echo "Connection test error expected $timeout, $asAttribute:\n";
97+
var_dump($e->getMessage());
98+
}
99+
}
100+
101+
$t1 = microtime(true);
102+
checkTimeElapsed("connectionTest ($timeout, $asAttribute): ", $t0, $t1, $delay);
103+
104+
return $conn;
105+
}
106+
107+
function queryTest($conn, $timeout)
108+
{
109+
global $query, $error;
110+
111+
// if timeout is 0 it means no timeout
112+
$delay = ($timeout > 0) ? $timeout : _DELAY;
113+
114+
$t0 = microtime(true);
115+
try {
116+
$conn->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout);
117+
$stmt = $conn->query($query);
118+
119+
if ($timeout > 0) {
120+
echo "Query test $timeout: should have timed out!\n";
121+
}
122+
} catch (PDOException $e) {
123+
if (!fnmatch($error, $e->getMessage())) {
124+
echo "Query test error expected $timeout:\n";
125+
var_dump($e->getMessage());
126+
}
127+
}
128+
129+
$t1 = microtime(true);
130+
131+
checkTimeElapsed("Query test ($timeout): ", $t0, $t1, $delay);
132+
133+
unset($stmt);
134+
}
135+
136+
function statementTest($conn, $timeout, $asAttribute)
137+
{
138+
global $query, $error;
139+
140+
// if timeout is 0 it means no timeout
141+
$delay = ($timeout > 0) ? $timeout : _DELAY;
142+
143+
$result = null;
144+
$t0 = microtime(true);
145+
146+
try {
147+
if ($asAttribute) {
148+
$stmt = $conn->prepare($query);
149+
$stmt->setAttribute(PDO::SQLSRV_ATTR_QUERY_TIMEOUT, $timeout);
150+
} else {
151+
$options = array(PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $timeout);
152+
$stmt = $conn->prepare($query, $options);
153+
}
154+
155+
$result = $stmt->execute();
156+
157+
if ($timeout > 0) {
158+
echo "statementTest $timeout: should have timed out!\n";
159+
}
160+
} catch (PDOException $e) {
161+
if (!fnmatch($error, $e->getMessage())) {
162+
echo "Statement test error expected $timeout, $asAttribute:\n";
163+
var_dump($e->getMessage());
164+
}
165+
}
166+
167+
$t1 = microtime(true);
168+
169+
checkTimeElapsed("statementTest ($timeout, $asAttribute): ", $t0, $t1, $delay);
170+
171+
unset($stmt);
172+
}
173+
174+
try {
175+
$rand = rand(1, 100);
176+
$timeout = $rand % 3;
177+
$asAttribute = $rand % 2;
178+
179+
$conn = connectionTest($timeout, $asAttribute);
180+
testErrors($conn);
181+
unset($conn);
182+
183+
$conn = connectionTest(0, !$asAttribute);
184+
queryTest($conn, $timeout);
185+
186+
for ($i = 0; $i < 2; $i++) {
187+
statementTest($conn, $timeout, $i);
188+
}
189+
unset($conn);
190+
191+
echo "Done\n";
192+
} catch (PdoException $e) {
193+
echo $e->getMessage() . PHP_EOL;
194+
}
195+
196+
?>
197+
--EXPECT--
198+
Done

0 commit comments

Comments
 (0)