LCOV - code coverage report
Current view: top level - server/permissions - GitVisibleChangeFilter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 23 46 50.0 %
Date: 2022-11-19 15:00:39 Functions: 7 9 77.8 %

          Line data    Source code
       1             : // Copyright (C) 2022 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.permissions;
      16             : 
      17             : import com.google.common.collect.ImmutableMap;
      18             : import com.google.common.collect.ImmutableSet;
      19             : import com.google.common.flogger.FluentLogger;
      20             : import com.google.gerrit.common.Nullable;
      21             : import com.google.gerrit.entities.Change;
      22             : import com.google.gerrit.entities.Project;
      23             : import com.google.gerrit.exceptions.StorageException;
      24             : import com.google.gerrit.server.git.SearchingChangeCacheImpl;
      25             : import com.google.gerrit.server.notedb.ChangeNotes;
      26             : import com.google.gerrit.server.query.change.ChangeData;
      27             : import java.io.IOException;
      28             : import java.util.HashMap;
      29             : import java.util.Objects;
      30             : import java.util.Set;
      31             : import java.util.stream.Stream;
      32             : import org.eclipse.jgit.lib.Repository;
      33             : 
      34             : /**
      35             :  * This class can tell efficiently if changes are visible to a user. It is intended to be used when
      36             :  * serving Git traffic on the Git wire protocol and in similar use cases when we need to know
      37             :  * efficiently if a (potentially large number) of changes are visible to a user.
      38             :  *
      39             :  * <p>The efficiency of this class comes from heuristic optimization:
      40             :  *
      41             :  * <ul>
      42             :  *   <li>For a low number of expected checks, we check visibility one-by-one.
      43             :  *   <li>For a high number of expected checks and settings where the change index is available, we
      44             :  *       load the N most recent changes from the index and filter them by visibility. This is fast,
      45             :  *       but comes with the caveat that older changes are pretended to be invisible.
      46             :  *   <li>For a high number of expected checks and settings where the change index is unavailable, we
      47             :  *       scan the repo and determine visibility one-by-one. This is *very* expensive.
      48             :  * </ul>
      49             :  *
      50             :  * <p>Changes that fail to load are pretended to be invisible. This is important on the Git paths as
      51             :  * we don't want to advertise change refs where we were unable to check the visibility (e.g. due to
      52             :  * data corruption on that change). At the same time, the overall operation should succeed as
      53             :  * otherwise a single broken change would break Git operations for an entire repo.
      54             :  */
      55             : public class GitVisibleChangeFilter {
      56          19 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      57             : 
      58             :   private static final int CHANGE_LIMIT_FOR_DIRECT_FILTERING = 5;
      59             : 
      60             :   private GitVisibleChangeFilter() {}
      61             : 
      62             :   /** Returns a map of all visible changes. Might pretend old changes are invisible. */
      63             :   static ImmutableMap<Change.Id, ChangeData> getVisibleChanges(
      64             :       @Nullable SearchingChangeCacheImpl searchingChangeCache,
      65             :       ChangeNotes.Factory changeNotesFactory,
      66             :       ChangeData.Factory changeDataFactory,
      67             :       Project.NameKey projectName,
      68             :       PermissionBackend.ForProject forProject,
      69             :       Repository repository,
      70             :       ImmutableSet<Change.Id> changes) {
      71             :     Stream<ChangeData> changeDatas;
      72          19 :     if (changes.size() < CHANGE_LIMIT_FOR_DIRECT_FILTERING) {
      73          19 :       logger.atFine().log("Loading changes one by one for project %s", projectName);
      74          19 :       changeDatas = loadChangeDatasOneByOne(changes, changeDataFactory, projectName);
      75           8 :     } else if (searchingChangeCache != null) {
      76           8 :       logger.atFine().log("Loading changes from SearchingChangeCache for project %s", projectName);
      77           8 :       changeDatas = searchingChangeCache.getChangeData(projectName);
      78             :     } else {
      79           0 :       logger.atFine().log("Loading changes from all refs for project %s", projectName);
      80           0 :       changeDatas =
      81           0 :           scanRepoForChangeDatas(changeNotesFactory, changeDataFactory, repository, projectName);
      82             :     }
      83          19 :     HashMap<Change.Id, ChangeData> result = new HashMap<>();
      84          19 :     changeDatas
      85          19 :         .filter(cd -> changes.contains(cd.getId()))
      86          19 :         .filter(
      87             :             cd -> {
      88             :               try {
      89          19 :                 return forProject.change(cd).test(ChangePermission.READ);
      90           0 :               } catch (PermissionBackendException e) {
      91           0 :                 throw new StorageException(e);
      92             :               }
      93             :             })
      94          19 :         .forEach(
      95             :             cd -> {
      96          18 :               if (result.containsKey(cd.getId())) {
      97           0 :                 logger.atWarning().log(
      98             :                     "Duplicate change datas for the repo %s: [%s, %s]",
      99           0 :                     projectName, cd, result.get(cd.getId()));
     100             :               }
     101          18 :               result.put(cd.getId(), cd);
     102          18 :             });
     103          19 :     return ImmutableMap.copyOf(result);
     104             :   }
     105             : 
     106             :   /** Get a stream of changes by loading them individually. */
     107             :   private static Stream<ChangeData> loadChangeDatasOneByOne(
     108             :       Set<Change.Id> ids, ChangeData.Factory changeDataFactory, Project.NameKey projectName) {
     109          19 :     return ids.stream()
     110          19 :         .map(
     111             :             id -> {
     112             :               try {
     113          19 :                 ChangeData cd = changeDataFactory.create(projectName, id);
     114          19 :                 cd.notes(); // Make sure notes are available. This will trigger loading notes and
     115             :                 // throw an exception in case the change is corrupt and can't be loaded. It will
     116             :                 // then be omitted from the result.
     117          19 :                 return cd;
     118           0 :               } catch (Exception e) {
     119             :                 // We drop changes that we can't load. The repositories contain 'dead' change refs
     120             :                 // and we want to overall operation to continue.
     121           0 :                 logger.atFinest().withCause(e).log("Can't load Change notes for %s", id);
     122           0 :                 return null;
     123             :               }
     124             :             })
     125          19 :         .filter(Objects::nonNull);
     126             :   }
     127             : 
     128             :   /** Get a stream of all changes by scanning the repo. This is extremely slow. */
     129             :   private static Stream<ChangeData> scanRepoForChangeDatas(
     130             :       ChangeNotes.Factory changeNotesFactory,
     131             :       ChangeData.Factory changeDataFactory,
     132             :       Repository repository,
     133             :       Project.NameKey projectName) {
     134             :     Stream<ChangeData> cds;
     135             :     try {
     136           0 :       cds =
     137             :           changeNotesFactory
     138           0 :               .scan(repository, projectName)
     139           0 :               .map(
     140             :                   notesResult -> {
     141           0 :                     if (!notesResult.error().isPresent()) {
     142           0 :                       return changeDataFactory.create(notesResult.notes());
     143             :                     }
     144           0 :                     logger.atWarning().withCause(notesResult.error().get()).log(
     145           0 :                         "Unable to load ChangeNotes for %s", notesResult.id());
     146           0 :                     return null;
     147             :                   })
     148           0 :               .filter(Objects::nonNull);
     149           0 :     } catch (IOException e) {
     150           0 :       throw new StorageException(e);
     151           0 :     }
     152           0 :     return cds;
     153             :   }
     154             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750