Merge pull request #23047 from nextcloud/techdebt/noid/warn-on-database-abuse

Log the number of queries built and executed
This commit is contained in:
Roeland Jago Douma 2020-10-04 10:07:49 +02:00 committed by GitHub
commit f4707d178e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 12 deletions

View File

@ -61,6 +61,7 @@ use OCP\Files\Folder;
use OCP\Files\IAppData; use OCP\Files\IAppData;
use OCP\Group\ISubAdmin; use OCP\Group\ISubAdmin;
use OCP\IConfig; use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IInitialStateService; use OCP\IInitialStateService;
use OCP\IL10N; use OCP\IL10N;
use OCP\ILogger; use OCP\ILogger;
@ -180,7 +181,10 @@ class DIContainer extends SimpleContainer implements IAppContainer {
$c->get('Protocol'), $c->get('Protocol'),
$c->get(MiddlewareDispatcher::class), $c->get(MiddlewareDispatcher::class),
$c->get(IControllerMethodReflector::class), $c->get(IControllerMethodReflector::class),
$c->get(IRequest::class) $c->get(IRequest::class),
$c->get(IConfig::class),
$c->get(IDBConnection::class),
$c->get(LoggerInterface::class)
); );
}); });

View File

@ -35,11 +35,14 @@ namespace OC\AppFramework\Http;
use OC\AppFramework\Http; use OC\AppFramework\Http;
use OC\AppFramework\Middleware\MiddlewareDispatcher; use OC\AppFramework\Middleware\MiddlewareDispatcher;
use OC\AppFramework\Utility\ControllerMethodReflector; use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\DB\Connection;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\Response;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IRequest; use OCP\IRequest;
use Psr\Log\LoggerInterface;
/** /**
* Class to dispatch the request to the middleware dispatcher * Class to dispatch the request to the middleware dispatcher
@ -58,6 +61,15 @@ class Dispatcher {
/** @var IRequest */ /** @var IRequest */
private $request; private $request;
/** @var IConfig */
private $config;
/** @var IDBConnection|Connection */
private $connection;
/** @var LoggerInterface */
private $logger;
/** /**
* @param Http $protocol the http protocol with contains all status headers * @param Http $protocol the http protocol with contains all status headers
* @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which * @param MiddlewareDispatcher $middlewareDispatcher the dispatcher which
@ -65,15 +77,24 @@ class Dispatcher {
* @param ControllerMethodReflector $reflector the reflector that is used to inject * @param ControllerMethodReflector $reflector the reflector that is used to inject
* the arguments for the controller * the arguments for the controller
* @param IRequest $request the incoming request * @param IRequest $request the incoming request
* @param IConfig $config
* @param IDBConnection $connection
* @param LoggerInterface $logger
*/ */
public function __construct(Http $protocol, public function __construct(Http $protocol,
MiddlewareDispatcher $middlewareDispatcher, MiddlewareDispatcher $middlewareDispatcher,
ControllerMethodReflector $reflector, ControllerMethodReflector $reflector,
IRequest $request) { IRequest $request,
IConfig $config,
IDBConnection $connection,
LoggerInterface $logger) {
$this->protocol = $protocol; $this->protocol = $protocol;
$this->middlewareDispatcher = $middlewareDispatcher; $this->middlewareDispatcher = $middlewareDispatcher;
$this->reflector = $reflector; $this->reflector = $reflector;
$this->request = $request; $this->request = $request;
$this->config = $config;
$this->connection = $connection;
$this->logger = $logger;
} }
@ -97,8 +118,36 @@ class Dispatcher {
$this->middlewareDispatcher->beforeController($controller, $this->middlewareDispatcher->beforeController($controller,
$methodName); $methodName);
$databaseStatsBefore = [];
if ($this->config->getSystemValueBool('debug', false)) {
$databaseStatsBefore = $this->connection->getStats();
}
$response = $this->executeController($controller, $methodName); $response = $this->executeController($controller, $methodName);
if (!empty($databaseStatsBefore)) {
$databaseStatsAfter = $this->connection->getStats();
$numBuilt = $databaseStatsAfter['built'] - $databaseStatsBefore['built'];
$numExecuted = $databaseStatsAfter['executed'] - $databaseStatsBefore['executed'];
if ($numBuilt > 50) {
$this->logger->debug('Controller {class}::{method} created {count} QueryBuilder objects, please check if they are created inside a loop by accident.' , [
'class' => (string) get_class($controller),
'method' => $methodName,
'count' => $numBuilt,
]);
}
if ($numExecuted > 100) {
$this->logger->warning('Controller {class}::{method} executed {count} queries.' , [
'class' => (string) get_class($controller),
'method' => $methodName,
'count' => $numExecuted,
]);
}
}
// if an exception appears, the middleware checks if it can handle the // if an exception appears, the middleware checks if it can handle the
// exception and creates a response. If no response is created, it is // exception and creates a response. If no response is created, it is
// assumed that theres no middleware who can handle it and the error is // assumed that theres no middleware who can handle it and the error is

View File

@ -59,6 +59,12 @@ class Connection extends ReconnectWrapper implements IDBConnection {
protected $lockedTable = null; protected $lockedTable = null;
/** @var int */
protected $queriesBuilt = 0;
/** @var int */
protected $queriesExecuted = 0;
public function connect() { public function connect() {
try { try {
return parent::connect(); return parent::connect();
@ -68,12 +74,20 @@ class Connection extends ReconnectWrapper implements IDBConnection {
} }
} }
public function getStats(): array {
return [
'built' => $this->queriesBuilt,
'executed' => $this->queriesExecuted,
];
}
/** /**
* Returns a QueryBuilder for the connection. * Returns a QueryBuilder for the connection.
* *
* @return \OCP\DB\QueryBuilder\IQueryBuilder * @return \OCP\DB\QueryBuilder\IQueryBuilder
*/ */
public function getQueryBuilder() { public function getQueryBuilder() {
$this->queriesBuilt++;
return new QueryBuilder( return new QueryBuilder(
$this, $this,
\OC::$server->getSystemConfig(), \OC::$server->getSystemConfig(),
@ -90,6 +104,7 @@ class Connection extends ReconnectWrapper implements IDBConnection {
public function createQueryBuilder() { public function createQueryBuilder() {
$backtrace = $this->getCallerBacktrace(); $backtrace = $this->getCallerBacktrace();
\OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]); \OC::$server->getLogger()->debug('Doctrine QueryBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
$this->queriesBuilt++;
return parent::createQueryBuilder(); return parent::createQueryBuilder();
} }
@ -102,6 +117,7 @@ class Connection extends ReconnectWrapper implements IDBConnection {
public function getExpressionBuilder() { public function getExpressionBuilder() {
$backtrace = $this->getCallerBacktrace(); $backtrace = $this->getCallerBacktrace();
\OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]); \OC::$server->getLogger()->debug('Doctrine ExpressionBuilder retrieved in {backtrace}', ['app' => 'core', 'backtrace' => $backtrace]);
$this->queriesBuilt++;
return parent::getExpressionBuilder(); return parent::getExpressionBuilder();
} }
@ -191,6 +207,7 @@ class Connection extends ReconnectWrapper implements IDBConnection {
public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null) { public function executeQuery($query, array $params = [], $types = [], QueryCacheProfile $qcp = null) {
$query = $this->replaceTablePrefix($query); $query = $this->replaceTablePrefix($query);
$query = $this->adapter->fixupStatement($query); $query = $this->adapter->fixupStatement($query);
$this->queriesExecuted++;
return parent::executeQuery($query, $params, $types, $qcp); return parent::executeQuery($query, $params, $types, $qcp);
} }
@ -211,6 +228,7 @@ class Connection extends ReconnectWrapper implements IDBConnection {
public function executeUpdate($query, array $params = [], array $types = []) { public function executeUpdate($query, array $params = [], array $types = []) {
$query = $this->replaceTablePrefix($query); $query = $this->replaceTablePrefix($query);
$query = $this->adapter->fixupStatement($query); $query = $this->adapter->fixupStatement($query);
$this->queriesExecuted++;
return parent::executeUpdate($query, $params, $types); return parent::executeUpdate($query, $params, $types);
} }

View File

@ -33,6 +33,9 @@ use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\Response;
use OCP\IConfig; use OCP\IConfig;
use OCP\IRequest;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class TestController extends Controller { class TestController extends Controller {
/** /**
@ -72,7 +75,12 @@ class TestController extends Controller {
} }
} }
/**
* Class DispatcherTest
*
* @package Test\AppFramework\Http
* @group DB
*/
class DispatcherTest extends \Test\TestCase { class DispatcherTest extends \Test\TestCase {
/** @var MiddlewareDispatcher */ /** @var MiddlewareDispatcher */
private $middlewareDispatcher; private $middlewareDispatcher;
@ -80,16 +88,24 @@ class DispatcherTest extends \Test\TestCase {
private $dispatcher; private $dispatcher;
private $controllerMethod; private $controllerMethod;
private $response; private $response;
/** @var IRequest|MockObject */
private $request; private $request;
private $lastModified; private $lastModified;
private $etag; private $etag;
/** @var Http|MockObject */
private $http; private $http;
private $reflector; private $reflector;
/** @var IConfig|MockObject */
private $config;
/** @var LoggerInterface|MockObject */
private $logger;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
$this->controllerMethod = 'test'; $this->controllerMethod = 'test';
$this->config = $this->createMock(IConfig::class);
$this->logger = $this->createMock(LoggerInterface::class);
$app = $this->getMockBuilder( $app = $this->getMockBuilder(
'OC\AppFramework\DependencyInjection\DIContainer') 'OC\AppFramework\DependencyInjection\DIContainer')
->disableOriginalConstructor() ->disableOriginalConstructor()
@ -99,7 +115,7 @@ class DispatcherTest extends \Test\TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$this->http = $this->getMockBuilder( $this->http = $this->getMockBuilder(
'\OC\AppFramework\Http') \OC\AppFramework\Http::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
@ -124,7 +140,10 @@ class DispatcherTest extends \Test\TestCase {
$this->http, $this->http,
$this->middlewareDispatcher, $this->middlewareDispatcher,
$this->reflector, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$this->response = $this->createMock(Response::class); $this->response = $this->createMock(Response::class);
@ -299,7 +318,10 @@ class DispatcherTest extends \Test\TestCase {
); );
$this->dispatcher = new Dispatcher( $this->dispatcher = new Dispatcher(
$this->http, $this->middlewareDispatcher, $this->reflector, $this->http, $this->middlewareDispatcher, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$controller = new TestController('app', $this->request); $controller = new TestController('app', $this->request);
@ -330,7 +352,10 @@ class DispatcherTest extends \Test\TestCase {
); );
$this->dispatcher = new Dispatcher( $this->dispatcher = new Dispatcher(
$this->http, $this->middlewareDispatcher, $this->reflector, $this->http, $this->middlewareDispatcher, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$controller = new TestController('app', $this->request); $controller = new TestController('app', $this->request);
@ -364,7 +389,10 @@ class DispatcherTest extends \Test\TestCase {
); );
$this->dispatcher = new Dispatcher( $this->dispatcher = new Dispatcher(
$this->http, $this->middlewareDispatcher, $this->reflector, $this->http, $this->middlewareDispatcher, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$controller = new TestController('app', $this->request); $controller = new TestController('app', $this->request);
@ -397,7 +425,10 @@ class DispatcherTest extends \Test\TestCase {
); );
$this->dispatcher = new Dispatcher( $this->dispatcher = new Dispatcher(
$this->http, $this->middlewareDispatcher, $this->reflector, $this->http, $this->middlewareDispatcher, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$controller = new TestController('app', $this->request); $controller = new TestController('app', $this->request);
@ -431,7 +462,10 @@ class DispatcherTest extends \Test\TestCase {
); );
$this->dispatcher = new Dispatcher( $this->dispatcher = new Dispatcher(
$this->http, $this->middlewareDispatcher, $this->reflector, $this->http, $this->middlewareDispatcher, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$controller = new TestController('app', $this->request); $controller = new TestController('app', $this->request);
@ -467,7 +501,10 @@ class DispatcherTest extends \Test\TestCase {
); );
$this->dispatcher = new Dispatcher( $this->dispatcher = new Dispatcher(
$this->http, $this->middlewareDispatcher, $this->reflector, $this->http, $this->middlewareDispatcher, $this->reflector,
$this->request $this->request,
$this->config,
\OC::$server->getDatabaseConnection(),
$this->logger
); );
$controller = new TestController('app', $this->request); $controller = new TestController('app', $this->request);