summaryrefslogtreecommitdiffstats
path: root/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java')
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java262
1 files changed, 262 insertions, 0 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
new file mode 100644
index 0000000000..4b4d060365
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -0,0 +1,262 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Caches external IDs of all accounts. The external IDs are always loaded from NoteDb. */
+@Singleton
+class ExternalIdCacheImpl implements ExternalIdCache {
+ private static final Logger log = LoggerFactory.getLogger(ExternalIdCacheImpl.class);
+
+ private final LoadingCache<ObjectId, AllExternalIds> extIdsByAccount;
+ private final ExternalIdReader externalIdReader;
+ private final Lock lock;
+
+ @Inject
+ ExternalIdCacheImpl(ExternalIdReader externalIdReader) {
+ this.extIdsByAccount =
+ CacheBuilder.newBuilder()
+ // The cached data is potentially pretty large and we are always only interested
+ // in the latest value, hence the maximum cache size is set to 1.
+ // This can lead to extra cache loads in case of the following race:
+ // 1. thread 1 reads the notes ref at revision A
+ // 2. thread 2 updates the notes ref to revision B and stores the derived value
+ // for B in the cache
+ // 3. thread 1 attempts to read the data for revision A from the cache, and misses
+ // 4. later threads attempt to read at B
+ // In this race unneeded reloads are done in step 3 (reload from revision A) and
+ // step 4 (reload from revision B, because the value for revision B was lost when the
+ // reload from revision A was done, since the cache can hold only one entry).
+ // These reloads could be avoided by increasing the cache size to 2. However the race
+ // window between reading the ref and looking it up in the cache is small so that
+ // it's rare that this race happens. Therefore it's not worth to double the memory
+ // usage of this cache, just to avoid this.
+ .maximumSize(1)
+ .build(new Loader(externalIdReader));
+ this.externalIdReader = externalIdReader;
+ this.lock = new ReentrantLock(true /* fair */);
+ }
+
+ @Override
+ public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : extIds) {
+ extId.checkThatBlobIdIsSet();
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : extIds) {
+ m.remove(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onUpdate(
+ ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> updatedExtIds)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ removeKeys(m.values(), updatedExtIds.stream().map(e -> e.key()).collect(toSet()));
+ for (ExternalId updatedExtId : updatedExtIds) {
+ updatedExtId.checkThatBlobIdIsSet();
+ m.put(updatedExtId.accountId(), updatedExtId);
+ }
+ });
+ }
+
+ @Override
+ public void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException {
+ ExternalIdsUpdate.checkSameAccount(Iterables.concat(toRemove, toAdd), accountId);
+
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : toRemove) {
+ m.remove(extId.accountId(), extId);
+ }
+ for (ExternalId extId : toAdd) {
+ extId.checkThatBlobIdIsSet();
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : toRemove) {
+ m.remove(extId.accountId(), extId);
+ }
+ for (ExternalId extId : toAdd) {
+ extId.checkThatBlobIdIsSet();
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException {
+ return get().byAccount().get(accountId);
+ }
+
+ @Override
+ public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
+ return get().byAccount();
+ }
+
+ @Override
+ public ImmutableSetMultimap<String, ExternalId> byEmails(String... emails) throws IOException {
+ AllExternalIds allExternalIds = get();
+ ImmutableSetMultimap.Builder<String, ExternalId> byEmails = ImmutableSetMultimap.builder();
+ for (String email : emails) {
+ byEmails.putAll(email, allExternalIds.byEmail().get(email));
+ }
+ return byEmails.build();
+ }
+
+ @Override
+ public ImmutableSetMultimap<String, ExternalId> allByEmail() throws IOException {
+ return get().byEmail();
+ }
+
+ private AllExternalIds get() throws IOException {
+ try {
+ return extIdsByAccount.get(externalIdReader.readRevision());
+ } catch (ExecutionException e) {
+ throw new IOException("Cannot load external ids", e);
+ }
+ }
+
+ private void updateCache(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Consumer<Multimap<Account.Id, ExternalId>> update) {
+ lock.lock();
+ try {
+ ListMultimap<Account.Id, ExternalId> m;
+ if (!ObjectId.zeroId().equals(oldNotesRev)) {
+ m =
+ MultimapBuilder.hashKeys()
+ .arrayListValues()
+ .build(extIdsByAccount.get(oldNotesRev).byAccount());
+ } else {
+ m = MultimapBuilder.hashKeys().arrayListValues().build();
+ }
+ update.accept(m);
+ extIdsByAccount.put(newNotesRev, AllExternalIds.create(m));
+ } catch (ExecutionException e) {
+ log.warn("Cannot update external IDs", e);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private static void removeKeys(Collection<ExternalId> ids, Collection<ExternalId.Key> toRemove) {
+ Collections2.transform(ids, e -> e.key()).removeAll(toRemove);
+ }
+
+ private static class Loader extends CacheLoader<ObjectId, AllExternalIds> {
+ private final ExternalIdReader externalIdReader;
+
+ Loader(ExternalIdReader externalIdReader) {
+ this.externalIdReader = externalIdReader;
+ }
+
+ @Override
+ public AllExternalIds load(ObjectId notesRev) throws Exception {
+ Multimap<Account.Id, ExternalId> extIdsByAccount =
+ MultimapBuilder.hashKeys().arrayListValues().build();
+ for (ExternalId extId : externalIdReader.all(notesRev)) {
+ extId.checkThatBlobIdIsSet();
+ extIdsByAccount.put(extId.accountId(), extId);
+ }
+ return AllExternalIds.create(extIdsByAccount);
+ }
+ }
+
+ @AutoValue
+ abstract static class AllExternalIds {
+ static AllExternalIds create(Multimap<Account.Id, ExternalId> byAccount) {
+ ImmutableSetMultimap<String, ExternalId> byEmail =
+ byAccount.values().stream()
+ .filter(e -> !Strings.isNullOrEmpty(e.email()))
+ .collect(toImmutableSetMultimap(ExternalId::email, e -> e));
+ return new AutoValue_ExternalIdCacheImpl_AllExternalIds(
+ ImmutableSetMultimap.copyOf(byAccount), byEmail);
+ }
+
+ public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount();
+
+ public abstract ImmutableSetMultimap<String, ExternalId> byEmail();
+ }
+}