diff --git a/src/Model/Project.php b/src/Model/Project.php index 6647fcb5b..137d1c232 100644 --- a/src/Model/Project.php +++ b/src/Model/Project.php @@ -107,7 +107,8 @@ protected function getMetadata(): ?array { * @return bool */ public function exists(): bool { - return !empty( $this->getDomain() ); + return !empty( $this->getDomain() ) + && $this->repository->checkReplication($this->getBasicInfo()['dbName']); } /** @@ -148,9 +149,9 @@ public function getUrl( bool $withTrailingSlash = true ): string { /** * @param Page|string $page Full page title including namespace, or a Page object. * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid - * an API call. This should be used only if you fetched the page title via other - * means (SQL query), and is not from user input alone. Only applicable if $page - * is a Page object. + * an API call. This should be used only if you fetched the page title via other + * means (SQL query), and is not from user input alone. Only applicable if $page + * is a Page object. * @return string */ public function getUrlForPage( Page|string $page, bool $useUnnormalizedPageTitle = false ): string { @@ -356,5 +357,5 @@ public function userHasOptedIn( User $user ): bool { */ public function getTableName( string $tableName, ?string $tableExtension = null ): string { return $this->getRepository()->getTableName( $this->getDatabaseName(), $tableName, $tableExtension ); - } + } } diff --git a/src/Repository/GlobalContribsRepository.php b/src/Repository/GlobalContribsRepository.php index ddc342945..008a079ef 100644 --- a/src/Repository/GlobalContribsRepository.php +++ b/src/Repository/GlobalContribsRepository.php @@ -55,6 +55,8 @@ public function globalEditCounts( User $user ): ?array { // Pre-populate all projects' metadata, to prevent each project call from fetching it. $this->caProject->getRepository()->getAll(); + + $this->checkReplicationAllProjects(); // Compile the output. $out = []; @@ -110,6 +112,34 @@ protected function globalEditCountsFromCentralAuth( User $user ): ?array { // Cache and return. return $this->setCache( $cacheKey, $out ); } + + /** + * Get, slice by slice, the list of projects that are actually replicated. + * Takes about 0.5s per slice. + * @return bool[] Keyed by database name, all values are true. + */ + public function checkReplicationAllProjects(): array { + $cacheKey = $this->getCacheKey( "global_replication_check" ); + if ( $this->cache->hasItem( $cacheKey ) ) { + return $this->cache->getItem( $cacheKey )->get(); + } + $result = []; + $exists = true; + $i = 0; + $sql = "SELECT DISTINCT table_schema + FROM information_schema.tables"; + while ( $exists ) { + $i += 1; + try { + $queryResult = $this->executeProjectsQuery( "s$i", $sql )->fetchFirstColumn(); + $result = array_merge( $result, $queryResult ); + } catch ( \Throwable ) { + $exists = false; + } + } + return $this->setCache( $cacheKey, $result, 'PT1H' ); + } + /** * Loop through the given dbNames and create Project objects for each. diff --git a/src/Repository/ProjectRepository.php b/src/Repository/ProjectRepository.php index 78603d61e..ed8f9eeed 100644 --- a/src/Repository/ProjectRepository.php +++ b/src/Repository/ProjectRepository.php @@ -219,7 +219,47 @@ public function getOne( string $project ): ?array { // Cache for one hour and return. return $this->setCache( $cacheKey, $basicInfo, 'PT1H' ); } - + + /** + * Is this project actually replicated? Sometimes projets aren't, + * despite being listed in meta_p.wiki. See T322466. + * @param string $project Database name, without _p. + * @return bool + */ + public function checkReplication( string $project ): bool { + if ( '' == $project ) { + // This means we failed to getBasicInfo. Let's try and AGF. + // Plus, keeps tests from breaking down. + return true; + } + $cacheKey = $this->getCacheKey( $project, "replication_check" ); + if ( $this->cache->hasItem( $cacheKey )) { + return $this->cache->getItem( $cacheKey )->get(); + } + // GlobalContribs preloads replication checks for *all* projects + $allProjectsCacheKey = $this->getCacheKey( '', "global_replication_check" ); + if ( $this->cache->hasItem( $allProjectsCacheKey )) { + $globalReplicationChecks = $this->cache->getItem( $allProjectsCacheKey )->get(); + return array_key_exists( $project, $globalReplicationChecks ); + } + $dbList = $this->getDbList(); + if ( !array_key_exists( $project, $dbList )) { + $result = false; + } else { + $dbSlice = $dbList[$project]; + $sql = "SELECT 1 + FROM information_schema.tables + WHERE table_schema = :project + LIMIT 1"; + $queryResult = $this->executeProjectsQuery( $dbSlice, $sql, [ + 'project' => $project . "_p", + ] )->fetchAssociative(); + $result = ( 1 == count( $queryResult )); + } + // Cache for 1h and return + return $this->setCache( $cacheKey, $result, 'PT1H' ); // feels long to me, but as long as getOne + } + /** * Get metadata about a project, including the 'dbName', 'url' and 'lang' * diff --git a/tests/Model/GlobalContribsTest.php b/tests/Model/GlobalContribsTest.php index abd60d000..9378d0828 100644 --- a/tests/Model/GlobalContribsTest.php +++ b/tests/Model/GlobalContribsTest.php @@ -86,6 +86,8 @@ public function testGlobalEdits(): void { 'dbName' => 'wiki1', 'url' => 'https://wiki1.example.org', ] ); + $wiki1Repo->method('checkReplication') + ->willReturn(true); $wiki1 = new Project( 'wiki1' ); $wiki1->setRepository( $wiki1Repo ); diff --git a/tests/TestAdapter.php b/tests/TestAdapter.php index 3c2486e1c..414053ef8 100644 --- a/tests/TestAdapter.php +++ b/tests/TestAdapter.php @@ -30,6 +30,8 @@ public function getProjectRepo(): MockObject { 'dbName' => 'test_wiki', 'lang' => 'en', ] ); + $repo->method('checkReplication') + ->willReturn(true); return $repo; }