Page MenuHomePhabricator

Subscribing to PageRecordChanged has different behaviour than subscribing to its subevents
Closed, InvalidPublic

Description

CommunityConfiguration uses the domain events mechanism to respond to page changes (so that cache can be invalidated). Recently, we received a bug report (T401322) that made me realise we shouldn't use just PageRevisionUpdated, as that does not include other changes of a page, such as page deletion.

To fix that, I decided to switch to PageRecordChanged. According to the docs, that should group together other events related to page changes: PageCreatedEvent, PageDeletedEvent, PageMovedEvent and PageLatestRevisionChangedEvent. In the bug report, @daniel confirmed directly subscribing to PageRecordChanged can be done to capture all related events:

@daniel I realise I am the first one who tries to subscribe to PageRecordChangedEvent directly. https://www.mediawiki.org/wiki/Manual:Domain_events says that is allowed (in case I want to cover more events at the same time), but I would appreciate a double-check from you as well.

Yes, that's fine. That'S exactly the kind of use case we had in mind when making this possible.

Keep in mind that the information you can get from the generic event object is rather limited. Let me know how this goes for you. One big question was whether listeners would end up needing to type-check and down-cast anyway, so they could as well just subscribe to the more specific events...

I implemented this in https://gerrit.wikimedia.org/r/c/mediawiki/extensions/CommunityConfiguration/+/1176770/6. Unfortunately, CI did not like what I did, and failed with:

12:48:12 There were 2 failures:
12:48:12 
12:48:12 1) GrowthExperiments\Tests\Integration\NewcomerMilestoneIngressTest::testMilestoneNotification with data set "notification sent when threshold reached" (4, 5, 1, 1)
12:48:12 Expected 1 notification(s) for milestone threshold
12:48:12 Failed asserting that actual size 0 matches expected size 1.
12:48:12 
12:48:12 /workspace/src/extensions/GrowthExperiments/tests/phpunit/integration/NewcomerTasks/MediawikiEventSubscribers/NewcomerMilestoneIngressTest.php:102
12:48:12 
12:48:12 2) GrowthExperiments\Tests\Integration\TaskTypeManagerTest::testFiltersTaskWhenLimitReached
12:48:12 Failed asserting that two arrays are identical.
12:48:12 --- Expected
12:48:12 +++ Actual
12:48:12 @@ @@
12:48:12  Array &0 (
12:48:12      0 => 'copyedit'
12:48:12 +    1 => 'link-recommendation'
12:48:12  )
12:48:12 
12:48:12 /workspace/src/extensions/GrowthExperiments/tests/phpunit/integration/NewcomerTasks/TaskType/TaskTypeManagerTest.php:66

To see what might cause this error, I tried splitting PageRecordChangedListener into the PageDeletedEvent and PageRevisionUpdated (deprecated, but used in CC beforehand), handling them separately. This passed the CI with no issues. I also tried switching to PageLatestRevisionChanged (as the replacement for PageRevisionUpdated) and subscribing to the remaining two events (PageCreatedEvent and PageMovedEvent), which also passed CI.

Based on those observations, it seems like subscribing to PageRecordChanged and individually to its subevents produce different results. I'm not sure what else I could try to localize the error – so far, my impression is the issue is somewhere within domain events rather than the way I integrate with it.

Filling a task with my observations, hoping I could be helped out.

Event Timeline

@daniel Would you be willing to take a look? For now, I subscribed to PageDeletedEvent and PageRevisionUpdatedListener separately, but as far as the docs say, that shouldn't be necessary. We'll be going ahead with the separate events for now to fix T401322 (given there is no response/movement on this task), but I still think the behaviour should be corrected (or the docs, depending on which one is wrong).

The issue seems to be this bit of code:

                $pageBefore = $event->getPageRecordBefore();
		if ( $pageBefore === null ) {
			// Nothing to do here. The page did not exist before, so cache cannot be populated.
			return;
		}

The assumption that the cache doesn't need to be invalidated if the page didn't previously exist seems reasonable, but is apparently wrong. Maybe the cache gets populated with an empty value by a previous attempt to read the config? Or it's carried over from another test?

In any case, the final version of your patch has:

        public function handlePageRevisionUpdatedEvent( PageRevisionUpdatedEvent $event ): void {
		$this->invalidateForPage( $event->getPage() );
	}

This unconditionally invalidates the cache when a revision is created, including the case when the page didn't previously exists. That works.

The implementation of PageRevisionUpdatedEvent::getPage() is return $this->getPageRecordAfter(). if we change our original code accordingly, it works:

                $pageBefore = $event->getPageRecordBefore();
		if ( $pageBefore === null ) {
			$page = $event->getPageRecordAfter();
		}

In summary, you hit the fact that the behavior of PageRecordChangedEvent::getPageBefore() is not the same as PageRevisionUpdatedEvent::getPage().

Note btw that getPageRecordBefore() is still needed to handle the deletion case. When using PageRecordChangedEvent, you need to check both, getPageBefore() and getPageRecordAfter(). Or you can use getPageId(), if all you needd is the ID. It's tempting to create a getPage() method that will never return null, we used to have that. But it's unclear what it should return in the case of page moves, so we removed it.

That reminds me - how should page moves be handled in your case? Should the cache be invalidated for the old title, or the new? Or both?

PS: NewcomerMilestoneIngressTest always failes for me in the way aou descfribe above, even on latest master. That issue seems unrelated.

PPS: If you use PageRecordChangedEvent, you may get multiple events for the same user action - creating a page causes a PageLatestRevisionChangedEvent (aka PageRevisionUpdatedEvent) and also a PageCreatedEvent (in that order, because page creation is not complete until after the first revision has been created). Similarly, moving a page will trigger a PageMoved event and as well as a PageLatestRevisionChangedEvent for the dummy revision.

I'm closing this as invlaid, since the premis of the task description doesn't hold. If there is something we could do in the DomainEvents framework that would avoid similar issues in the future, please file a ne wticket (and ping me on it).