// 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 java.util.stream.Collectors.toSet; 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.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.Set; 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, ImmutableSetMultimap<Account.Id, ExternalId>> 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) { 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 onRemoveByKeys( ObjectId oldNotesRev, ObjectId newNotesRev, Account.Id accountId, Collection<ExternalId.Key> extIdKeys) throws IOException { updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.get(accountId), extIdKeys)); } @Override public void onRemoveByKeys( ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys) throws IOException { updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.values(), extIdKeys)); } @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) { 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) { m.put(extId.accountId(), extId); } }); } @Override public void onReplaceByKeys( ObjectId oldNotesRev, ObjectId newNotesRev, Account.Id accountId, Collection<ExternalId.Key> toRemove, Collection<ExternalId> toAdd) throws IOException { ExternalIdsUpdate.checkSameAccount(toAdd, accountId); updateCache( oldNotesRev, newNotesRev, m -> { removeKeys(m.get(accountId), toRemove); for (ExternalId extId : toAdd) { m.put(extId.accountId(), extId); } }); } @Override public void onReplaceByKeys( ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> toRemove, Collection<ExternalId> toAdd) throws IOException { updateCache( oldNotesRev, newNotesRev, m -> { removeKeys(m.values(), toRemove); for (ExternalId extId : toAdd) { 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) { m.put(extId.accountId(), extId); } }); } @Override public Set<ExternalId> byAccount(Account.Id accountId) throws IOException { try { return extIdsByAccount.get(externalIdReader.readRevision()).get(accountId); } catch (ExecutionException e) { throw new IOException("Cannot list external ids by account", e); } } @Override public Set<ExternalId> byEmail(String email) throws IOException { try { return extIdsByAccount .get(externalIdReader.readRevision()) .values() .stream() .filter(e -> email.equals(e.email())) .collect(toSet()); } catch (ExecutionException e) { throw new IOException("Cannot list external ids by email", 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)); } else { m = MultimapBuilder.hashKeys().arrayListValues().build(); } update.accept(m); extIdsByAccount.put(newNotesRev, ImmutableSetMultimap.copyOf(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, ImmutableSetMultimap<Account.Id, ExternalId>> { private final ExternalIdReader externalIdReader; Loader(ExternalIdReader externalIdReader) { this.externalIdReader = externalIdReader; } @Override public ImmutableSetMultimap<Account.Id, ExternalId> load(ObjectId notesRev) throws Exception { Multimap<Account.Id, ExternalId> extIdsByAccount = MultimapBuilder.hashKeys().arrayListValues().build(); for (ExternalId extId : externalIdReader.all(notesRev)) { extIdsByAccount.put(extId.accountId(), extId); } return ImmutableSetMultimap.copyOf(extIdsByAccount); } } }