Skip to content

Add reuse_prototype field to /wiki API endpoint#1082

Open
outdooracorn wants to merge 14 commits into
mainfrom
ollie/reuse-prototype
Open

Add reuse_prototype field to /wiki API endpoint#1082
outdooracorn wants to merge 14 commits into
mainfrom
ollie/reuse-prototype

Conversation

@outdooracorn
Copy link
Copy Markdown
Member

When ->with('wikiLatestProfile') is used in the PublicWikiController this change only results in 1 extra database query per API request.

Bug: T421877

@outdooracorn outdooracorn self-assigned this Apr 11, 2026
@ranyardm-wmde
Copy link
Copy Markdown
Contributor

ranyardm-wmde commented Apr 13, 2026

IMO given we don't have enough monitoring to predict how this will affect database load, this should ideally not be the accepted solution as it will create the multiple queries per search problem we discussed in ticket breakdown. Apparently I'm wrong here.

@dati18 dati18 force-pushed the ollie/reuse-prototype branch from 7c192cd to 148925e Compare May 22, 2026 10:43
@dati18 dati18 marked this pull request as ready for review May 22, 2026 10:48
Comment thread app/Providers/AppServiceProvider.php Outdated
@dati18 dati18 force-pushed the ollie/reuse-prototype branch from 1e2b090 to 8ac9b3d Compare May 22, 2026 15:12
@dati18 dati18 changed the title PoC: Add reuse_prototype field to /wiki API endpoint Add reuse_prototype field to /wiki API endpoint May 26, 2026
rosalieper
rosalieper previously approved these changes May 27, 2026
Copy link
Copy Markdown
Contributor

@rosalieper rosalieper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am please with how this prototype did not turnout as a bigger change

Comment thread app/Providers/AppServiceProvider.php Outdated
rosalieper
rosalieper previously approved these changes May 27, 2026
@outdooracorn outdooracorn dismissed rosalieper’s stale review May 27, 2026 15:18

I can't add a "request changes" review as I created the PR. Removing this approval instead. Will create my review comment now.

Copy link
Copy Markdown
Member Author

@outdooracorn outdooracorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the task's Task Breakdown section:

  • We would like to provide an alternative API endpoint so that the Suite team don't rely on an internal API endpoint that we could change at any point. We agreed to use /reusePrototype which points at the same PublicWikiController as the internal /wiki endpoint. See routes/api.php#L59.
    • If this turns out to be more than copy and editing routes/api.php#L59 - 2hrs of work to write and review, then reconsider

I don't see any changes to routes/api.php or a comment saying that this was attempted but would take too long to implement.

@dati18 you were in the task breakdown meeting where we discussed this. How come it's not included in this PR?

@rosalieper remember, as the reviewer, it's important to read through the task so you know what the PR should contain.

Comment on lines +20 to +22
// Checking relation load state before reading it to avoid N+1 query
// This relies on the controller to eager load `wikiLatestProfile` relationship
'reuse_prototype' => $this->relationLoaded('wikiLatestProfile')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to think about to avoid accidentally creating an N+1 query issue in the future. I don't think it requires us to check this every time the toArray() method is called, though. Can we check that PublicWikiController eager loads the relationship in a test instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to create a PublicWikiControllerTest unit test for this check. The current PublicWikiTest looks more like an e2e test which is not where I would expect this type of check to go. My hunch is that it will be easier to test in a unit test as well.

Copy link
Copy Markdown
Contributor

@dati18 dati18 May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow stashed the change and didn't notice. I also made a change to show() to make sure wikiLatestProfile was eager-loaded...
This is a stupid fault. I apologize. :(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a unit test for the relation check

Copy link
Copy Markdown
Contributor

@dati18 dati18 May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

based on your comment, I did the following:

  • added the endpoint /reusePrototype and also made sure show() eager-loads 'wikiLatestProfile'
  • added PublicWikiControllerTest unit test
  • modified the reusePrototypeTest by changing the endpoint to reusePrototype instead of generic this->route, also extended it to also call GET /reusePrototype/id

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can create a successful test to check for eager loading, then we can remove this check here.

@dati18 dati18 force-pushed the ollie/reuse-prototype branch from 0d903dc to 976f2d9 Compare May 27, 2026 17:16
@rosalieper
Copy link
Copy Markdown
Contributor

rosalieper commented May 27, 2026

@rosalieper remember, as the reviewer, it's important to read through the task so you know what the PR should contain.

@outdooracorn Thank you for your comment, I read through the task description twice actually because it has a long description with updates and followup comment. Thanks for catching this. It left my mind at some point during the review but I will remember this incident. :)

