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 : }
|