Files
gerrit/java/com/google/gerrit/server/schema/GroupBundle.java
Edwin Kempin 6b4d94b60f Write debug log that shows up in trace when meta data file is read/saved
Reading/saving meta data files is a rather expensive operation. E.g. if
a request leads to excessive reads of meta data files we want to see
that in the trace. Reading of meta data files e.g. happens on cache
misses (account cache, group cache etc).

Unfortunately VersionedMetaData has no reference to the project name
so we must pass this in from a lot of places.

Example logs:
[2018-08-10 15:27:29,333] [HTTP-75] DEBUG com.google.gerrit.server.git.meta.VersionedMetaData : Read file 'group.config' from ref 'refs/groups/ca/ca1fd42646e71d8081add52fbb0171a8504c97cd' of project 'All-Users' from revision '919687c42e13f76552fb186f36d43f447544e64b' [CONTEXT forced=true TRACE_ID="1533907649289-2d0c7a5d" ]
[2018-08-10 15:27:29,334] [HTTP-75] DEBUG com.google.gerrit.server.git.meta.VersionedMetaData : Read file 'members' from ref 'refs/groups/ca/ca1fd42646e71d8081add52fbb0171a8504c97cd' of project 'All-Users' from revision '919687c42e13f76552fb186f36d43f447544e64b' [CONTEXT forced=true TRACE_ID="1533907649289-2d0c7a5d" ]
[2018-08-10 15:27:29,336] [HTTP-75] DEBUG com.google.gerrit.server.git.meta.VersionedMetaData : Read file 'subgroups' from ref 'refs/groups/ca/ca1fd42646e71d8081add52fbb0171a8504c97cd' of project 'All-Users' from revision '919687c42e13f76552fb186f36d43f447544e64b' [CONTEXT forced=true TRACE_ID="1533907649289-2d0c7a5d" ]

Change-Id: Ibb438213b01b0a5cf67fd277d298a0359b65bb1d
Signed-off-by: Edwin Kempin <ekempin@google.com>
2018-08-16 13:18:24 +02:00

779 lines
30 KiB
Java

