Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Commit

Permalink
[LWMeta] Add Conjure metadata types and conversion methods (#6653)
Browse files Browse the repository at this point in the history
[LWMeta] Add Conjure metadata types and conversion methods
  • Loading branch information
Toemmsche authored Jul 13, 2023
1 parent 43a048f commit 4f9b28d
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package com.palantir.util;

import com.fasterxml.jackson.annotation.JsonIgnoreType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.palantir.logsafe.Preconditions;
Expand Down Expand Up @@ -115,8 +114,7 @@ public static <K extends DeterministicHashable, V, R> Map<K, R> decode(
return keyToValue;
}

@VisibleForTesting
static <K extends DeterministicHashable> KeyListChecksum computeChecksum(
public static <K extends DeterministicHashable> KeyListChecksum computeChecksum(
ChecksumType checksumType, List<K> keyList) {
switch (checksumType) {
case CRC32_OF_DETERMINISTIC_HASHCODE: {
Expand Down
2 changes: 2 additions & 0 deletions lock-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ dependencies {
testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
testImplementation 'com.google.guava:guava'
testImplementation 'com.palantir.common:streams'
testImplementation 'com.palantir.safe-logging:preconditions-assertj'
testImplementation 'io.dropwizard.metrics:metrics-core'
testImplementation 'org.slf4j:slf4j-api'
testImplementation project(':atlasdb-api')
testImplementation project(':lock-api-objects')
testImplementation project(':lock-conjure-api:lock-conjure-api-objects')
testImplementation project(':timelock-api:timelock-api-objects')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* (c) Copyright 2023 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.lock.watch;

import com.google.common.annotations.VisibleForTesting;
import com.palantir.atlasdb.timelock.api.ConjureChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureCreatedChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureDeletedChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureLockDescriptorListChecksum;
import com.palantir.atlasdb.timelock.api.ConjureLockRequestMetadata;
import com.palantir.atlasdb.timelock.api.ConjureUnchangedChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureUpdatedChangeMetadata;
import com.palantir.common.streams.KeyedStream;
import com.palantir.conjure.java.lib.Bytes;
import com.palantir.lock.LockDescriptor;
import com.palantir.lock.watch.ChangeMetadata.Created;
import com.palantir.lock.watch.ChangeMetadata.Deleted;
import com.palantir.lock.watch.ChangeMetadata.Unchanged;
import com.palantir.lock.watch.ChangeMetadata.Updated;
import com.palantir.logsafe.SafeArg;
import com.palantir.logsafe.Unsafe;
import com.palantir.logsafe.logger.SafeLogger;
import com.palantir.logsafe.logger.SafeLoggerFactory;
import com.palantir.util.IndexEncodingUtils;
import com.palantir.util.IndexEncodingUtils.ChecksumType;
import com.palantir.util.IndexEncodingUtils.IndexEncodingResult;
import com.palantir.util.IndexEncodingUtils.KeyListChecksum;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.immutables.value.Value;

public final class ConjureLockRequestMetadataUtils {
private ConjureLockRequestMetadataUtils() {}

@VisibleForTesting
static final ChecksumType DEFAULT_CHECKSUM_TYPE = ChecksumType.CRC32_OF_DETERMINISTIC_HASHCODE;

private static final SafeLogger log = SafeLoggerFactory.get(ConjureLockRequestMetadataUtils.class);

public static ConjureMetadataConversionResult toConjureIndexEncoded(
Set<LockDescriptor> lockDescriptors, LockRequestMetadata metadata) {
IndexEncodingResult<LockDescriptor, ConjureChangeMetadata> encoded = IndexEncodingUtils.encode(
lockDescriptors,
metadata.lockDescriptorToChangeMetadata(),
changeMetadata -> changeMetadata.accept(ChangeMetadataToConjureVisitor.INSTANCE),
DEFAULT_CHECKSUM_TYPE);
KeyListChecksum checksum = encoded.keyListChecksum();
ConjureLockRequestMetadata conjureLockRequestMetadata = ConjureLockRequestMetadata.builder()
.indexToChangeMetadata(encoded.indexToValue())
.lockListChecksum(checksumToConjure(checksum))
.build();
return ImmutableConjureMetadataConversionResult.builder()
.lockList(encoded.keyList())
.conjureMetadata(conjureLockRequestMetadata)
.build();
}

public static LockRequestMetadata fromConjureIndexEncoded(ConjureMetadataConversionResult conversionResult) {
List<LockDescriptor> keyList = conversionResult.lockList();
ConjureLockRequestMetadata conjureMetadata = conversionResult.conjureMetadata();
KeyListChecksum checksum =
checksumFromConjure(conversionResult.conjureMetadata().getLockListChecksum());
IndexEncodingResult<LockDescriptor, ConjureChangeMetadata> encoded =
IndexEncodingResult.<LockDescriptor, ConjureChangeMetadata>builder()
.keyList(keyList)
.indexToValue(conjureMetadata.getIndexToChangeMetadata())
.keyListChecksum(checksum)
.build();
Map<LockDescriptor, Optional<ChangeMetadata>> optChangeMetadata = IndexEncodingUtils.decode(
encoded,
conjureChangeMetadata -> conjureChangeMetadata.accept(ChangeMetadataFromConjureVisitor.INSTANCE));
Map<LockDescriptor, ChangeMetadata> changeMetadata = KeyedStream.ofEntries(
optChangeMetadata.entrySet().stream())
.flatMap(Optional::stream)
.collectToMap();
return LockRequestMetadata.of(changeMetadata);
}

private static ConjureLockDescriptorListChecksum checksumToConjure(KeyListChecksum checksum) {
return ConjureLockDescriptorListChecksum.of(checksum.type().getId(), Bytes.from(checksum.value()));
}

private static KeyListChecksum checksumFromConjure(ConjureLockDescriptorListChecksum conjureChecksum) {
return KeyListChecksum.of(
ChecksumType.valueOf(conjureChecksum.getTypeId()),
conjureChecksum.getValue().asNewByteArray());
}

@Unsafe
@Value.Immutable
public interface ConjureMetadataConversionResult {
List<LockDescriptor> lockList();

ConjureLockRequestMetadata conjureMetadata();

static ImmutableConjureMetadataConversionResult.Builder builder() {
return ImmutableConjureMetadataConversionResult.builder();
}
}

private enum ChangeMetadataToConjureVisitor implements ChangeMetadata.Visitor<ConjureChangeMetadata> {
INSTANCE;

@Override
public ConjureChangeMetadata visit(Unchanged unchanged) {
return ConjureChangeMetadata.unchanged(ConjureUnchangedChangeMetadata.of());
}

@Override
public ConjureChangeMetadata visit(Updated updated) {
return ConjureChangeMetadata.updated(
ConjureUpdatedChangeMetadata.of(Bytes.from(updated.oldValue()), Bytes.from(updated.newValue())));
}

@Override
public ConjureChangeMetadata visit(Deleted deleted) {
return ConjureChangeMetadata.deleted(ConjureDeletedChangeMetadata.of(Bytes.from(deleted.oldValue())));
}

@Override
public ConjureChangeMetadata visit(Created created) {
return ConjureChangeMetadata.created(ConjureCreatedChangeMetadata.of(Bytes.from(created.newValue())));
}
}

private enum ChangeMetadataFromConjureVisitor implements ConjureChangeMetadata.Visitor<Optional<ChangeMetadata>> {
INSTANCE;

@Override
public Optional<ChangeMetadata> visitUnchanged(ConjureUnchangedChangeMetadata unchanged) {
return Optional.of(ChangeMetadata.unchanged());
}

@Override
public Optional<ChangeMetadata> visitUpdated(ConjureUpdatedChangeMetadata updated) {
return Optional.of(ChangeMetadata.updated(
updated.getOldValue().asNewByteArray(),
updated.getNewValue().asNewByteArray()));
}

@Override
public Optional<ChangeMetadata> visitDeleted(ConjureDeletedChangeMetadata deleted) {
return Optional.of(ChangeMetadata.deleted(deleted.getOldValue().asNewByteArray()));
}

@Override
public Optional<ChangeMetadata> visitCreated(ConjureCreatedChangeMetadata created) {
return Optional.of(ChangeMetadata.created(created.getNewValue().asNewByteArray()));
}

@Override
public Optional<ChangeMetadata> visitUnknown(String unknownType) {
log.trace(
"Encountered an unknown ConjureChangeMetadata type. This is likely a new type that was added in a"
+ " future version. This ChangeMetadata will be discarded",
SafeArg.of("unknownType", unknownType));
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* (c) Copyright 2023 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.lock.watch;

import static com.palantir.logsafe.testing.Assertions.assertThatLoggableExceptionThrownBy;
import static org.assertj.core.api.Assertions.assertThat;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.palantir.atlasdb.encoding.PtBytes;
import com.palantir.atlasdb.timelock.api.ConjureChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureCreatedChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureDeletedChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureLockDescriptorListChecksum;
import com.palantir.atlasdb.timelock.api.ConjureLockRequestMetadata;
import com.palantir.atlasdb.timelock.api.ConjureUnchangedChangeMetadata;
import com.palantir.atlasdb.timelock.api.ConjureUpdatedChangeMetadata;
import com.palantir.conjure.java.lib.Bytes;
import com.palantir.lock.LockDescriptor;
import com.palantir.lock.StringLockDescriptor;
import com.palantir.lock.watch.ConjureLockRequestMetadataUtils.ConjureMetadataConversionResult;
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException;
import com.palantir.util.IndexEncodingUtils;
import com.palantir.util.IndexEncodingUtils.KeyListChecksum;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.BeforeClass;
import org.junit.Test;

public class ConjureLockRequestMetadataUtilsTest {
private static final LockDescriptor LOCK_1 = StringLockDescriptor.of("lock1");
private static final LockDescriptor LOCK_2 = StringLockDescriptor.of("lock2");
private static final LockDescriptor LOCK_3 = StringLockDescriptor.of("lock3");
private static final LockDescriptor LOCK_4 = StringLockDescriptor.of("lock4");
private static final byte[] BYTES_OLD = PtBytes.toBytes("old");
private static final byte[] BYTES_NEW = PtBytes.toBytes("new");
private static final byte[] BYTES_DELETED = PtBytes.toBytes("deleted");
private static final byte[] BYTES_CREATED = PtBytes.toBytes("created");
private static final List<LockDescriptor> LOCK_LIST = ImmutableList.of(LOCK_1, LOCK_2, LOCK_3, LOCK_4);
// Although this is quite verbose, we explicitly want to test all possible types of change metadata and ensure
// that we do the conversion right for each of them.
private static final LockRequestMetadata LOCK_REQUEST_METADATA = LockRequestMetadata.of(ImmutableMap.of(
LOCK_1,
ChangeMetadata.unchanged(),
LOCK_2,
ChangeMetadata.updated(BYTES_OLD, BYTES_NEW),
LOCK_3,
ChangeMetadata.deleted(BYTES_DELETED),
LOCK_4,
ChangeMetadata.created(BYTES_CREATED)));
private static final Map<Integer, ConjureChangeMetadata> CONJURE_LOCKS_WITH_METADATA = ImmutableMap.of(
0,
ConjureChangeMetadata.unchanged(ConjureUnchangedChangeMetadata.of()),
1,
ConjureChangeMetadata.updated(
ConjureUpdatedChangeMetadata.of(Bytes.from(BYTES_OLD), Bytes.from(BYTES_NEW))),
2,
ConjureChangeMetadata.deleted(ConjureDeletedChangeMetadata.of(Bytes.from(BYTES_DELETED))),
3,
ConjureChangeMetadata.created(ConjureCreatedChangeMetadata.of(Bytes.from(BYTES_CREATED))));

private static ConjureMetadataConversionResult conjureMetadataConversionResult;

// This is a good candidate for a static construction method, but we would rather avoid testing internals of
// IndexEncodingUtils (checksum computation) within the tests for Conjure conversion.
@BeforeClass
public static void setup() {
KeyListChecksum checksum =
IndexEncodingUtils.computeChecksum(ConjureLockRequestMetadataUtils.DEFAULT_CHECKSUM_TYPE, LOCK_LIST);
ConjureLockRequestMetadata conjureMetadata = ConjureLockRequestMetadata.builder()
.indexToChangeMetadata(CONJURE_LOCKS_WITH_METADATA)
.lockListChecksum(
ConjureLockDescriptorListChecksum.of(checksum.type().getId(), Bytes.from(checksum.value())))
.build();
conjureMetadataConversionResult = ConjureMetadataConversionResult.builder()
.lockList(LOCK_LIST)
.conjureMetadata(conjureMetadata)
.build();
}

@Test
public void convertsToConjureCorrectly() {
// ImmutableSet remembers insertion order, which is critical here
assertThat(ConjureLockRequestMetadataUtils.toConjureIndexEncoded(
ImmutableSet.copyOf(LOCK_LIST), LOCK_REQUEST_METADATA))
.isEqualTo(conjureMetadataConversionResult);
}

@Test
public void convertsFromConjureCorrectly() {
assertThat(ConjureLockRequestMetadataUtils.fromConjureIndexEncoded(conjureMetadataConversionResult))
.isEqualTo(LOCK_REQUEST_METADATA);
}

@Test
public void canConvertSparseMetadata() {
List<LockDescriptor> lockDescriptors = IntStream.range(0, 10)
.mapToObj(Integer::toString)
.map(StringLockDescriptor::of)
.collect(Collectors.toList());
// Unique metadata on some locks, but not all
Map<LockDescriptor, ChangeMetadata> lockDescriptorToChangeMetadata = ImmutableMap.of(
lockDescriptors.get(0), ChangeMetadata.created(BYTES_CREATED),
lockDescriptors.get(4), ChangeMetadata.deleted(BYTES_DELETED),
lockDescriptors.get(9), ChangeMetadata.unchanged(),
lockDescriptors.get(5), ChangeMetadata.updated(BYTES_OLD, BYTES_NEW),
lockDescriptors.get(7), ChangeMetadata.unchanged());
LockRequestMetadata metadata = LockRequestMetadata.of(lockDescriptorToChangeMetadata);

assertThat(ConjureLockRequestMetadataUtils.fromConjureIndexEncoded(
ConjureLockRequestMetadataUtils.toConjureIndexEncoded(
ImmutableSet.copyOf(lockDescriptors), metadata)))
.isEqualTo(metadata);
}

@Test
public void changedLockOrderIsDetected() {
List<LockDescriptor> modifiedLockList = new ArrayList<>(LOCK_LIST);
Collections.swap(modifiedLockList, 0, 1);
ConjureMetadataConversionResult conversionResult = ImmutableConjureMetadataConversionResult.copyOf(
conjureMetadataConversionResult)
.withLockList(modifiedLockList);
assertThatLoggableExceptionThrownBy(
() -> ConjureLockRequestMetadataUtils.fromConjureIndexEncoded(conversionResult))
.isInstanceOf(SafeIllegalArgumentException.class)
.hasMessageStartingWith("Key list integrity check failed");
}

@Test
public void handlesEmptyData() {
assertThat(ConjureLockRequestMetadataUtils.fromConjureIndexEncoded(
ConjureLockRequestMetadataUtils.toConjureIndexEncoded(
ImmutableSet.of(), LockRequestMetadata.of(ImmutableMap.of()))))
.isEqualTo(LockRequestMetadata.of(ImmutableMap.of()));
}
}
Loading

0 comments on commit 4f9b28d

Please sign in to comment.