*/
public function show($id) {
return new PublicWikiResource(Wiki::findOrFail($id));
return new PublicWikiResource(Wiki::query()->with('wikiLatestProfile')->findOrFail($id));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as $query in index(), I think we can benefit from eager loading here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes you think that? Did you count the number of DB queries like I did in https://phabricator.wikimedia.org/T421877#11810168?

I'm wondering that as we are only getting a single Wiki, rather than a list of Wikis, if the N+1 issue won't appear.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do get this error when I checkout the PR, composer install, rebuild the container and then visit

Can you pasted the command? I just run composer install and didn't see similar error. I tore down everything and rebuilt/reinstalled also...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering that as we are only getting a single Wiki, rather than a list of Wikis, if the N+1 issue won't appear.

Ah you're right, it got me right there... since show() returns a single wiki, worst-case scenario is wikiLatestProfile was lazy-loaded and we do 1 extra query.

So I have 2 options:

  • Keep it for the sake of explicitness/consistency and avoid hidden lazy loading.
  • Reverse it back because its impact is very much minimal

What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rosalieper your opinion is also valuable as well :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vote is to remove it from this PR as:

  • it is not required
  • it is not related to the task (T421877)

It's good to keep PRs small and to the point where possible.

Comment thread tests/Http/Controllers/PublicWikiControllerTest.php Outdated
@dati18 dati18 requested a review from rosalieper May 29, 2026 07:56
Copy link
Copy Markdown
Member Author

@outdooracorn outdooracorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't have enough time to provide a full review 🥲

I do get this error when I checkout the PR, composer install, rebuild the container and then visit http://localhost:8082/wiki?sort=pages&direction=desc&page=1&per_page=10:

public/index.php:55
The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied The exception occurred while attempting to log: Query Executed: Context: {"sql":"select count(*) as aggregate from `wikis` where `wikis`.`deleted_at` is null","bindings":[],"connection":"mysql"} Context: {"exception":{}} Context: {"exception":{}} Context: {"exception":{}} Context: {"exception":{}} Context: {"exception":{}} 

This doesn't happen when I checkout origin/main, composer install, rebuild the container, and then visit http://localhost:8082/wiki?sort=pages&direction=desc&page=1&per_page=10. So there is likely something for you to look into here. Hopefully I haven't done anything silly locally! 🙈

*/
public function show($id) {
return new PublicWikiResource(Wiki::findOrFail($id));
return new PublicWikiResource(Wiki::query()->with('wikiLatestProfile')->findOrFail($id));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What makes you think that? Did you count the number of DB queries like I did in https://phabricator.wikimedia.org/T421877#11810168?

I'm wondering that as we are only getting a single Wiki, rather than a list of Wikis, if the N+1 issue won't appear.

Copy link
Copy Markdown
Member Author

@outdooracorn outdooracorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested again locally and couldn't reproduce my previous error, so not sure what the issue was there. Sorry for the noise.

class PublicWikiControllerTest extends TestCase {
use DatabaseTransactions;

public function testShowEagerLoadsWikiLatestProfileForResource(): void {
Copy link
Copy Markdown
Member Author

@outdooracorn outdooracorn Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes even if wikiLatestProfile isn't eager loaded.

The PublicWikiResource will lazy load the model if it hasn't already been loaded so $this->assertTrue($resource->resource->relationLoaded('wikiLatestProfile')); will always be true.

Looking at PublicWikiController a bit closer, it might be harder than I initially thought to test this.

EDIT: the above is incorrect.

You mentioned in another comment about creating a test queries counter and ensure 'wiki_profiles' is hit only once. Is this something that Laravel provides helpers for? This might be useful if not too difficult to implement.

Copy link
Copy Markdown
Contributor

@dati18 dati18 Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure?
If I change show() to lazy load the test fails... :(

public function show($id) {
        return new PublicWikiResource(Wiki::query()->findOrFail($id));
}
public function show($id) {
        return new PublicWikiResource(Wiki::findOrFail($id));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@outdooracorn can you test it again?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! I see what happened here, I removed the eager loading from the index() method as that is the method that requires eager loading to avoid N+1 issues. I hadn't noticed you wrote this test only for the show() method. Why aren't testing the index() method?

Copy link
Copy Markdown
Contributor

@dati18 dati18 Jun 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes... index() returns a collection of wikis so it's worth testing than show()
I just added a test for index() by by generating a random n number of wikis (from 3 to 100) and checking for the number of queries (it should be 1) and making sure wikiLatestProfile is eager-loaded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fun read about relation loaded and stuff

https://laravel-news.com/relationloaded

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool that you have figured out how to count the SQL queries executed.

However, there are some things I don't like about this new test:

  • Tests should be deterministic - given the same code and environment, they should behave the same way every time they run. Creating a random number of Wikis is not deterministic. I would create 3 - 5 wikis for this test.
  • Tests should not echo or otherwise print output. Instead, they should communicate results through assertions, exceptions, and PHPUnit's output mechanisms

Comment on lines +20 to +22
// Checking relation load state before reading it to avoid N+1 query
// This relies on the controller to eager load `wikiLatestProfile` relationship
'reuse_prototype' => $this->relationLoaded('wikiLatestProfile')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can create a successful test to check for eager loading, then we can remove this check here.

@dati18 dati18 force-pushed the ollie/reuse-prototype branch from a714fa9 to e9c7a31 Compare June 3, 2026 10:01
@dati18 dati18 force-pushed the ollie/reuse-prototype branch from e9c7a31 to bdd52e8 Compare June 3, 2026 11:55
Comment on lines +31 to +39
if ($this->app->environment('local')) {
\Event::listen(QueryExecuted::class, function (QueryExecuted $query) {
\Log::debug('Query Executed: ', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'connection' => $query->connectionName,
]);
});
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream or file "/var/www/html/storage/logs/laravel-2026-05-29.log" could not be opened in append mode: Failed to open stream: Permission denied

I got the above error again and tracked it down to:

  • some of the logs in /var/www/html/storage/logs/ were owned by root:root rather than www-data:www-data
  • this event listening doing a log

I don't think it's an issue in staging/production as I don't think logging to a file is enabled.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I just checked and had the same issue. But you've already specified "local"-only so I don't think it's going to be a problem on staging/prod

@dati18 dati18 force-pushed the ollie/reuse-prototype branch from 6d415c7 to 0657731 Compare June 3, 2026 21:25
Copy link
Copy Markdown
Member Author

@outdooracorn outdooracorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As requested by @dati18, not a full review. Mostly comments around the PublicWikiControllerTest.

});

$controller = new PublicWikiController;
$resourceCollection = $controller->index(request());
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request() method this for getting "an instance of the current request or an input item from the request" which does feel quite right to use here. I'd prefer to create an empty Request instance instead.

Suggested change
$resourceCollection = $controller->index(request());
$resourceCollection = $controller->index(new Request());

$resourceCollection = $controller->index(request());

$this->assertSame(1, $profileQueryCount);
$this->assertTrue($resourceCollection->collection[0]->resource->relationLoaded('wikiLatestProfile'));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional nitpick: if you want, you can use first() instead of collection[0] and ->resource isn't required.

Suggested change
$this->assertTrue($resourceCollection->collection[0]->resource->relationLoaded('wikiLatestProfile'));
$this->assertTrue($resourceCollection->first()->relationLoaded('wikiLatestProfile'));

class PublicWikiControllerTest extends TestCase {
use DatabaseTransactions;

public function testShowEagerLoadsWikiLatestProfileForResource(): void {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool that you have figured out how to count the SQL queries executed.

However, there are some things I don't like about this new test:

  • Tests should be deterministic - given the same code and environment, they should behave the same way every time they run. Creating a random number of Wikis is not deterministic. I would create 3 - 5 wikis for this test.
  • Tests should not echo or otherwise print output. Instead, they should communicate results through assertions, exceptions, and PHPUnit's output mechanisms

Comment on lines +42 to +43
'domain' => 'index-eager-load-test-' . $i . '.wikibase.cloud',
'sitename' => 'Index Eager Load Test Site ' . $i,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: We are using a PHP version that supports string interpolation so lets use it as it reduces the line length. I also find it much easier to read.

Suggested change
'domain' => 'index-eager-load-test-' . $i . '.wikibase.cloud',
'sitename' => 'Index Eager Load Test Site ' . $i,
'domain' => "index-eager-load-test-{$i}.wikibase.cloud",
'sitename' => "Index Eager Load Test Site {$i}",

*/
public function show($id) {
return new PublicWikiResource(Wiki::findOrFail($id));
return new PublicWikiResource(Wiki::query()->with('wikiLatestProfile')->findOrFail($id));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vote is to remove it from this PR as:

  • it is not required
  • it is not related to the task (T421877)

It's good to keep PRs small and to the point where possible.

@dati18 dati18 requested review from dati18, deer-wmde and tarrow June 4, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants