Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to update the repository description #7376

Merged
merged 10 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.projectnessie.versioned.storage.common.exceptions.RefNotFoundException;
import org.projectnessie.versioned.storage.common.exceptions.RetryTimeoutException;
import org.projectnessie.versioned.storage.common.logic.CommitLogic;
import org.projectnessie.versioned.storage.common.logic.CreateCommit;
import org.projectnessie.versioned.storage.common.logic.ImmutableRepositoryDescription;
import org.projectnessie.versioned.storage.common.logic.InternalRef;
import org.projectnessie.versioned.storage.common.logic.ReferenceLogic;
import org.projectnessie.versioned.storage.common.logic.RepositoryDescription;
Expand Down Expand Up @@ -234,4 +236,33 @@ public void internalRefsAndDefaultBranch(String defaultBranchName) {
soft.assertThat(newHashSet(referenceLogic.queryReferences(referencesQuery())))
.isEqualTo(refQuery);
}

@Test
public void updateRepositoryDescription() throws RetryTimeoutException {
RepositoryLogic repositoryLogic = repositoryLogic(persist);

repositoryLogic.initialize("main");

RepositoryDescription initial = requireNonNull(repositoryLogic.fetchRepositoryDescription());

RepositoryDescription updated =
RepositoryDescription.builder()
.putProperties("updated", "true")
.defaultBranchName("main2")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, exactly this one might be updated. Let's remove "defaultBranchName" from that check.
I'm not sure whether there'll ever be a command to update arbitrary information - rather per attribute.

// the following attributes are read-only, should not be updated
.oldestPossibleCommitTime(Instant.ofEpochSecond(12345))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the below setting are actually read-only attributes.

.repositoryCreatedTime(Instant.ofEpochSecond(456789))
.build();

RepositoryDescription previous = repositoryLogic.updateRepositoryDescription(updated);

soft.assertThat(previous).isEqualTo(initial);
soft.assertThat(repositoryLogic.fetchRepositoryDescription())
.isEqualTo(
ImmutableRepositoryDescription.builder()
.from(initial)
.putProperties("updated", "true")
.defaultBranchName("main2")
.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ static InternalRef internalReference(String name) {
InternalRef REF_REFS = internalReference("refs");

/**
* Internal reference with always exactly one commit that serves as a reference for when the
* repository has been created.
* Internal reference that points to the current {@link RepositoryDescription}.
*
* <p>The actual information is available via {@link #KEY_REPO_DESCRIPTION} from the HEAD commit.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.function.Consumer;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.projectnessie.versioned.storage.common.exceptions.RetryTimeoutException;

/** Logic to setup/initialize a Nessie repository. */
public interface RepositoryLogic {
Expand All @@ -33,5 +34,18 @@ void initialize(
@jakarta.annotation.Nullable
RepositoryDescription fetchRepositoryDescription();

/**
* Updates the repository description, and returns the previous description, or {@code null} if
* there was no previous description.
*
* @param repositoryDescription the new description.
* @return the previous description, or {@code null} if there was no previous description.
* @throws RetryTimeoutException if the update failed after all retries.
*/
@Nullable
@jakarta.annotation.Nullable
RepositoryDescription updateRepositoryDescription(RepositoryDescription repositoryDescription)
throws RetryTimeoutException;

boolean repositoryExists();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
import static org.projectnessie.versioned.storage.common.logic.CommitRetry.commitRetry;
import static org.projectnessie.versioned.storage.common.logic.CreateCommit.Add.commitAdd;
import static org.projectnessie.versioned.storage.common.logic.CreateCommit.newCommitBuilder;
import static org.projectnessie.versioned.storage.common.logic.InternalRef.KEY_REPO_DESCRIPTION;
Expand All @@ -38,12 +39,16 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.projectnessie.versioned.storage.common.exceptions.CommitConflictException;
import org.projectnessie.versioned.storage.common.exceptions.CommitWrappedException;
import org.projectnessie.versioned.storage.common.exceptions.ObjNotFoundException;
import org.projectnessie.versioned.storage.common.exceptions.ObjTooLargeException;
import org.projectnessie.versioned.storage.common.exceptions.RefAlreadyExistsException;
import org.projectnessie.versioned.storage.common.exceptions.RefConditionFailedException;
import org.projectnessie.versioned.storage.common.exceptions.RefNotFoundException;
import org.projectnessie.versioned.storage.common.exceptions.RetryTimeoutException;
import org.projectnessie.versioned.storage.common.indexes.StoreIndex;
import org.projectnessie.versioned.storage.common.indexes.StoreIndexElement;
import org.projectnessie.versioned.storage.common.logic.CommitRetry.RetryException;
import org.projectnessie.versioned.storage.common.logic.StringLogic.StringValue;
import org.projectnessie.versioned.storage.common.objtypes.CommitObj;
import org.projectnessie.versioned.storage.common.objtypes.CommitOp;
Expand Down Expand Up @@ -135,20 +140,28 @@ public RepositoryDescription fetchRepositoryDescription() {
requireNonNull(
op.value(), "Commit operation for repository description has no value"));

return readRepositoryDescription(value);
return deserialize(value);
} catch (ObjNotFoundException e) {
return null;
}
}

private RepositoryDescription readRepositoryDescription(StringValue value) {
private static RepositoryDescription deserialize(StringValue value) {
try {
return SHARED_OBJECT_MAPPER.readValue(value.completeValue(), RepositoryDescription.class);
} catch (ObjNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}

private static byte[] serialize(RepositoryDescription repositoryDescription) {
try {
return SHARED_OBJECT_MAPPER.writeValueAsBytes(repositoryDescription);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private void addRepositoryDescription(
CreateCommit.Builder b,
Consumer<RepositoryDescription.Builder> repositoryDescription,
Expand All @@ -169,14 +182,59 @@ private void addRepositoryDescription(
null,
"application/json",
SHARED_OBJECT_MAPPER.writeValueAsBytes(repoDesc.build()));
// can safely ignore the ID returned from storeObj() - it's fine, if the obj already exists
// can safely ignore the response from storeObj() - it's fine, if the obj already exists
persist.storeObj(string);
b.addAdds(commitAdd(KEY_REPO_DESCRIPTION, 0, requireNonNull(string.id()), null, null));
} catch (ObjTooLargeException | ObjNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}

@Override
public RepositoryDescription updateRepositoryDescription(RepositoryDescription newDescription)
throws RetryTimeoutException {
// prevent modification of read-only attributes
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This came to my mind after @dimas-b suggestion to introduce an option in quarkus-cli to allow end users to modify the repo description. If we ever do that later, I think we'd need a safety net like this one.

RepositoryDescription existingDescription = requireNonNull(fetchRepositoryDescription());
RepositoryDescription sanitizedDescription =
ImmutableRepositoryDescription.builder()
.from(newDescription)
.oldestPossibleCommitTime(existingDescription.oldestPossibleCommitTime())
.repositoryCreatedTime(existingDescription.repositoryCreatedTime())
.build();
byte[] serialized = serialize(sanitizedDescription);
try {
StringValue existing =
commitRetry(
persist,
(p, retryState) -> {
try {
Reference reference = requireNonNull(persist.fetchReference(REF_REPO.name()));
return stringLogic(persist)
.updateStringOnRef(
reference,
KEY_REPO_DESCRIPTION,
b ->
b.message("Update repository description")
.commitType(CommitType.INTERNAL),
"application/json",
serialized);
} catch (RefConditionFailedException | CommitConflictException e) {
throw new RetryException();
} catch (ObjNotFoundException | RefNotFoundException e) {
throw new CommitWrappedException(e);
}
});
return existing != null ? deserialize(existing) : null;
} catch (CommitConflictException e) {
throw new RuntimeException(
"An unexpected internal error happened while committing a repository description update");
} catch (CommitWrappedException e) {
throw new RuntimeException(
"An unexpected internal error happened while committing a repository description update",
e.getCause());
}
}

@SuppressWarnings({"JavaTimeDefaultTimeZone"})
private void initializeInternalRef(
InternalRef internalRef, Consumer<CreateCommit.Builder> commitEnhancer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@
*/
package org.projectnessie.versioned.storage.common.logic;

import java.util.function.Consumer;
import javax.annotation.Nullable;
import org.projectnessie.versioned.storage.common.exceptions.CommitConflictException;
import org.projectnessie.versioned.storage.common.exceptions.ObjNotFoundException;
import org.projectnessie.versioned.storage.common.exceptions.RefConditionFailedException;
import org.projectnessie.versioned.storage.common.exceptions.RefNotFoundException;
import org.projectnessie.versioned.storage.common.indexes.StoreKey;
import org.projectnessie.versioned.storage.common.logic.CreateCommit.Builder;
import org.projectnessie.versioned.storage.common.objtypes.StringObj;
import org.projectnessie.versioned.storage.common.persist.ObjId;
import org.projectnessie.versioned.storage.common.persist.Reference;

/**
* Provides and encapsulates all logic around string data compression and diff creation and
Expand All @@ -34,6 +42,30 @@ StringObj updateString(StringValue previousValue, String contentType, byte[] str
StringObj updateString(StringValue previousValue, String contentType, String stringValue)
throws ObjNotFoundException;

/**
* Updates a string value stored as a content on a reference.
*
* @param reference The reference to update.
* @param storeKey The store key to use for storing the string value.
* @param commitEnhancer A consumer that can be used to enhance the commit that will be created.
* @param contentType The content type of the string value.
* @param stringValueUtf8 The string value as UTF-8 encoded byte array.
* @return The previously stored string value, if any, or {@code null} if no string value was
* stored on the reference.
*/
@Nullable
@jakarta.annotation.Nullable
StringValue updateStringOnRef(
Reference reference,
StoreKey storeKey,
Consumer<Builder> commitEnhancer,
String contentType,
byte[] stringValueUtf8)
throws ObjNotFoundException,
CommitConflictException,
RefNotFoundException,
RefConditionFailedException;

/**
* Describes a string value and allows retrieval of complete string values. Implementations can
* lazily decompress data and/or re-assemble values that consist of diffs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,35 @@

import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
import static org.projectnessie.nessie.relocated.protobuf.UnsafeByteOperations.unsafeWrap;
import static org.projectnessie.versioned.storage.common.logic.CreateCommit.Add.commitAdd;
import static org.projectnessie.versioned.storage.common.logic.Logics.commitLogic;
import static org.projectnessie.versioned.storage.common.logic.Logics.indexesLogic;
import static org.projectnessie.versioned.storage.common.objtypes.CommitHeaders.EMPTY_COMMIT_HEADERS;
import static org.projectnessie.versioned.storage.common.objtypes.StringObj.stringData;
import static org.projectnessie.versioned.storage.common.persist.ObjType.STRING;

import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.function.Consumer;
import org.projectnessie.nessie.relocated.protobuf.ByteString;
import org.projectnessie.versioned.storage.common.exceptions.CommitConflictException;
import org.projectnessie.versioned.storage.common.exceptions.ObjNotFoundException;
import org.projectnessie.versioned.storage.common.exceptions.RefConditionFailedException;
import org.projectnessie.versioned.storage.common.exceptions.RefNotFoundException;
import org.projectnessie.versioned.storage.common.indexes.StoreIndex;
import org.projectnessie.versioned.storage.common.indexes.StoreIndexElement;
import org.projectnessie.versioned.storage.common.indexes.StoreKey;
import org.projectnessie.versioned.storage.common.logic.CreateCommit.Builder;
import org.projectnessie.versioned.storage.common.objtypes.CommitObj;
import org.projectnessie.versioned.storage.common.objtypes.CommitOp;
import org.projectnessie.versioned.storage.common.objtypes.Compression;
import org.projectnessie.versioned.storage.common.objtypes.StringObj;
import org.projectnessie.versioned.storage.common.persist.ObjId;
import org.projectnessie.versioned.storage.common.persist.Persist;
import org.projectnessie.versioned.storage.common.persist.Reference;

final class StringLogicImpl implements StringLogic {
private final Persist persist;
Expand Down Expand Up @@ -68,6 +86,54 @@ public StringObj updateString(StringValue previousValue, String contentType, Str
return updateString(previousValue, contentType, stringValue.getBytes(StandardCharsets.UTF_8));
}

@Override
public StringValue updateStringOnRef(
Reference reference,
StoreKey storeKey,
Consumer<Builder> commitEnhancer,
String contentType,
byte[] stringValueUtf8)
throws ObjNotFoundException,
CommitConflictException,
RefNotFoundException,
RefConditionFailedException {
CommitLogic commitLogic = commitLogic(persist);
IndexesLogic indexesLogic = indexesLogic(persist);
CommitObj head = commitLogic.headCommit(reference);
StoreIndex<CommitOp> index = indexesLogic.buildCompleteIndexOrEmpty(head);
StoreIndexElement<CommitOp> existingElement = index.get(storeKey);

// If we are updating an existing string, reuse its content-id (which may be null,
// e.g. for repo descriptions). Otherwise, generate a new content-id.
ObjId existingValueId = null;
UUID contentId = null;
if (existingElement != null) {
CommitOp op = existingElement.content();
if (op.action().exists()) {
existingValueId = op.value();
contentId = op.contentId();
}
} else {
contentId = UUID.randomUUID();
}

StringValue existing = existingValueId != null ? fetchString(existingValueId) : null;
StringObj newValue = updateString(existing, contentType, stringValueUtf8);

ObjId newValueId = requireNonNull(newValue.id());
if (!newValueId.equals(existingValueId)) {
CreateCommit.Builder builder =
CreateCommit.newCommitBuilder()
.parentCommitId(reference.pointer())
.headers(EMPTY_COMMIT_HEADERS)
.addAdds(commitAdd(storeKey, 0, newValueId, existingValueId, contentId));
commitEnhancer.accept(builder);
CommitObj committed = commitLogic.doCommit(builder.build(), singletonList(newValue));
persist.updateReferencePointer(reference, requireNonNull(committed).id());
}
return existing;
}

static final class StringValueHolder implements StringValue {
final StringObj obj;

Expand Down