Problem/Motivation
Contrib/custom database drivers are not in the Drupal\Core\Database namespace, and in this case Log::findCaller will not report the caller correctly, but just the first function/method coming after the first call to the database connection, or worse after a vendor namespaced method if the driver implements methods there.
Incurred into this while experimenting a db driver https://github.com/mondrake/drudbal in conjuction with #2605284: Testing framework does not work with contributed database drivers, and getting failure for the LoggingTest.
See the trace below: in this case the identified 'caller' is always 'query' since it's the first method coming after either not namespaced or not into Drupal\Core\Database namespace.
[0] => Drupal\Core\Database\Log -> findCaller
[1] => Drupal\Core\Database\Log -> log
[2] => Drupal\Core\Database\Statement -> execute
[3] => Doctrine\DBAL\Statement -> execute
[4] => Drupal\Driver\Database\dbal\Connection -> query
[5] => -> db_query
[6] => Drupal\drudbal\Tests\LoggingTest -> testEnableLogging
[7] => Drupal\simpletest\TestBase -> run
[8] => -> _simpletest_batch_operation
[9] => -> _batch_process
[10] => -> _batch_do
[11] => -> _batch_page
[12] => Drupal\system\Controller\BatchController -> batchPage
[13] => -> call_user_func_array
[14] => Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber -> Drupal\Core\EventSubscriber\{closure}
[15] => Drupal\Core\Render\Renderer -> executeInRenderContext
[16] => Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber -> wrapControllerExecutionInRenderContext
[17] => Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber -> Drupal\Core\EventSubscriber\{closure}
[18] => -> call_user_func_array
[19] => Symfony\Component\HttpKernel\HttpKernel -> handleRaw
[20] => Symfony\Component\HttpKernel\HttpKernel -> handle
[21] => Drupal\Core\StackMiddleware\Session -> handle
[22] => Drupal\Core\StackMiddleware\KernelPreHandle -> handle
[23] => Drupal\page_cache\StackMiddleware\PageCache -> pass
[24] => Drupal\page_cache\StackMiddleware\PageCache -> handle
[25] => Drupal\Core\StackMiddleware\ReverseProxyMiddleware -> handle
[26] => Drupal\Core\StackMiddleware\NegotiationMiddleware -> handle
[27] => Stack\StackedHttpKernel -> handle
[28] => Drupal\Core\DrupalKernel -> handle
Proposed resolution
I think we can safely assume in D8 any logged query will always pass by the Connection object. Get its class name, and drop any stack entry with classes that are not the same as the connection class, drop any entry of the connection class, and then process the remaining stack trace as before.
I think we can safely assume in D8 any logged query will always pass through methods in the namespace of the Connection object.
- Get the connection class namespace,
- Drop any stack entry with methods from classes that are not within that namespace,
- Drop any stack entry with methods from classes that are within that namespace,
- then process the remaining stack trace as before.
This will get to:
[0] => -> db_query
[1] => Drupal\drudbal\Tests\LoggingTest -> testEnableLogging
[2] => Drupal\simpletest\TestBase -> run
[3] => -> _simpletest_batch_operation
[4] => -> _batch_process
[5] => -> _batch_do
[6] => -> _batch_page
[7] => Drupal\system\Controller\BatchController -> batchPage
[8] => -> call_user_func_array
[9] => Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber -> Drupal\Core\EventSubscriber\{closure}
[10] => Drupal\Core\Render\Renderer -> executeInRenderContext
[11] => Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber -> wrapControllerExecutionInRenderContext
[12] => Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber -> Drupal\Core\EventSubscriber\{closure}
[13] => -> call_user_func_array
[14] => Symfony\Component\HttpKernel\HttpKernel -> handleRaw
[15] => Symfony\Component\HttpKernel\HttpKernel -> handle
[16] => Drupal\Core\StackMiddleware\Session -> handle
[17] => Drupal\Core\StackMiddleware\KernelPreHandle -> handle
[18] => Drupal\page_cache\StackMiddleware\PageCache -> pass
[19] => Drupal\page_cache\StackMiddleware\PageCache -> handle
[20] => Drupal\Core\StackMiddleware\ReverseProxyMiddleware -> handle
[21] => Drupal\Core\StackMiddleware\NegotiationMiddleware -> handle
[22] => Stack\StackedHttpKernel -> handle
[23] => Drupal\Core\DrupalKernel -> handle
Remaining tasks
Review patch.
User interface changes
None
API changes
None
Data model changes
None
| Comment | File | Size | Author |
|---|---|---|---|
| #92 | 2867788-92.patch | 14.31 KB | mrinalini9 |
| #89 | 2867788-89.patch | 2.13 KB | mrinalini9 |
| #79 | 2867788-79.patch | 14.31 KB | mondrake |
| #79 | interdiff_77-79.txt | 527 bytes | mondrake |
| #77 | 2867788-77.patch | 14.33 KB | mondrake |
Comments
Comment #2
mondrakeComment #3
mondrakeComment #5
mondrakeFailure in #4 is a DrupalCI failure.
Comment #6
dawehnerI want to support you doing your research. Here is for example a test for your fix, even it doesn't run yet. Do you have an idea by any change?
Comment #8
mondrakeThanks a lot @dawehner!
Let's see this, I do not think we need a separate test class here, LoggingTest is there already. The test passes locally with the contrib driver on, but should fail on d.o. because the debug trace data hardcodes the contrib driver's Connection class, whereas on d.o. it will be searching for mysql/pgsql/sqlite core's Connection classes. Maybe we can just check the current active connection class before running the test and just update the sample in the array?
Comment #11
mondrakeSo #6 and #8 were aborting because of infinite loops while removing from the $stack array. Fixed here, and fixed my point in #8 by implementing a protected method to return the connection's class name, and stubbing it in the test.
Comment #12
mondrakeUsing a mocked Log class in the test, we can also introduce a protected method to get the debug backtrace and stub it in the test, thus avoiding to change the signature of the findCaller method (I can't see other use cases for passing a different backtrace to it, other than testing). Also added a dataprovider to the test to test the (unfathomable) case of no methods from the Connection class being present in the trace (which caused CI errors in #6 an #8). Finally, small adjustment to findCaller docs to make it up-to-date.
Comment #13
mondrakeMy original assumption
is incorrect. Sqlite fails because the core Database opens a Statement class directly, without passing through the Connection class. So here I am 'widening' the approach to check for methods in the Connection class namespace instead.
Comment #14
dawehner@mondrake
Good point. Its certainly worth trying out all database engines.
Can't you combine those two while statements into one using
in_array(, , TRUE)?Comment #15
mondrake@dawehner thanks. Actually I think we can just incorporate the logic in the main
forloop, and avoidarray_shiftcalls.Comment #16
larowlanShould we statically cache this as a property on the class? Reflection isn't cheap - how frequently will findCaller be called? Does the namespace ever change?
In my opinion this whole chunk would be more readable if it used
instead of
forI realise its an existing issue, but if we're touching it, we should leave it in a happier place
There is some ugmo existing code here. Can we touch it up while we're here?
would move three lines to one.
why not
why not
this is definitely a boolean
nice work
nit: personally I prefer named test cases (use names in array keys instead of integers).
The need for comments to describe the cases, illustrates that they would be useful.
Comment #17
mondrakeThanks @larowlan!
#16:
1. Better not use Reflection at all, I am adding a getter to the Connection class instead.
2. and 3. Fully agree but I am quite sure if we change here it will be pushed back. How about a follow-up, or extending the scope of #2088271: Undefined index: args in DatabaseLog->findCaller()?
4. and 5. OK done
6. :)
7. OK done
Comment #18
mondrake__NAMESPACE__ constant is inherited by extending classes still with the abstract class namespace, contrib will fail. Here making getNamespace abstract in the base Connection, and implementing in each driver.
Comment #20
mondrakeAdding an abstract method to the Connection class is probably controversial. Just doing sth simpler.
Comment #22
mondrakeSorry for the noise.
So here instead of using Reflection to get the namespace of the driver's Connection class, which is expensive as pointed out by @larowlan in #15.1, we get the class name through
get_class, and just skip the part of the name that comes after the last backslash (included). Please comment if it still makes sense to cache it.Comment #23
larowlanLooking good to me, can you comment about the cleanup in #2088271: Undefined index: args in DatabaseLog->findCaller()?
Comment #24
mondrake@larowlan re
That one is about ensuring an 'args' key is defined for the stack trace entry, very much like #16.1 is about ensuring that a 'class' key exists. We could just have a protected method to
array_mergea set of default keys into the actual stack trace entry. In that issue we are missing a test, though; with the test added in this patch we could then also add other sample backtrace arrays with the case of missing 'class' and 'args'.Comment #25
mondrakeRe #22, actually there is no purpose even to use
get_class: the namespace is always specified in the connection options and we can retrieve it viaConnection::getConnectionOptions()['namespace'].Comment #27
mondrakeActually no, sorry - the 'namespace' connection key can be missing with core drivers. So #22 still stands as the patch to review, unless we want to replicate here the piece of code from
Database::openConnectionthat resolves the driver classbut I do not think it makes sense here.
Reuploading patch in #22 to be clean.
Comment #29
mondrakeTrying again along the lines of #25. This time we are resolving the namespace in
Database::openConnectionin case it's missing, and passing it over to the connection options when instatiating the connection. This patch collides with #2605284: Testing framework does not work with contributed database drivers, though.Comment #32
mondrakeComment #33
mondrakePostponing on #2605284: Testing framework does not work with contributed database drivers that has a different way to get the Connection's namespace.
Comment #34
mondrakeUnpostponing after commit of #2605284: Testing framework does not work with contributed database drivers.
Comment #35
daffie commentedComment #36
mondrakeRerolled + adjustments after #2605284: Testing framework does not work with contributed database drivers went in + moved the test debug_bactrace stack to the test dataprovider.
Comment #38
mondrakeArgh I did not realize that in the end in #2605284: Testing framework does not work with contributed database drivers we set
Database::getDatabaseDriverNamespacevisibility to protected.I think it's better to change it to public rather then duplicating code or using reflection here.
Comment #40
mondrakeDatabase::getConnectionInforeturns an array with multiple targets so we take the default one.Comment #41
mondrakeLet's reduce unnecessary calls - here the namespace will be only determined once.
Comment #43
mondrakeFix test
Comment #44
daffie commentedThe patch looks good, but I do have some remarks:
Changing this method from protected to public does warrend the creation of a change record.
I think that moving the calling of
debug_backtrace()to a separate method makes things only more complicated and unnecessary. If you do not agree, please explain why you think so.Can you add some documentation about why/what this variable is for/what it does.
Why do you compare the result of
strpos()with=== 0and not with=== FALSE?Why do you add the part
$i < $stack_count - 2? It is properly necessary, but I am missing the point. Sorry.Comment #45
mondrakeThanks @daffie
Replies to #44.2/3/4/5 can be found in the comments in this patch.
#44.1 I haven't tackled yet, let's see feedback from maintainers if change of visibility is OK or not.
Comment #46
daffie commentedJust testing if a simpler solution would also work.
Comment #48
daffie commentedPlease do not see my following remarks as personal. Your work @mondrake on this and other issues is great. with that said:
I get bad feeling of codesmell when I look at the above code. I find it diffecult to read and to really see want is happening.
I am unable to test this code. Therefor am I asking you @mondrake if it is possible to change it to something like:
If there is no other solution I will give your patch RTBC.
Comment #49
mondrake@daffie I never take feedback in issue queues as personal... and there are no such things as stupid questions :)
Thanks for your input. I agree that's difficult to read. I was thinking to change the approach and instead of looping backwards through the debug stack, start from the very last entry (which is the first call made), and stop when you find a db_* function or a method from the Database or database driver namespace. That should be more easily readable.
The db_* functions are wrappers to methods in classes of the Database or database driver namespace, regardless if the driver is core or contrib.
I do not have time right now to look at it, will do later if noone beats me at it :)
Comment #51
mondrakeThis does #49 and should be more readable.
Comment #53
mondrakeReroll.
Comment #55
bohart+1 to RTBC.
The latest patch #53 was tested and added to Oracle Database Driver into installation instruction as a required one for proper work.
Related issue: #3107885: Fix Database\LoggingTest
Comment #56
daffie commented@bohart: Thank you for testing the patch with your Oracle database driver.
The patch loks good to me. All my remarks are addressed.
The patch works for the Oacle database driver.
There are tests added.
The readability of the code from the findCaller method has been improved.
For me it is RTBC.
Comment #57
mondrakeNeeds a reroll after commit of #3109097: Drupal 9 does not handle db_* functions any more, and moving to 9.0.x since
db_functions are no longer to be supported. If it will have to be backported to 8.x, the patch should include thedb_check again.Comment #58
mondrakeTaking the opportunity to use the null coalesce operator in PHP 7+. I believe we need a new sample backtrace in the test, the current one is still representing a call to
db_query.Interdiff is complicated since it's intertwined wit the reroll.
Comment #59
daffie commented@mondrake: I think that something went wrong with your interdiff file.
Comment #60
mondrake@daffie yes, it's difficult to get a clean interdiff when rerolling. Better look at the patch itself.
Comment #62
mondrakeHere's with a re-sampled backtrace from running
LoggingTest::testEnableLoggingon a contrib driver, this time with no longer adb_querycall in the stack.Comment #63
daffie commentedThe db_query parts have been removed.
It all looks good again.
Back to RTBC.
Comment #64
daffie commentedComment #65
beakerboyI'd be happy to test this on 8.8.x with SQL Server. I need to set up a travis-ci environment for 8.9.x and 9.0.x. #53 worked for me on 8.8.x another RTBC.
Comment #67
mondrakerandom test failure
Comment #68
alexpottSo this change is interesting. Here's the code of the method:
I think in Drupal 9 this method is not even necessary. I've opened #3112476: Always set $info['namespace'] on database connection info. I'm not sure about making a method public that's not really necessary.
Comment #69
mondrake@alexpott #3112476: Always set $info['namespace'] on database connection info would defer all this to 9.1+, whereas this issue is for 9.0 with potential backport to 8.x - to allow contrib drivers to be compatible with these versions.
Comment #70
alexpott@mondrake i don't think this really increases compatibility or not. Also looking at the code we can do this without affecting the public API.
This could be done this...
Note that exceptions will be thrown in the connection key and default target don't exist which imo is the correct behaviour because we can't log on a non-existent connection.
Comment #71
mondrake#70: @larowlan had some concerns on using reflection back in #16, but I think this can work. IMHO, using reflection in runtime code means something is wrong with the public API, but OK, when #3112476: Always set $info['namespace'] on database connection info this all may become unnecessary.
Comment #72
alexpottWell this is in-between runtime and debug code. It's only on during views ui query capture in core. Which is not the 99.999999% use of core db code.
Comment #73
daffie commentedThe changes look good to me and it addresses the points made by @alexpott.
Back to RTBC.
Comment #74
beakerboy#71 does not apply to 8.8.x. If someone could backport the patch, I can test on my sqlsrv github repo.
Comment #75
alexpottCommitted 97bac4f and pushed to 9.0.x. Thanks!
If we want to fix this in Drupal 8.x then we need a new patch for 8.9.x
Comment #77
mondrakeHere's a patch for D8.9. Rerolled #71, and added back checks for
db_*functions and a test for that, taking from #53.Comment #79
mondrakeComment #80
mondrakeComment #81
beakerboyWorks for sql server#79 Works for SQL Server...I have reviewed the code and it meets my expectations and appears to be well formatted. I have installed the patch on my testing environment and testing failures now pass.
Comment #82
daffie commented@Beakerboy: Thank you for reviewing the patch from this issue. Because of your short comment: "Works for sql server", I was wondering if you followed the reviewing guidelines set in https://www.drupal.org/patch/review? Let me point out that I am myself not always following those guidelines as good as should.
Comment #83
alexpottI think we need to do #3112476: Always set $info['namespace'] on database connection info first. If we can get the default namespace setting in $info back in 8.x then we don't need the additional protected methods for finding a driver namespace or mocking out debug_backtrace() - which leads to less API via inheritance to support. Which is a good thing - especially as our test coverage involves mocking out this API.
Comment #84
beakerboy@alexpott, #3112476: Always set $info['namespace'] on database connection info has been RTBC for 3 weeks and it is holding up this issue. Do you have any comments?
Comment #85
beakerboy@mondrake, Patch #71 needs to be rerolled
Comment #86
mondrakeBlocker is in now
Comment #87
mondrakeActually, dunno... more than a reroll here we need a totally new patch if we want to port this to D8. Maybe we could close this as a backport and open a D8 issue only.
Comment #89
mrinalini9 commentedRerolled patch for 9.1.x, please review.
Comment #90
mondrake@mrinalini9 this is already committed to D9. Needs a patch for D8.
Comment #91
mrinalini9 commentedComment #92
mrinalini9 commentedHi @mondrake, I have rerolled patch for 8.8.x, please review.
Comment #93
sokru commentedComment #95
beakerboy@mrinalini9 Is your patch any different than #79? They both fix the bug, so I don't see why one of them cannot be used.
Comment #96
catchRe-titling to make it clear this is about the 8.9 backport. What is the difference between the #95 and #79 patches?
Comment #97
mondrakeI am afraid this backport has stalled. I'm marking the issue fixed not to drag it forward. I'd rather suggest opening a backport-specific issue it this has to be a thing.