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 4 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,6 +45,7 @@
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.InternalRef;
Expand Down Expand Up @@ -234,4 +235,26 @@ 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()
.defaultBranchName("main")
.putProperties("updated", "true")
.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(updated);
}
}
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 @@ -33,17 +34,23 @@

import java.io.IOException;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
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 +142,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 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 +184,51 @@ 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));
b.addAdds(
commitAdd(KEY_REPO_DESCRIPTION, 0, requireNonNull(string.id()), null, UUID.randomUUID()));
Copy link
Member

Choose a reason for hiding this comment

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

This change will only work for new repositories, but existing repositories do not have a content-ID for the repo-description. The update-logic does not work without a content-ID.

I'd prefer to leave the content-ID as null. (There's no need for a CID here)

I know that the repo-config stuff uses a CID, see comment in SLImpl

} catch (ObjTooLargeException | ObjNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}

@Override
public RepositoryDescription updateRepositoryDescription(
RepositoryDescription repositoryDescription) throws RetryTimeoutException {
try {
return commitRetry(
persist,
(p, retryState) -> {
try {
Reference reference = Objects.requireNonNull(persist.fetchReference(REF_REPO.name()));
Copy link
Member

Choose a reason for hiding this comment

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

We should probably fix the javadoc of REF_REPO, which currently says Internal reference with always exactly one commit ...

StringValue existing =
stringLogic(persist)
.updateStringOnRef(
reference,
KEY_REPO_DESCRIPTION,
b ->
b.message("Update repository description")
.commitType(CommitType.INTERNAL),
"application/json",
serialize(repositoryDescription));
return existing != null ? deserialize(existing) : null;
} catch (RefConditionFailedException | CommitConflictException e) {
throw new RetryException();
} catch (ObjNotFoundException | RefNotFoundException e) {
throw new CommitWrappedException(e);
}
});
} 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,53 @@ 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);
ObjId existingValueId = null;
UUID existingContentId = null;
StringValue existing = null;
if (existingElement != null) {
CommitOp op = existingElement.content();
if (op.action().exists()) {
existingValueId = op.value();
existingContentId = op.contentId();
existing = existingValueId != null ? fetchString(requireNonNull(existingValueId)) : null;
}
}
if (existingContentId == null) {
Copy link
Member

Choose a reason for hiding this comment

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

This makes updating repo-descriptions of already existing repos impossible (those have no CID for repo-description).

Mabe replacing the if with a simple else helps here.

Should also have some comment to explain the logic wrt repo-desc + repo-config

existingContentId = UUID.randomUUID();
}

StringObj newValue = updateString(existing, contentType, stringValueUtf8);

ObjId newValueId = newValue.id();
if (!requireNonNull(newValueId).equals(existingValueId)) {
CreateCommit.Builder builder =
CreateCommit.newCommitBuilder()
.parentCommitId(reference.pointer())
.headers(EMPTY_COMMIT_HEADERS)
.addAdds(commitAdd(storeKey, 0, newValueId, existingValueId, existingContentId));
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