diff --git a/lib/Cake/Model/Datasource/Session/DatabaseSession.php b/lib/Cake/Model/Datasource/Session/DatabaseSession.php index 4a1193bea..08c398a7a 100644 --- a/lib/Cake/Model/Datasource/Session/DatabaseSession.php +++ b/lib/Cake/Model/Datasource/Session/DatabaseSession.php @@ -103,6 +103,9 @@ class DatabaseSession implements CakeSessionHandlerInterface { /** * Helper function called on write for database sessions. * + * Will retry, once, if the save triggers a PDOException which + * can happen if a race condition is encountered + * * @param int $id ID that uniquely identifies session in database * @param mixed $data The value of the data to be saved. * @return bool True for successful write, false otherwise. @@ -114,7 +117,17 @@ class DatabaseSession implements CakeSessionHandlerInterface { $expires = time() + $this->_timeout; $record = compact('id', 'data', 'expires'); $record[$this->_model->primaryKey] = $id; - return $this->_model->save($record); + + $options = array( + 'validate' => false, + 'callbacks' => false, + 'counterCache' => false + ); + try { + return $this->_model->save($record, $options); + } catch (PDOException $e) { + return $this->_model->save($record, $options); + } } /** diff --git a/lib/Cake/Test/Case/Model/Datasource/Session/DatabaseSessionTest.php b/lib/Cake/Test/Case/Model/Datasource/Session/DatabaseSessionTest.php index b39a32516..1a4b316f0 100644 --- a/lib/Cake/Test/Case/Model/Datasource/Session/DatabaseSessionTest.php +++ b/lib/Cake/Test/Case/Model/Datasource/Session/DatabaseSessionTest.php @@ -191,4 +191,56 @@ class DatabaseSessionTest extends CakeTestCase { $storage->gc(); $this->assertFalse($storage->read('foo')); } + +/** + * testConcurrentInsert + * + * @return void + */ + public function testConcurrentInsert() { + $this->skipIf( + $this->db instanceof Sqlite, + 'Sqlite does not throw exceptions when attempting to insert a duplicate primary key' + ); + + ClassRegistry::removeObject('Session'); + + $mockedModel = $this->getMockForModel( + 'SessionTestModel', + array('exists'), + array('alias' => 'MockedSessionTestModel', 'table' => 'sessions') + ); + Configure::write('Session.handler.model', 'MockedSessionTestModel'); + + $counter = 0; + // First save + $mockedModel->expects($this->at($counter++)) + ->method('exists') + ->will($this->returnValue(false)); + + // Second save + $mockedModel->expects($this->at($counter++)) + ->method('exists') + ->will($this->returnValue(false)); + + // Second save retry + $mockedModel->expects($this->at($counter++)) + ->method('exists') + ->will($this->returnValue(true)); + + // Datasource exists check + $mockedModel->expects($this->at($counter++)) + ->method('exists') + ->will($this->returnValue(true)); + + $this->storage = new DatabaseSession(); + + $this->storage->write('foo', 'Some value'); + $return = $this->storage->read('foo'); + $this->assertSame('Some value', $return); + + $this->storage->write('foo', 'Some other value'); + $return = $this->storage->read('foo'); + $this->assertSame('Some other value', $return); + } }