diff --git a/lib/dml/sqlsrv_native_moodle_database.php b/lib/dml/sqlsrv_native_moodle_database.php index 30ceb2c8368..6fc935dc046 100644 --- a/lib/dml/sqlsrv_native_moodle_database.php +++ b/lib/dml/sqlsrv_native_moodle_database.php @@ -41,6 +41,8 @@ class sqlsrv_native_moodle_database extends moodle_database { protected $last_error_reporting; // To handle SQL*Server-Native driver default verbosity protected $temptables; // Control existing temptables (sqlsrv_moodle_temptables object) protected $collation; // current DB collation cache + /** @var array list of open recordsets */ + protected $recordsets = array(); /** * Constructor - instantiates the database, specifying if it's external (connect to other systems) or no (Moodle DB) @@ -789,7 +791,20 @@ class sqlsrv_native_moodle_database extends moodle_database { * @return sqlsrv_native_moodle_recordset */ protected function create_recordset($result) { - return new sqlsrv_native_moodle_recordset($result); + $rs = new sqlsrv_native_moodle_recordset($result, $this); + $this->recordsets[] = $rs; + return $rs; + } + + /** + * Do not use outside of recordset class. + * @internal + * @param sqlsrv_native_moodle_recordset $rs + */ + public function recordset_closed(sqlsrv_native_moodle_recordset $rs) { + if ($key = array_search($rs, $this->recordsets, true)) { + unset($this->recordsets[$key]); + } } /** @@ -1367,6 +1382,12 @@ class sqlsrv_native_moodle_database extends moodle_database { * @return void */ protected function begin_transaction() { + // Recordsets do not work well with transactions in SQL Server, + // let's prefetch the recordsets to memory to work around these problems. + foreach ($this->recordsets as $rs) { + $rs->transaction_starts(); + } + $this->query_start('native sqlsrv_begin_transaction', NULL, SQL_QUERY_AUX); $result = sqlsrv_begin_transaction($this->sqlsrv); $this->query_end($result); diff --git a/lib/dml/sqlsrv_native_moodle_recordset.php b/lib/dml/sqlsrv_native_moodle_recordset.php index e397e2e52a2..677c2d07ef7 100644 --- a/lib/dml/sqlsrv_native_moodle_recordset.php +++ b/lib/dml/sqlsrv_native_moodle_recordset.php @@ -31,9 +31,49 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset { protected $rsrc; protected $current; - public function __construct($rsrc) { - $this->rsrc = $rsrc; + /** @var array recordset buffer */ + protected $buffer = null; + + /** @var sqlsrv_native_moodle_database */ + protected $db; + + public function __construct($rsrc, sqlsrv_native_moodle_database $db) { + $this->rsrc = $rsrc; $this->current = $this->fetch_next(); + $this->db = $db; + } + + /** + * Inform existing open recordsets that transaction + * is starting, this works around MARS problem described + * in MDL-37734. + */ + public function transaction_starts() { + if ($this->buffer !== null) { + $this->unregister(); + return; + } + if (!$this->rsrc) { + $this->unregister(); + return; + } + // This might eat memory pretty quickly... + raise_memory_limit('2G'); + $this->buffer = array(); + + while($next = $this->fetch_next()) { + $this->buffer[] = $next; + } + } + + /** + * Unregister recordset from the global list of open recordsets. + */ + private function unregister() { + if ($this->db) { + $this->db->recordset_closed($this); + $this->db = null; + } } public function __destruct() { @@ -47,6 +87,7 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset { if (!$row = sqlsrv_fetch_array($this->rsrc, SQLSRV_FETCH_ASSOC)) { sqlsrv_free_stmt($this->rsrc); $this->rsrc = null; + $this->unregister(); return false; } @@ -69,7 +110,11 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset { } public function next() { - $this->current = $this->fetch_next(); + if ($this->buffer === null) { + $this->current = $this->fetch_next(); + } else { + $this->current = array_shift($this->buffer); + } } public function valid() { @@ -82,5 +127,7 @@ class sqlsrv_native_moodle_recordset extends moodle_recordset { $this->rsrc = null; } $this->current = null; + $this->buffer = null; + $this->unregister(); } }