// Copyright (C) 2017 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.schema;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsLast;
import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupById;
import com.google.gerrit.reviewdb.client.AccountGroupByIdAud;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.AuditLogReader;
import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
/**
* A bundle of all entities rooted at a single {@link AccountGroup} entity.
*
* <p>Used primarily during the migration process. Most callers should prefer {@link InternalGroup}
* instead.
*/
@AutoValue
abstract class GroupBundle {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
static {
// Initialization-time checks that the column set hasn't changed since the
// last time this file was updated.
checkColumns(AccountGroup.NameKey.class, 1);
checkColumns(AccountGroup.UUID.class, 1);
checkColumns(AccountGroup.Id.class, 1);
checkColumns(AccountGroup.class, 1, 2, 4, 7, 9, 10, 11);
checkColumns(AccountGroupById.Key.class, 1, 2);
checkColumns(AccountGroupById.class, 1);
checkColumns(AccountGroupByIdAud.Key.class, 1, 2, 3);
checkColumns(AccountGroupByIdAud.class, 1, 2, 3, 4);
checkColumns(AccountGroupMember.Key.class, 1, 2);
checkColumns(AccountGroupMember.class, 1);
checkColumns(AccountGroupMemberAudit.Key.class, 1, 2, 3);
checkColumns(AccountGroupMemberAudit.class, 1, 2, 3, 4);
}
public enum Source {
REVIEW_DB("ReviewDb"),
NOTE_DB("NoteDb");
private final String name;
private Source(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
@Singleton
public static class Factory {
private final AuditLogReader auditLogReader;
@Inject
Factory(AuditLogReader auditLogReader) {
this.auditLogReader = auditLogReader;
}
public GroupBundle fromNoteDb(
Project.NameKey projectName, Repository repo, AccountGroup.UUID uuid)
throws ConfigInvalidException, IOException {
GroupConfig groupConfig = GroupConfig.loadForGroup(projectName, repo, uuid);
InternalGroup internalGroup = groupConfig.getLoadedGroup().get();
AccountGroup.Id groupId = internalGroup.getId();
AccountGroup accountGroup =
new AccountGroup(
internalGroup.getNameKey(),
internalGroup.getId(),
internalGroup.getGroupUUID(),
internalGroup.getCreatedOn());
accountGroup.setDescription(internalGroup.getDescription());
accountGroup.setOwnerGroupUUID(internalGroup.getOwnerGroupUUID());
accountGroup.setVisibleToAll(internalGroup.isVisibleToAll());
return create(
Source.NOTE_DB,
accountGroup,
internalGroup
.getMembers()
.stream()
.map(
accountId ->
new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId)))
.collect(toImmutableSet()),
auditLogReader.getMembersAudit(repo, uuid),
internalGroup
.getSubgroups()
.stream()
.map(
subgroupUuid ->
new AccountGroupById(new AccountGroupById.Key(groupId, subgroupUuid)))
.collect(toImmutableSet()),
auditLogReader.getSubgroupsAudit(repo, uuid));
}
public static GroupBundle fromReviewDb(ReviewDb db, AccountGroup.UUID groupUuid)
throws OrmException {
JdbcSchema jdbcSchema = ReviewDbWrapper.unwrapJbdcSchema(db);
AccountGroup group = readAccountGroupFromReviewDb(jdbcSchema, groupUuid);
AccountGroup.Id groupId = group.getId();
return create(
Source.REVIEW_DB,
group,
readAccountGroupMembersFromReviewDb(jdbcSchema, groupId),
readAccountGroupMemberAuditsFromReviewDb(jdbcSchema, groupId),
readAccountGroupSubgroupsFromReviewDb(jdbcSchema, groupId),
readAccountGroupSubgroupAuditsFromReviewDb(jdbcSchema, groupId));
}
private static AccountGroup readAccountGroupFromReviewDb(
JdbcSchema jdbcSchema, AccountGroup.UUID groupUuid) throws OrmException {
try (Statement stmt = jdbcSchema.getConnection().createStatement();
ResultSet rs =
stmt.executeQuery(
"SELECT group_id,"
+ " name,"
+ " created_on,"
+ " description,"
+ " owner_group_uuid,"
+ " visible_to_all"
+ " FROM account_groups"
+ " WHERE group_uuid = '"
+ groupUuid.get()
+ "'")) {
if (!rs.next()) {
throw new OrmException(String.format("Group %s not found", groupUuid));
}
AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
AccountGroup.NameKey groupName = new AccountGroup.NameKey(rs.getString(2));
Timestamp createdOn = rs.getTimestamp(3);
String description = rs.getString(4);
AccountGroup.UUID ownerGroupUuid = new AccountGroup.UUID(rs.getString(5));
boolean visibleToAll = "Y".equals(rs.getString(6));
AccountGroup group = new AccountGroup(groupName, groupId, groupUuid, createdOn);
group.setDescription(description);
group.setOwnerGroupUUID(ownerGroupUuid);
group.setVisibleToAll(visibleToAll);
if (rs.next()) {
throw new OrmException(String.format("Group UUID %s is ambiguous", groupUuid));
}
return group;
} catch (SQLException e) {
throw new OrmException(
String.format("Failed to read account group %s from ReviewDb", groupUuid.get()), e);
}
}
private static List<AccountGroupMember> readAccountGroupMembersFromReviewDb(
JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
try (Statement stmt = jdbcSchema.getConnection().createStatement();
ResultSet rs =
stmt.executeQuery(
"SELECT account_id"
+ " FROM account_group_members"
+ " WHERE group_id = '"
+ groupId.get()
+ "'")) {
List<AccountGroupMember> members = new ArrayList<>();
while (rs.next()) {
Account.Id accountId = new Account.Id(rs.getInt(1));
members.add(new AccountGroupMember(new AccountGroupMember.Key(accountId, groupId)));
}
return members;
} catch (SQLException e) {
throw new OrmException(
String.format(
"Failed to read members of account group %s from ReviewDb", groupId.get()),
e);
}
}
private static List<AccountGroupMemberAudit> readAccountGroupMemberAuditsFromReviewDb(
JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
try (Statement stmt = jdbcSchema.getConnection().createStatement();
ResultSet rs =
stmt.executeQuery(
"SELECT account_id, added_by, added_on, removed_by, removed_on"
+ " FROM account_group_members_audit"
+ " WHERE group_id = '"
+ groupId.get()
+ "'")) {
List<AccountGroupMemberAudit> audits = new ArrayList<>();
while (rs.next()) {
Account.Id accountId = new Account.Id(rs.getInt(1));
Account.Id addedBy = new Account.Id(rs.getInt(2));
Timestamp addedOn = rs.getTimestamp(3);
Timestamp removedOn = rs.getTimestamp(5);
Account.Id removedBy = removedOn != null ? new Account.Id(rs.getInt(4)) : null;
AccountGroupMemberAudit.Key key =
new AccountGroupMemberAudit.Key(accountId, groupId, addedOn);
AccountGroupMemberAudit audit = new AccountGroupMemberAudit(key, addedBy);
audit.removed(removedBy, removedOn);
audits.add(audit);
}
return audits;
} catch (SQLException e) {
throw new OrmException(
String.format(
"Failed to read member audits of account group %s from ReviewDb", groupId.get()),
e);
}
}
private static List<AccountGroupById> readAccountGroupSubgroupsFromReviewDb(
JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
try (Statement stmt = jdbcSchema.getConnection().createStatement();
ResultSet rs =
stmt.executeQuery(
"SELECT include_uuid"
+ " FROM account_group_by_id"
+ " WHERE group_id = '"
+ groupId.get()
+ "'")) {
List<AccountGroupById> subgroups = new ArrayList<>();
while (rs.next()) {
AccountGroup.UUID includedGroupUuid = new AccountGroup.UUID(rs.getString(1));
subgroups.add(new AccountGroupById(new AccountGroupById.Key(groupId, includedGroupUuid)));
}
return subgroups;
} catch (SQLException e) {
throw new OrmException(
String.format(
"Failed to read subgroups of account group %s from ReviewDb", groupId.get()),
e);
}
}
private static List<AccountGroupByIdAud> readAccountGroupSubgroupAuditsFromReviewDb(
JdbcSchema jdbcSchema, AccountGroup.Id groupId) throws OrmException {
try (Statement stmt = jdbcSchema.getConnection().createStatement();
ResultSet rs =
stmt.executeQuery(
"SELECT include_uuid, added_by, added_on, removed_by, removed_on"
+ " FROM account_group_by_id_aud"
+ " WHERE group_id = '"
+ groupId.get()
+ "'")) {
List<AccountGroupByIdAud> audits = new ArrayList<>();
while (rs.next()) {
AccountGroup.UUID includedGroupUuid = new AccountGroup.UUID(rs.getString(1));
Account.Id addedBy = new Account.Id(rs.getInt(2));
Timestamp addedOn = rs.getTimestamp(3);
Timestamp removedOn = rs.getTimestamp(5);
Account.Id removedBy = removedOn != null ? new Account.Id(rs.getInt(4)) : null;
AccountGroupByIdAud.Key key =
new AccountGroupByIdAud.Key(groupId, includedGroupUuid, addedOn);
AccountGroupByIdAud audit = new AccountGroupByIdAud(key, addedBy);
audit.removed(removedBy, removedOn);
audits.add(audit);
}
return audits;
} catch (SQLException e) {
throw new OrmException(
String.format(
"Failed to read subgroup audits of account group %s from ReviewDb", groupId.get()),
e);
}
}
}
private static final Comparator<AccountGroupMember> ACCOUNT_GROUP_MEMBER_COMPARATOR =
Comparator.comparingInt((AccountGroupMember m) -> m.getAccountGroupId().get())
.thenComparingInt(m -> m.getAccountId().get());
private static final Comparator<AccountGroupMemberAudit> ACCOUNT_GROUP_MEMBER_AUDIT_COMPARATOR =
Comparator.comparingInt((AccountGroupMemberAudit a) -> a.getGroupId().get())
.thenComparing(AccountGroupMemberAudit::getAddedOn)
.thenComparingInt(a -> a.getAddedBy().get())
.thenComparingInt(a -> a.getMemberId().get())
.thenComparing(
a -> a.getRemovedBy() != null ? a.getRemovedBy().get() : null,
nullsLast(naturalOrder()))
.thenComparing(AccountGroupMemberAudit::getRemovedOn, nullsLast(naturalOrder()));
private static final Comparator<AccountGroupById> ACCOUNT_GROUP_BY_ID_COMPARATOR =
Comparator.comparingInt((AccountGroupById m) -> m.getGroupId().get())
.thenComparing(AccountGroupById::getIncludeUUID);
private static final Comparator<AccountGroupByIdAud> ACCOUNT_GROUP_BY_ID_AUD_COMPARATOR =
Comparator.comparingInt((AccountGroupByIdAud a) -> a.getGroupId().get())
.thenComparing(AccountGroupByIdAud::getAddedOn)
.thenComparingInt(a -> a.getAddedBy().get())
.thenComparing(AccountGroupByIdAud::getIncludeUUID)
.thenComparing(
a -> a.getRemovedBy() != null ? a.getRemovedBy().get() : null,
nullsLast(naturalOrder()))
.thenComparing(AccountGroupByIdAud::getRemovedOn, nullsLast(naturalOrder()));
private static final Comparator<AuditEntry> AUDIT_ENTRY_COMPARATOR =
Comparator.comparing(AuditEntry::getTimestamp)
.thenComparing(AuditEntry::getAction, Comparator.comparingInt(Action::getOrder));
public static GroupBundle create(
Source source,
AccountGroup group,
Iterable<AccountGroupMember> members,
Iterable<AccountGroupMemberAudit> memberAudit,
Iterable<AccountGroupById> byId,
Iterable<AccountGroupByIdAud> byIdAudit) {
AccountGroup.UUID uuid = group.getGroupUUID();
return new AutoValue_GroupBundle.Builder()
.source(source)
.group(group)
.members(
logIfNotUnique(
source, uuid, members, ACCOUNT_GROUP_MEMBER_COMPARATOR, AccountGroupMember.class))
.memberAudit(
logIfNotUnique(
source,
uuid,
memberAudit,
ACCOUNT_GROUP_MEMBER_AUDIT_COMPARATOR,
AccountGroupMemberAudit.class))
.byId(
logIfNotUnique(
source, uuid, byId, ACCOUNT_GROUP_BY_ID_COMPARATOR, AccountGroupById.class))
.byIdAudit(
logIfNotUnique(
source,
uuid,
byIdAudit,
ACCOUNT_GROUP_BY_ID_AUD_COMPARATOR,
AccountGroupByIdAud.class))
.build();
}
private static <T> ImmutableSet<T> logIfNotUnique(
Source source,
AccountGroup.UUID uuid,
Iterable<T> iterable,
Comparator<T> comparator,
Class<T> clazz) {
List<T> list = Streams.stream(iterable).sorted(comparator).collect(toList());
ImmutableSet<T> set = ImmutableSet.copyOf(list);
if (set.size() != list.size()) {
// One way this can happen is that distinct audit entities can compare equal, because
// AccountGroup{MemberAudit,ByIdAud}.Key does not include the addedOn timestamp in its
// members() list. However, this particular issue only applies to pure adds, since removedOn
// *is* included in equality. As a result, if this happens, it means the audit log is already
// corrupt, and it's not clear if we can programmatically repair it. For migrating to NoteDb,
// we'll try our best to recreate it, but no guarantees it will match the real sequence of
// attempted operations, which is in any case lost in the mists of time.
logger.atWarning().log(
"group %s in %s has duplicate %s entities: %s",
uuid, source, clazz.getSimpleName(), iterable);
}
return set;
}
static Builder builder() {
return new AutoValue_GroupBundle.Builder().members().memberAudit().byId().byIdAudit();
}
public static ImmutableList<String> compareWithAudits(
GroupBundle reviewDbBundle, GroupBundle noteDbBundle) {
return compare(reviewDbBundle, noteDbBundle, true);
}
public static ImmutableList<String> compareWithoutAudits(
GroupBundle reviewDbBundle, GroupBundle noteDbBundle) {
return compare(reviewDbBundle, noteDbBundle, false);
}
private static ImmutableList<String> compare(
GroupBundle reviewDbBundle, GroupBundle noteDbBundle, boolean compareAudits) {
// Normalize the ReviewDb bundle to what we expect in NoteDb. This means that values in error
// messages will not reflect the actual data in ReviewDb, but it will make it easier for humans
// to see the difference.
reviewDbBundle = reviewDbBundle.truncateToSecond();
AccountGroup reviewDbGroup = new AccountGroup(reviewDbBundle.group());
reviewDbGroup.setDescription(Strings.emptyToNull(reviewDbGroup.getDescription()));
reviewDbBundle = reviewDbBundle.toBuilder().group(reviewDbGroup).build();
checkArgument(
reviewDbBundle.source() == Source.REVIEW_DB,
"first bundle's source must be %s: %s",
Source.REVIEW_DB,
reviewDbBundle);
checkArgument(
noteDbBundle.source() == Source.NOTE_DB,
"second bundle's source must be %s: %s",
Source.NOTE_DB,
noteDbBundle);
ImmutableList.Builder<String> result = ImmutableList.builder();
if (!reviewDbBundle.group().equals(noteDbBundle.group())) {
result.add(
"AccountGroups differ\n"
+ ("ReviewDb: " + reviewDbBundle.group() + "\n")
+ ("NoteDb : " + noteDbBundle.group()));
}
if (!reviewDbBundle.members().equals(noteDbBundle.members())) {
result.add(
"AccountGroupMembers differ\n"
+ ("ReviewDb: " + reviewDbBundle.members() + "\n")
+ ("NoteDb : " + noteDbBundle.members()));
}
if (compareAudits
&& !areMemberAuditsConsideredEqual(
reviewDbBundle.memberAudit(), noteDbBundle.memberAudit())) {
result.add(
"AccountGroupMemberAudits differ\n"
+ ("ReviewDb: " + reviewDbBundle.memberAudit() + "\n")
+ ("NoteDb : " + noteDbBundle.memberAudit()));
}
if (!reviewDbBundle.byId().equals(noteDbBundle.byId())) {
result.add(
"AccountGroupByIds differ\n"
+ ("ReviewDb: " + reviewDbBundle.byId() + "\n")
+ ("NoteDb : " + noteDbBundle.byId()));
}
if (compareAudits
&& !areByIdAuditsConsideredEqual(reviewDbBundle.byIdAudit(), noteDbBundle.byIdAudit())) {
result.add(
"AccountGroupByIdAudits differ\n"
+ ("ReviewDb: " + reviewDbBundle.byIdAudit() + "\n")
+ ("NoteDb : " + noteDbBundle.byIdAudit()));
}
return result.build();
}
private static boolean areMemberAuditsConsideredEqual(
ImmutableSet<AccountGroupMemberAudit> reviewDbMemberAudits,
ImmutableSet<AccountGroupMemberAudit> noteDbMemberAudits) {
ListMultimap<String, AuditEntry> reviewDbMemberAuditsByMemberId =
toMemberAuditEntriesByMemberId(reviewDbMemberAudits);
ListMultimap<String, AuditEntry> noteDbMemberAuditsByMemberId =
toMemberAuditEntriesByMemberId(noteDbMemberAudits);
return areConsideredEqual(reviewDbMemberAuditsByMemberId, noteDbMemberAuditsByMemberId);
}
private static boolean areByIdAuditsConsideredEqual(
ImmutableSet<AccountGroupByIdAud> reviewDbByIdAudits,
ImmutableSet<AccountGroupByIdAud> noteDbByIdAudits) {
ListMultimap<String, AuditEntry> reviewDbByIdAuditsById =
toByIdAuditEntriesById(reviewDbByIdAudits);
ListMultimap<String, AuditEntry> noteDbByIdAuditsById =
toByIdAuditEntriesById(noteDbByIdAudits);
return areConsideredEqual(reviewDbByIdAuditsById, noteDbByIdAuditsById);
}
private static ListMultimap<String, AuditEntry> toMemberAuditEntriesByMemberId(
ImmutableSet<AccountGroupMemberAudit> memberAudits) {
return memberAudits
.stream()
.flatMap(GroupBundle::toAuditEntries)
.collect(
Multimaps.toMultimap(
AuditEntry::getTarget,
Function.identity(),
MultimapBuilder.hashKeys().arrayListValues()::build));
}
private static Stream<AuditEntry> toAuditEntries(AccountGroupMemberAudit memberAudit) {
AuditEntry additionAuditEntry =
AuditEntry.create(
Action.ADD,
memberAudit.getAddedBy(),
memberAudit.getMemberId(),
memberAudit.getAddedOn());
if (memberAudit.isActive()) {
return Stream.of(additionAuditEntry);
}
AuditEntry removalAuditEntry =
AuditEntry.create(
Action.REMOVE,
memberAudit.getRemovedBy(),
memberAudit.getMemberId(),
memberAudit.getRemovedOn());
return Stream.of(additionAuditEntry, removalAuditEntry);
}
private static ListMultimap<String, AuditEntry> toByIdAuditEntriesById(
ImmutableSet<AccountGroupByIdAud> byIdAudits) {
return byIdAudits
.stream()
.flatMap(GroupBundle::toAuditEntries)
.collect(
Multimaps.toMultimap(
AuditEntry::getTarget,
Function.identity(),
MultimapBuilder.hashKeys().arrayListValues()::build));
}
private static Stream<AuditEntry> toAuditEntries(AccountGroupByIdAud byIdAudit) {
AuditEntry additionAuditEntry =
AuditEntry.create(
Action.ADD, byIdAudit.getAddedBy(), byIdAudit.getIncludeUUID(), byIdAudit.getAddedOn());
if (byIdAudit.isActive()) {
return Stream.of(additionAuditEntry);
}
AuditEntry removalAuditEntry =
AuditEntry.create(
Action.REMOVE,
byIdAudit.getRemovedBy(),
byIdAudit.getIncludeUUID(),
byIdAudit.getRemovedOn());
return Stream.of(additionAuditEntry, removalAuditEntry);
}
/**
* Determines whether the audit log entries are equal except for redundant entries. Entries of the
* same type (addition/removal) which follow directly on each other according to their timestamp
* are considered redundant.
*/
private static boolean areConsideredEqual(
ListMultimap<String, AuditEntry> reviewDbMemberAuditsByTarget,
ListMultimap<String, AuditEntry> noteDbMemberAuditsByTarget) {
for (String target : reviewDbMemberAuditsByTarget.keySet()) {
ImmutableList<AuditEntry> reviewDbAuditEntries =
reviewDbMemberAuditsByTarget
.get(target)
.stream()
.sorted(AUDIT_ENTRY_COMPARATOR)
.collect(toImmutableList());
ImmutableSet<AuditEntry> noteDbAuditEntries =
noteDbMemberAuditsByTarget
.get(target)
.stream()
.sorted(AUDIT_ENTRY_COMPARATOR)
.collect(toImmutableSet());
int reviewDbIndex = 0;
for (AuditEntry noteDbAuditEntry : noteDbAuditEntries) {
Set<AuditEntry> redundantReviewDbAuditEntries = new HashSet<>();
while (reviewDbIndex < reviewDbAuditEntries.size()) {
AuditEntry reviewDbAuditEntry = reviewDbAuditEntries.get(reviewDbIndex);
if (!reviewDbAuditEntry.getAction().equals(noteDbAuditEntry.getAction())) {
break;
}
redundantReviewDbAuditEntries.add(reviewDbAuditEntry);
reviewDbIndex++;
}
// The order of the entries is not perfect as ReviewDb included milliseconds for timestamps
// and we cut off everything below seconds due to NoteDb/git. Consequently, we don't have a
// way to know in this method in which exact order additions/removals within the same second
// happened. The best we can do is to group all additions within the same second as
// redundant entries and the removals afterward. To compensate that we possibly group
// non-redundant additions/removals, we also accept NoteDb audit entries which just occur
// anywhere as ReviewDb audit entries.
if (!redundantReviewDbAuditEntries.contains(noteDbAuditEntry)
&& !reviewDbAuditEntries.contains(noteDbAuditEntry)) {
return false;
}
}
if (reviewDbIndex < reviewDbAuditEntries.size()) {
// Some of the ReviewDb audit log entries aren't matched by NoteDb audit log entries.
return false;
}
}
return true;
}
public AccountGroup.Id id() {
return group().getId();
}
public AccountGroup.UUID uuid() {
return group().getGroupUUID();
}
public abstract Source source();
public abstract AccountGroup group();
public abstract ImmutableSet<AccountGroupMember> members();
public abstract ImmutableSet<AccountGroupMemberAudit> memberAudit();
public abstract ImmutableSet<AccountGroupById> byId();
public abstract ImmutableSet<AccountGroupByIdAud> byIdAudit();
public abstract Builder toBuilder();
public GroupBundle truncateToSecond() {
AccountGroup newGroup = new AccountGroup(group());
if (newGroup.getCreatedOn() != null) {
newGroup.setCreatedOn(TimeUtil.truncateToSecond(newGroup.getCreatedOn()));
}
return toBuilder()
.group(newGroup)
.memberAudit(
memberAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet()))
.byIdAudit(
byIdAudit().stream().map(GroupBundle::truncateToSecond).collect(toImmutableSet()))
.build();
}
private static AccountGroupMemberAudit truncateToSecond(AccountGroupMemberAudit a) {
AccountGroupMemberAudit result =
new AccountGroupMemberAudit(
new AccountGroupMemberAudit.Key(
a.getKey().getParentKey(),
a.getKey().getGroupId(),
TimeUtil.truncateToSecond(a.getKey().getAddedOn())),
a.getAddedBy());
if (a.getRemovedOn() != null) {
result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn()));
}
return result;
}
private static AccountGroupByIdAud truncateToSecond(AccountGroupByIdAud a) {
AccountGroupByIdAud result =
new AccountGroupByIdAud(
new AccountGroupByIdAud.Key(
a.getKey().getParentKey(),
a.getKey().getIncludeUUID(),
TimeUtil.truncateToSecond(a.getKey().getAddedOn())),
a.getAddedBy());
if (a.getRemovedOn() != null) {
result.removed(a.getRemovedBy(), TimeUtil.truncateToSecond(a.getRemovedOn()));
}
return result;
}
public InternalGroup toInternalGroup() {
return InternalGroup.create(
group(),
members().stream().map(AccountGroupMember::getAccountId).collect(toImmutableSet()),
byId().stream().map(AccountGroupById::getIncludeUUID).collect(toImmutableSet()));
}
@Override
public int hashCode() {
throw new UnsupportedOperationException(
"hashCode is not supported because equals is not supported");
}
@Override
public boolean equals(Object o) {
throw new UnsupportedOperationException("Use GroupBundle.compare(a, b) instead of equals");
}
@AutoValue
abstract static class AuditEntry {
private static AuditEntry create(
Action action, Account.Id userId, Account.Id memberId, Timestamp timestamp) {
return new AutoValue_GroupBundle_AuditEntry(
action, userId, String.valueOf(memberId.get()), timestamp);
}
private static AuditEntry create(
Action action, Account.Id userId, AccountGroup.UUID subgroupId, Timestamp timestamp) {
return new AutoValue_GroupBundle_AuditEntry(action, userId, subgroupId.get(), timestamp);
}
abstract Action getAction();
abstract Account.Id getUserId();
abstract String getTarget();
abstract Timestamp getTimestamp();
}
enum Action {
ADD(1),
REMOVE(2);
private final int order;
Action(int order) {
this.order = order;
}
public int getOrder() {
return order;
}
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder source(Source source);
abstract Builder group(AccountGroup group);
abstract Builder members(AccountGroupMember... member);
abstract Builder members(Iterable<AccountGroupMember> member);
abstract Builder memberAudit(AccountGroupMemberAudit... audit);
abstract Builder memberAudit(Iterable<AccountGroupMemberAudit> audit);
abstract Builder byId(AccountGroupById... byId);
abstract Builder byId(Iterable<AccountGroupById> byId);
abstract Builder byIdAudit(AccountGroupByIdAud... audit);
abstract Builder byIdAudit(Iterable<AccountGroupByIdAud> audit);
abstract GroupBundle build();
}
}