LCOV - code coverage report
Current view: top level - server/account - AccountsUpdate.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 165 168 98.2 %
Date: 2022-11-19 15:00:39 Functions: 33 33 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2017 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.account;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static com.google.common.base.Preconditions.checkState;
      19             : import static java.util.Objects.requireNonNull;
      20             : import static java.util.stream.Collectors.toList;
      21             : import static java.util.stream.Collectors.toSet;
      22             : 
      23             : import com.google.common.annotations.VisibleForTesting;
      24             : import com.google.common.base.Strings;
      25             : import com.google.common.base.Throwables;
      26             : import com.google.common.collect.ImmutableList;
      27             : import com.google.common.collect.Iterables;
      28             : import com.google.gerrit.entities.Account;
      29             : import com.google.gerrit.exceptions.DuplicateKeyException;
      30             : import com.google.gerrit.exceptions.StorageException;
      31             : import com.google.gerrit.git.LockFailureException;
      32             : import com.google.gerrit.git.RefUpdateUtil;
      33             : import com.google.gerrit.server.GerritPersonIdent;
      34             : import com.google.gerrit.server.IdentifiedUser;
      35             : import com.google.gerrit.server.account.externalids.ExternalIdNotes;
      36             : import com.google.gerrit.server.account.externalids.ExternalIdNotes.ExternalIdNotesLoader;
      37             : import com.google.gerrit.server.account.externalids.ExternalIds;
      38             : import com.google.gerrit.server.config.AllUsersName;
      39             : import com.google.gerrit.server.config.CachedPreferences;
      40             : import com.google.gerrit.server.config.VersionedDefaultPreferences;
      41             : import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
      42             : import com.google.gerrit.server.git.GitRepositoryManager;
      43             : import com.google.gerrit.server.git.meta.MetaDataUpdate;
      44             : import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
      45             : import com.google.gerrit.server.notedb.Sequences;
      46             : import com.google.gerrit.server.update.RetryHelper;
      47             : import com.google.gerrit.server.update.RetryableAction.Action;
      48             : import com.google.inject.Provider;
      49             : import com.google.inject.assistedinject.Assisted;
      50             : import com.google.inject.assistedinject.AssistedInject;
      51             : import java.io.IOException;
      52             : import java.util.ArrayList;
      53             : import java.util.List;
      54             : import java.util.Objects;
      55             : import java.util.Optional;
      56             : import java.util.Set;
      57             : import java.util.function.Consumer;
      58             : import org.eclipse.jgit.errors.ConfigInvalidException;
      59             : import org.eclipse.jgit.lib.BatchRefUpdate;
      60             : import org.eclipse.jgit.lib.ObjectId;
      61             : import org.eclipse.jgit.lib.PersonIdent;
      62             : import org.eclipse.jgit.lib.Repository;
      63             : import org.eclipse.jgit.revwalk.RevCommit;
      64             : 
      65             : /**
      66             :  * Creates and updates accounts.
      67             :  *
      68             :  * <p>This class should be used for all account updates. See {@link AccountDelta} for what can be
      69             :  * updated.
      70             :  *
      71             :  * <p>Batch updates of multiple different accounts can be performed atomically, see {@link
      72             :  * #updateBatch(List)}. Batch creation is not supported.
      73             :  *
      74             :  * <p>For any account update the caller must provide a commit message, the account ID and an {@link
      75             :  * ConfigureDeltaFromState}. The account updater reads the current {@link AccountState} and prepares
      76             :  * updates to the account by calling setters on the provided {@link AccountDelta.Builder}. If the
      77             :  * current account state is of no interest the caller may also provide a {@link Consumer} for {@link
      78             :  * AccountDelta.Builder} instead of the account updater.
      79             :  *
      80             :  * <p>The provided commit message is used for the update of the user branch. Using a precise and
      81             :  * unique commit message allows to identify the code from which an update was made when looking at a
      82             :  * commit in the user branch, and thus help debugging.
      83             :  *
      84             :  * <p>For creating a new account a new account ID can be retrieved from {@link
      85             :  * Sequences#nextAccountId()}.
      86             :  *
      87             :  * <p>The account updates are written to NoteDb. In NoteDb accounts are represented as user branches
      88             :  * in the {@code All-Users} repository. Optionally a user branch can contain a 'account.config' file
      89             :  * that stores account properties, such as full name, display name, preferred email, status and the
      90             :  * active flag. The timestamp of the first commit on a user branch denotes the registration date.
      91             :  * The initial commit on the user branch may be empty (since having an 'account.config' is
      92             :  * optional). See {@link AccountConfig} for details of the 'account.config' file format. In addition
      93             :  * the user branch can contain a 'preferences.config' config file to store preferences (see {@link
      94             :  * StoredPreferences}) and a 'watch.config' config file to store project watches (see {@link
      95             :  * ProjectWatches}). External IDs are stored separately in the {@code refs/meta/external-ids} notes
      96             :  * branch (see {@link ExternalIdNotes}).
      97             :  *
      98             :  * <p>On updating an account the account is evicted from the account cache and reindexed. The
      99             :  * eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate}
     100             :  * class which receives the event about updating the user branch that is triggered by this class.
     101             :  *
     102             :  * <p>If external IDs are updated, the ExternalIdCache is automatically updated by {@link
     103             :  * ExternalIdNotes}. In addition {@link ExternalIdNotes} takes care about evicting and reindexing
     104             :  * corresponding accounts. This is needed because external ID updates don't touch the user branches.
     105             :  * Hence in this case the accounts are not evicted and reindexed via {@link ReindexAfterRefUpdate}.
     106             :  *
     107             :  * <p>Reindexing and flushing accounts from the account cache can be disabled by
     108             :  *
     109             :  * <ul>
     110             :  *   <li>binding {@link GitReferenceUpdated#DISABLED} and
     111             :  *   <li>passing an {@link
     112             :  *       com.google.gerrit.server.account.externalids.ExternalIdNotes.FactoryNoReindex} factory as
     113             :  *       parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser,
     114             :  *       ExternalIdNotes.ExternalIdNotesLoader)}
     115             :  * </ul>
     116             :  *
     117             :  * <p>If there are concurrent account updates updating the user branch in NoteDb may fail with
     118             :  * {@link LockFailureException}. In this case the account update is automatically retried and the
     119             :  * account updater is invoked once more with the updated account state. This means the whole
     120             :  * read-modify-write sequence is atomic. Retrying is limited by a timeout. If the timeout is
     121             :  * exceeded the account update can still fail with {@link LockFailureException}.
     122             :  */
     123             : public class AccountsUpdate {
     124             :   public interface Factory {
     125             :     /**
     126             :      * Creates an {@code AccountsUpdate} which uses the identity of the specified user as author for
     127             :      * all commits related to accounts. The server identity will be used as committer.
     128             :      *
     129             :      * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
     130             :      * com.google.gerrit.server.UserInitiated} annotation on the provider of an {@code
     131             :      * AccountsUpdate} instead.
     132             :      *
     133             :      * @param currentUser the user to which modifications should be attributed
     134             :      * @param externalIdNotesLoader the loader that should be used to load external ID notes
     135             :      */
     136             :     AccountsUpdate create(IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader);
     137             : 
     138             :     /**
     139             :      * Creates an {@code AccountsUpdate} which uses the server identity as author and committer for
     140             :      * all commits related to accounts.
     141             :      *
     142             :      * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
     143             :      * com.google.gerrit.server.ServerInitiated} annotation on the provider of an {@code
     144             :      * AccountsUpdate} instead.
     145             :      *
     146             :      * @param externalIdNotesLoader the loader that should be used to load external ID notes
     147             :      */
     148             :     AccountsUpdate createWithServerIdent(ExternalIdNotesLoader externalIdNotesLoader);
     149             :   }
     150             : 
     151             :   /**
     152             :    * Account updates are commonly performed by evaluating the current account state and creating a
     153             :    * delta to be applied to it in a later step. This is done by implementing this interface.
     154             :    *
     155             :    * <p>If the current account state is not needed, use a {@link Consumer} of {@link
     156             :    * AccountDelta.Builder} instead.
     157             :    */
     158             :   @FunctionalInterface
     159             :   public interface ConfigureDeltaFromState {
     160             :     /**
     161             :      * Receives the current {@link AccountState} (which is immutable) and configures an {@link
     162             :      * AccountDelta.Builder} with changes to the account.
     163             :      *
     164             :      * @param accountState the state of the account that is being updated
     165             :      * @param delta the changes to be applied
     166             :      */
     167             :     void configure(AccountState accountState, AccountDelta.Builder delta) throws IOException;
     168             :   }
     169             : 
     170             :   /** Data holder for the set of arguments required to update an account. Used for batch updates. */
     171             :   public static class UpdateArguments {
     172             :     private final String message;
     173             :     private final Account.Id accountId;
     174             :     private final ConfigureDeltaFromState configureDeltaFromState;
     175             : 
     176             :     public UpdateArguments(
     177          33 :         String message, Account.Id accountId, ConfigureDeltaFromState configureDeltaFromState) {
     178          33 :       this.message = message;
     179          33 :       this.accountId = accountId;
     180          33 :       this.configureDeltaFromState = configureDeltaFromState;
     181          33 :     }
     182             :   }
     183             : 
     184             :   private final GitRepositoryManager repoManager;
     185             :   private final GitReferenceUpdated gitRefUpdated;
     186             :   private final Optional<IdentifiedUser> currentUser;
     187             :   private final AllUsersName allUsersName;
     188             :   private final ExternalIds externalIds;
     189             :   private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
     190             :   private final RetryHelper retryHelper;
     191             :   private final ExternalIdNotesLoader extIdNotesLoader;
     192             :   private final PersonIdent committerIdent;
     193             :   private final PersonIdent authorIdent;
     194             : 
     195             :   /** Invoked after reading the account config. */
     196             :   private final Runnable afterReadRevision;
     197             : 
     198             :   /** Invoked after updating the account but before committing the changes. */
     199             :   private final Runnable beforeCommit;
     200             : 
     201             :   /** Single instance that accumulates updates from the batch. */
     202             :   private ExternalIdNotes externalIdNotes;
     203             : 
     204             :   @AssistedInject
     205             :   @SuppressWarnings("BindingAnnotationWithoutInject")
     206             :   AccountsUpdate(
     207             :       GitRepositoryManager repoManager,
     208             :       GitReferenceUpdated gitRefUpdated,
     209             :       AllUsersName allUsersName,
     210             :       ExternalIds externalIds,
     211             :       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
     212             :       RetryHelper retryHelper,
     213             :       @GerritPersonIdent PersonIdent serverIdent,
     214             :       @Assisted ExternalIdNotesLoader extIdNotesLoader) {
     215         151 :     this(
     216             :         repoManager,
     217             :         gitRefUpdated,
     218         151 :         Optional.empty(),
     219             :         allUsersName,
     220             :         externalIds,
     221             :         metaDataUpdateInternalFactory,
     222             :         retryHelper,
     223             :         extIdNotesLoader,
     224             :         serverIdent,
     225         151 :         createPersonIdent(serverIdent, Optional.empty()),
     226             :         AccountsUpdate::doNothing,
     227             :         AccountsUpdate::doNothing);
     228         151 :   }
     229             : 
     230             :   @AssistedInject
     231             :   @SuppressWarnings("BindingAnnotationWithoutInject")
     232             :   AccountsUpdate(
     233             :       GitRepositoryManager repoManager,
     234             :       GitReferenceUpdated gitRefUpdated,
     235             :       AllUsersName allUsersName,
     236             :       ExternalIds externalIds,
     237             :       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
     238             :       RetryHelper retryHelper,
     239             :       @GerritPersonIdent PersonIdent serverIdent,
     240             :       @Assisted IdentifiedUser currentUser,
     241             :       @Assisted ExternalIdNotesLoader extIdNotesLoader) {
     242          23 :     this(
     243             :         repoManager,
     244             :         gitRefUpdated,
     245          23 :         Optional.of(currentUser),
     246             :         allUsersName,
     247             :         externalIds,
     248             :         metaDataUpdateInternalFactory,
     249             :         retryHelper,
     250             :         extIdNotesLoader,
     251             :         serverIdent,
     252          23 :         createPersonIdent(serverIdent, Optional.of(currentUser)),
     253             :         AccountsUpdate::doNothing,
     254             :         AccountsUpdate::doNothing);
     255          23 :   }
     256             : 
     257             :   @VisibleForTesting
     258             :   public AccountsUpdate(
     259             :       GitRepositoryManager repoManager,
     260             :       GitReferenceUpdated gitRefUpdated,
     261             :       Optional<IdentifiedUser> currentUser,
     262             :       AllUsersName allUsersName,
     263             :       ExternalIds externalIds,
     264             :       Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
     265             :       RetryHelper retryHelper,
     266             :       ExternalIdNotesLoader extIdNotesLoader,
     267             :       PersonIdent committerIdent,
     268             :       PersonIdent authorIdent,
     269             :       Runnable afterReadRevision,
     270         151 :       Runnable beforeCommit) {
     271         151 :     this.repoManager = requireNonNull(repoManager, "repoManager");
     272         151 :     this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated");
     273         151 :     this.currentUser = currentUser;
     274         151 :     this.allUsersName = requireNonNull(allUsersName, "allUsersName");
     275         151 :     this.externalIds = requireNonNull(externalIds, "externalIds");
     276         151 :     this.metaDataUpdateInternalFactory =
     277         151 :         requireNonNull(metaDataUpdateInternalFactory, "metaDataUpdateInternalFactory");
     278         151 :     this.retryHelper = requireNonNull(retryHelper, "retryHelper");
     279         151 :     this.extIdNotesLoader = requireNonNull(extIdNotesLoader, "extIdNotesLoader");
     280         151 :     this.committerIdent = requireNonNull(committerIdent, "committerIdent");
     281         151 :     this.authorIdent = requireNonNull(authorIdent, "authorIdent");
     282         151 :     this.afterReadRevision = requireNonNull(afterReadRevision, "afterReadRevision");
     283         151 :     this.beforeCommit = requireNonNull(beforeCommit, "beforeCommit");
     284         151 :   }
     285             : 
     286             :   /** Returns an instance that runs all specified consumers. */
     287             :   public static ConfigureDeltaFromState joinConsumers(
     288             :       List<Consumer<AccountDelta.Builder>> consumers) {
     289           5 :     return (accountStateIgnored, update) -> consumers.forEach(c -> c.accept(update));
     290             :   }
     291             : 
     292             :   private static ConfigureDeltaFromState fromConsumer(Consumer<AccountDelta.Builder> consumer) {
     293         151 :     return (a, u) -> consumer.accept(u);
     294             :   }
     295             : 
     296             :   private static PersonIdent createPersonIdent(
     297             :       PersonIdent serverIdent, Optional<IdentifiedUser> user) {
     298         151 :     return user.isPresent() ? user.get().newCommitterIdent(serverIdent) : serverIdent;
     299             :   }
     300             : 
     301             :   /**
     302             :    * Like {@link #insert(String, Account.Id, ConfigureDeltaFromState)}, but using a {@link Consumer}
     303             :    * instead, i.e. the update does not depend on the current account state (which, for insertion,
     304             :    * would only contain the account ID).
     305             :    */
     306             :   public AccountState insert(
     307             :       String message, Account.Id accountId, Consumer<AccountDelta.Builder> init)
     308             :       throws IOException, ConfigInvalidException {
     309         151 :     return insert(message, accountId, fromConsumer(init));
     310             :   }
     311             : 
     312             :   /**
     313             :    * Inserts a new account.
     314             :    *
     315             :    * @param message commit message for the account creation, must not be {@code null or empty}
     316             :    * @param accountId ID of the new account
     317             :    * @param init to populate the new account
     318             :    * @return the newly created account
     319             :    * @throws DuplicateKeyException if the account already exists
     320             :    * @throws IOException if creating the user branch fails due to an IO error
     321             :    * @throws ConfigInvalidException if any of the account fields has an invalid value
     322             :    */
     323             :   public AccountState insert(String message, Account.Id accountId, ConfigureDeltaFromState init)
     324             :       throws IOException, ConfigInvalidException {
     325         151 :     return execute(
     326         151 :             ImmutableList.of(
     327             :                 repo -> {
     328         151 :                   AccountConfig accountConfig = read(repo, accountId);
     329         151 :                   Account account = accountConfig.getNewAccount(committerIdent.getWhenAsInstant());
     330         151 :                   AccountState accountState = AccountState.forAccount(account);
     331         151 :                   AccountDelta.Builder deltaBuilder = AccountDelta.builder();
     332         151 :                   init.configure(accountState, deltaBuilder);
     333             : 
     334         151 :                   AccountDelta accountDelta = deltaBuilder.build();
     335         151 :                   accountConfig.setAccountDelta(accountDelta);
     336         151 :                   externalIdNotes =
     337         151 :                       createExternalIdNotes(
     338         151 :                           repo, accountConfig.getExternalIdsRev(), accountId, accountDelta);
     339         151 :                   CachedPreferences defaultPreferences =
     340         151 :                       CachedPreferences.fromConfig(
     341         151 :                           VersionedDefaultPreferences.get(repo, allUsersName));
     342             : 
     343         151 :                   return new UpdatedAccount(message, accountConfig, defaultPreferences, true);
     344             :                 }))
     345         151 :         .get(0)
     346         151 :         .get();
     347             :   }
     348             : 
     349             :   /**
     350             :    * Like {@link #update(String, Account.Id, ConfigureDeltaFromState)}, but using a {@link Consumer}
     351             :    * instead, i.e. the update does not depend on the current account state.
     352             :    */
     353             :   public Optional<AccountState> update(
     354             :       String message, Account.Id accountId, Consumer<AccountDelta.Builder> update)
     355             :       throws IOException, ConfigInvalidException {
     356          27 :     return update(message, accountId, fromConsumer(update));
     357             :   }
     358             : 
     359             :   /**
     360             :    * Gets the account and updates it atomically.
     361             :    *
     362             :    * <p>Changing the registration date of an account is not supported.
     363             :    *
     364             :    * @param message commit message for the account update, must not be {@code null or empty}
     365             :    * @param accountId ID of the account
     366             :    * @param configureDeltaFromState deltaBuilder to update the account, only invoked if the account
     367             :    *     exists
     368             :    * @return the updated account, {@link Optional#empty} if the account doesn't exist
     369             :    * @throws IOException if updating the user branch fails due to an IO error
     370             :    * @throws LockFailureException if updating the user branch still fails due to concurrent updates
     371             :    *     after the retry timeout exceeded
     372             :    * @throws ConfigInvalidException if any of the account fields has an invalid value
     373             :    */
     374             :   public Optional<AccountState> update(
     375             :       String message, Account.Id accountId, ConfigureDeltaFromState configureDeltaFromState)
     376             :       throws LockFailureException, IOException, ConfigInvalidException {
     377          33 :     return updateBatch(
     378          33 :             ImmutableList.of(new UpdateArguments(message, accountId, configureDeltaFromState)))
     379          33 :         .get(0);
     380             :   }
     381             : 
     382             :   private ExecutableUpdate createExecutableUpdate(UpdateArguments updateArguments) {
     383          33 :     return repo -> {
     384          33 :       AccountConfig accountConfig = read(repo, updateArguments.accountId);
     385          33 :       CachedPreferences defaultPreferences =
     386          33 :           CachedPreferences.fromConfig(VersionedDefaultPreferences.get(repo, allUsersName));
     387          33 :       Optional<AccountState> accountState =
     388          33 :           AccountState.fromAccountConfig(externalIds, accountConfig, defaultPreferences);
     389          33 :       if (!accountState.isPresent()) {
     390           1 :         return null;
     391             :       }
     392             : 
     393          33 :       AccountDelta.Builder deltaBuilder = AccountDelta.builder();
     394          33 :       updateArguments.configureDeltaFromState.configure(accountState.get(), deltaBuilder);
     395             : 
     396          33 :       AccountDelta delta = deltaBuilder.build();
     397          33 :       accountConfig.setAccountDelta(delta);
     398          33 :       ExternalIdNotes.checkSameAccount(
     399          33 :           Iterables.concat(
     400          33 :               delta.getCreatedExternalIds(),
     401          33 :               delta.getUpdatedExternalIds(),
     402          33 :               delta.getDeletedExternalIds()),
     403             :           updateArguments.accountId);
     404             : 
     405          33 :       if (externalIdNotes == null) {
     406          33 :         externalIdNotes =
     407          33 :             extIdNotesLoader.load(
     408          33 :                 repo, accountConfig.getExternalIdsRev().orElse(ObjectId.zeroId()));
     409             :       }
     410          33 :       externalIdNotes.replace(delta.getDeletedExternalIds(), delta.getCreatedExternalIds());
     411          33 :       externalIdNotes.upsert(delta.getUpdatedExternalIds());
     412             : 
     413          33 :       CachedPreferences cachedDefaultPreferences =
     414          33 :           CachedPreferences.fromConfig(VersionedDefaultPreferences.get(repo, allUsersName));
     415             : 
     416          33 :       return new UpdatedAccount(
     417             :           updateArguments.message, accountConfig, cachedDefaultPreferences, false);
     418             :     };
     419             :   }
     420             : 
     421             :   /**
     422             :    * Updates multiple different accounts atomically. This will only store a single new value (aka
     423             :    * set of all external IDs of the host) in the external ID cache, which is important for storage
     424             :    * economy. All {@code updates} must be for different accounts.
     425             :    *
     426             :    * <p>NOTE on error handling: Since updates are executed in multiple stages, with some stages
     427             :    * resulting from the union of all individual updates, we cannot point to the update that caused
     428             :    * the error. Callers should be aware that a single "update of death" (or a set of updates that
     429             :    * together have this property) will always prevent the entire batch from being executed.
     430             :    */
     431             :   public ImmutableList<Optional<AccountState>> updateBatch(List<UpdateArguments> updates)
     432             :       throws IOException, ConfigInvalidException {
     433          33 :     checkArgument(
     434          33 :         updates.stream().map(u -> u.accountId.get()).distinct().count() == updates.size(),
     435             :         "updates must all be for different accounts");
     436          33 :     return execute(updates.stream().map(this::createExecutableUpdate).collect(toList()));
     437             :   }
     438             : 
     439             :   private AccountConfig read(Repository allUsersRepo, Account.Id accountId)
     440             :       throws IOException, ConfigInvalidException {
     441         151 :     AccountConfig accountConfig = new AccountConfig(accountId, allUsersName, allUsersRepo).load();
     442         151 :     afterReadRevision.run();
     443         151 :     return accountConfig;
     444             :   }
     445             : 
     446             :   private ImmutableList<Optional<AccountState>> execute(List<ExecutableUpdate> executableUpdates)
     447             :       throws IOException, ConfigInvalidException {
     448         151 :     List<Optional<AccountState>> accountState = new ArrayList<>();
     449         151 :     List<UpdatedAccount> updatedAccounts = new ArrayList<>();
     450         151 :     executeWithRetry(
     451             :         () -> {
     452             :           // Reset state for retry.
     453         151 :           externalIdNotes = null;
     454         151 :           accountState.clear();
     455         151 :           updatedAccounts.clear();
     456             : 
     457         151 :           try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
     458         151 :             for (ExecutableUpdate executableUpdate : executableUpdates) {
     459         151 :               updatedAccounts.add(executableUpdate.execute(allUsersRepo));
     460         151 :             }
     461         151 :             commit(
     462         151 :                 allUsersRepo, updatedAccounts.stream().filter(Objects::nonNull).collect(toList()));
     463         151 :             for (UpdatedAccount ua : updatedAccounts) {
     464         151 :               accountState.add(ua == null ? Optional.empty() : ua.getAccountState());
     465         151 :             }
     466             :           }
     467         151 :           return null;
     468             :         });
     469         151 :     return ImmutableList.copyOf(accountState);
     470             :   }
     471             : 
     472             :   private void executeWithRetry(Action<Void> action) throws IOException, ConfigInvalidException {
     473             :     try {
     474         151 :       retryHelper.accountUpdate("updateAccount", action).call();
     475           3 :     } catch (Exception e) {
     476           1 :       Throwables.throwIfUnchecked(e);
     477           0 :       Throwables.throwIfInstanceOf(e, IOException.class);
     478           0 :       Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
     479           0 :       throw new StorageException(e);
     480         151 :     }
     481         151 :   }
     482             : 
     483             :   private ExternalIdNotes createExternalIdNotes(
     484             :       Repository allUsersRepo, Optional<ObjectId> rev, Account.Id accountId, AccountDelta update)
     485             :       throws IOException, ConfigInvalidException, DuplicateKeyException {
     486         151 :     ExternalIdNotes.checkSameAccount(
     487         151 :         Iterables.concat(
     488         151 :             update.getCreatedExternalIds(),
     489         151 :             update.getUpdatedExternalIds(),
     490         151 :             update.getDeletedExternalIds()),
     491             :         accountId);
     492             : 
     493         151 :     ExternalIdNotes extIdNotes = extIdNotesLoader.load(allUsersRepo, rev.orElse(ObjectId.zeroId()));
     494         151 :     extIdNotes.replace(update.getDeletedExternalIds(), update.getCreatedExternalIds());
     495         151 :     extIdNotes.upsert(update.getUpdatedExternalIds());
     496         151 :     return extIdNotes;
     497             :   }
     498             : 
     499             :   private void commit(Repository allUsersRepo, List<UpdatedAccount> updatedAccounts)
     500             :       throws IOException {
     501         151 :     if (updatedAccounts.isEmpty()) {
     502           1 :       return;
     503             :     }
     504             : 
     505         151 :     beforeCommit.run();
     506             : 
     507         151 :     BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
     508             : 
     509             :     String externalIdUpdateMessage =
     510         151 :         updatedAccounts.size() == 1
     511         151 :             ? Iterables.getOnlyElement(updatedAccounts).message
     512         151 :             : "Batch update for " + updatedAccounts.size() + " accounts";
     513         151 :     ObjectId oldExternalIdsRevision = externalIdNotes.getRevision();
     514             :     // These update the same ref, so they need to be stacked on top of one another using the same
     515             :     // ExternalIdNotes instance.
     516         151 :     RevCommit revCommit =
     517         151 :         commitExternalIdUpdates(externalIdUpdateMessage, allUsersRepo, batchRefUpdate);
     518         151 :     boolean externalIdsUpdated = !Objects.equals(revCommit.getId(), oldExternalIdsRevision);
     519         151 :     for (UpdatedAccount updatedAccount : updatedAccounts) {
     520             : 
     521             :       // These updates are all for different refs (because batches never update the same account
     522             :       // more than once), so there can be multiple commits in the same batch, all with the same base
     523             :       // revision in their AccountConfig.
     524             :       // We allow empty commits:
     525             :       // 1) When creating a new account, so that the user branch gets created with an empty commit
     526             :       // when no account properties are set and hence no
     527             :       // 'account.config' file will be created.
     528             :       // 2) When updating "refs/meta/external-ids", so that refs/users/* meta ref is updated too.
     529             :       // This allows to schedule reindexing of account transactionally  on refs/users/* meta
     530             :       // updates.
     531         151 :       boolean allowEmptyCommit = externalIdsUpdated || updatedAccount.created;
     532         151 :       commitAccountConfig(
     533             :           updatedAccount.message,
     534             :           allUsersRepo,
     535             :           batchRefUpdate,
     536             :           updatedAccount.accountConfig,
     537             :           allowEmptyCommit);
     538         151 :     }
     539             : 
     540         151 :     RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
     541             : 
     542         151 :     Set<Account.Id> accountsToSkipForReindex = getUpdatedAccountIds(batchRefUpdate);
     543         151 :     extIdNotesLoader.updateExternalIdCacheAndMaybeReindexAccounts(
     544             :         externalIdNotes, accountsToSkipForReindex);
     545             : 
     546         151 :     gitRefUpdated.fire(
     547         151 :         allUsersName, batchRefUpdate, currentUser.map(IdentifiedUser::state).orElse(null));
     548         151 :   }
     549             : 
     550             :   private static Set<Account.Id> getUpdatedAccountIds(BatchRefUpdate batchRefUpdate) {
     551         151 :     return batchRefUpdate.getCommands().stream()
     552         151 :         .map(c -> Account.Id.fromRef(c.getRefName()))
     553         151 :         .filter(Objects::nonNull)
     554         151 :         .collect(toSet());
     555             :   }
     556             : 
     557             :   private void commitAccountConfig(
     558             :       String message,
     559             :       Repository allUsersRepo,
     560             :       BatchRefUpdate batchRefUpdate,
     561             :       AccountConfig accountConfig,
     562             :       boolean allowEmptyCommit)
     563             :       throws IOException {
     564         151 :     try (MetaDataUpdate md = createMetaDataUpdate(message, allUsersRepo, batchRefUpdate)) {
     565         151 :       md.setAllowEmpty(allowEmptyCommit);
     566         151 :       accountConfig.commit(md);
     567             :     }
     568         151 :   }
     569             : 
     570             :   private RevCommit commitExternalIdUpdates(
     571             :       String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate) throws IOException {
     572         151 :     try (MetaDataUpdate md = createMetaDataUpdate(message, allUsersRepo, batchRefUpdate)) {
     573         151 :       return externalIdNotes.commit(md);
     574             :     }
     575             :   }
     576             : 
     577             :   private MetaDataUpdate createMetaDataUpdate(
     578             :       String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate) {
     579         151 :     MetaDataUpdate metaDataUpdate =
     580         151 :         metaDataUpdateInternalFactory.get().create(allUsersName, allUsersRepo, batchRefUpdate);
     581         151 :     if (!message.endsWith("\n")) {
     582         151 :       message = message + "\n";
     583             :     }
     584             : 
     585         151 :     metaDataUpdate.getCommitBuilder().setMessage(message);
     586         151 :     metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
     587         151 :     metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
     588         151 :     return metaDataUpdate;
     589             :   }
     590             : 
     591         151 :   private static void doNothing() {}
     592             : 
     593             :   @FunctionalInterface
     594             :   private interface ExecutableUpdate {
     595             :     UpdatedAccount execute(Repository allUsersRepo) throws IOException, ConfigInvalidException;
     596             :   }
     597             : 
     598             :   private class UpdatedAccount {
     599             :     final String message;
     600             :     final AccountConfig accountConfig;
     601             :     final CachedPreferences defaultPreferences;
     602             :     final boolean created;
     603             : 
     604             :     UpdatedAccount(
     605             :         String message,
     606             :         AccountConfig accountConfig,
     607             :         CachedPreferences defaultPreferences,
     608         151 :         boolean created) {
     609         151 :       checkState(!Strings.isNullOrEmpty(message), "message for account update must be set");
     610         151 :       this.message = requireNonNull(message);
     611         151 :       this.accountConfig = requireNonNull(accountConfig);
     612         151 :       this.defaultPreferences = defaultPreferences;
     613         151 :       this.created = created;
     614         151 :     }
     615             : 
     616             :     Optional<AccountState> getAccountState() throws IOException {
     617         151 :       return AccountState.fromAccountConfig(
     618             :           externalIds, accountConfig, externalIdNotes, defaultPreferences);
     619             :     }
     620             :   }
     621             : }

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