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

Add option for table prefix to DynamoDB #7598

Merged
merged 9 commits into from
Oct 6, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ as necessary. Empty sections will not end in the release notes.
### New Features

- Spark SQL extensions now support the `DROP ... IF EXISTS` syntax for branches and tags.
- `table-prefix` configuration option added to DynamoDB version store.

### Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 Dremio
*
* 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 org.projectnessie.quarkus.config;

import io.quarkus.runtime.annotations.StaticInitSafe;
import io.smallrye.config.ConfigMapping;
import java.util.Optional;

@StaticInitSafe
@ConfigMapping(prefix = "nessie.version.store.persist.dynamodb")
public interface QuarkusDynamoDBConfig {
Optional<String> tablePrefix();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import org.projectnessie.quarkus.config.QuarkusDynamoDBConfig;
import org.projectnessie.quarkus.providers.versionstore.StoreType;
import org.projectnessie.versioned.storage.common.persist.Backend;
import org.projectnessie.versioned.storage.dynamodb.DynamoDBBackendConfig;
Expand All @@ -31,10 +32,16 @@ public class DynamoDBBackendBuilder implements BackendBuilder {

@Inject DynamoDbClient client;

@Inject QuarkusDynamoDBConfig dynamoDBConfig;

@Override
public Backend buildBackend() {
DynamoDBBackendFactory factory = new DynamoDBBackendFactory();
DynamoDBBackendConfig c = DynamoDBBackendConfig.builder().client(client).build();
DynamoDBBackendConfig c =
DynamoDBBackendConfig.builder()
.client(client)
.tablePrefix(dynamoDBConfig.tablePrefix())
.build();
return factory.buildBackend(c);
}
}
13 changes: 7 additions & 6 deletions site/docs/try/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ A complete set of the Quarkus Cassandra extension configuration options can be f

When setting `nessie.version.store.type=DYNAMODB` which enables DynamoDB as the version store used by the Nessie server, the following configurations are applicable in combination with `nessie.version.store.type`:

| Property | Default values | Type | Description |
|-----------------------------------------|----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
| `quarkus.dynamodb.aws.region` | | `String` | Sets DynamoDB AWS region. |
| `quarkus.dynamodb.aws.credentials.type` | | | Sets the credentials provider that should be used to authenticate with AWS. |
| `quarkus.dynamodb.endpoint-override` | | `URI` | Sets the endpoint URI with which the SDK should communicate. If not specified, an appropriate endpoint to be used for the given service and region. |
| `quarkus.dynamodb.sync-client.type` | `url` | `url, apache` | Sets the type of the sync HTTP client implementation |
| Property | Default values | Type | Description |
|------------------------------------------------------|----------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| `quarkus.dynamodb.aws.region` | | `String` | Sets DynamoDB AWS region. |
| `quarkus.dynamodb.aws.credentials.type` | | | Sets the credentials provider that should be used to authenticate with AWS. |
| `quarkus.dynamodb.endpoint-override` | | `URI` | Sets the endpoint URI with which the SDK should communicate. If not specified, an appropriate endpoint to be used for the given service and region. |
| `quarkus.dynamodb.sync-client.type` | `url` | `url, apache` | Sets the type of the sync HTTP client implementation |
| `nessie.version.store.persist.dynamodb.table-prefix` | n/a | `String` | Prefix for tables, default is no prefix. |

!!! info
A complete set of DynamoDB configuration options for Quarkus can be found on [quarkiverse.github.io](https://quarkiverse.github.io/quarkiverse-docs/quarkus-amazon-services/dev/amazon-dynamodb.html#_configuration_reference)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,54 +15,148 @@
*/
package org.projectnessie.versioned.storage.dynamodb;

import static java.util.Objects.requireNonNull;
import static org.projectnessie.versioned.storage.common.logic.Logics.repositoryLogic;
import static org.projectnessie.versioned.storage.common.objtypes.ContentValueObj.contentValue;

import java.util.List;
import java.util.Optional;
import org.assertj.core.api.SoftAssertions;
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.projectnessie.nessie.relocated.protobuf.ByteString;
import org.projectnessie.versioned.storage.common.config.StoreConfig;
import org.projectnessie.versioned.storage.common.exceptions.ObjTooLargeException;
import org.projectnessie.versioned.storage.common.logic.RepositoryLogic;
import org.projectnessie.versioned.storage.common.persist.Obj;
import org.projectnessie.versioned.storage.common.persist.ObjId;
import org.projectnessie.versioned.storage.common.persist.Persist;
import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
import org.projectnessie.versioned.storage.testextension.BackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
import org.projectnessie.versioned.storage.testextension.NessiePersist;
import org.projectnessie.versioned.storage.testextension.PersistExtension;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

@NessieBackend(DynamoDBBackendTestFactory.class)
@ExtendWith({PersistExtension.class, SoftAssertionsExtension.class})
public class ITDynamoDBPersist extends AbstractPersistTests {
@InjectSoftAssertions protected SoftAssertions soft;

@NessiePersist protected Persist persist;

@Test
public void dynamoDbHardItemSizeLimit() throws Exception {
persist.storeObj(
contentValue(ObjId.randomObjId(), "foo", 42, ByteString.copyFrom(new byte[350 * 1024])));

persist.storeObjs(
new Obj[] {
contentValue(ObjId.randomObjId(), "foo", 42, ByteString.copyFrom(new byte[350 * 1024]))
});

// DynamoDB's hard 400k limit
soft.assertThatThrownBy(
() ->
persist.storeObj(
contentValue(
ObjId.randomObjId(), "foo", 42, ByteString.copyFrom(new byte[400 * 1024]))))
.isInstanceOf(ObjTooLargeException.class);
soft.assertThatThrownBy(
() ->
persist.storeObjs(
new Obj[] {

@Nested
@ExtendWith({PersistExtension.class, SoftAssertionsExtension.class})
public class DynamoDbHardItemSizeLimits {
@InjectSoftAssertions protected SoftAssertions soft;

@NessiePersist protected Persist persist;

@Test
public void dynamoDbHardItemSizeLimit() throws Exception {
persist.storeObj(
contentValue(ObjId.randomObjId(), "foo", 42, ByteString.copyFrom(new byte[350 * 1024])));

persist.storeObjs(
new Obj[] {
contentValue(ObjId.randomObjId(), "foo", 42, ByteString.copyFrom(new byte[350 * 1024]))
});

// DynamoDB's hard 400k limit
soft.assertThatThrownBy(
() ->
persist.storeObj(
contentValue(
ObjId.randomObjId(), "foo", 42, ByteString.copyFrom(new byte[400 * 1024]))
}))
.isInstanceOf(ObjTooLargeException.class);
ObjId.randomObjId(),
"foo",
42,
ByteString.copyFrom(new byte[400 * 1024]))))
.isInstanceOf(ObjTooLargeException.class);
soft.assertThatThrownBy(
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicated code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Potentially, although I did not modify the body of that test at all so the duplication was there already.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK let's leave it, as it's not related to your changes 👍

() ->
persist.storeObjs(
new Obj[] {
contentValue(
ObjId.randomObjId(),
"foo",
42,
ByteString.copyFrom(new byte[400 * 1024]))
}))
.isInstanceOf(ObjTooLargeException.class);
}
}

@Nested
@ExtendWith({PersistExtension.class, SoftAssertionsExtension.class})
public class TablePrefixes {
@InjectSoftAssertions protected SoftAssertions soft;

@NessiePersist(initializeRepo = false)
protected BackendTestFactory factory;

@Test
void tablePrefix() {
DynamoDBBackendTestFactory dynamoDBBackendTestFactory = (DynamoDBBackendTestFactory) factory;
try (DynamoDBBackend backendA =
dynamoDBBackendTestFactory.createNewBackend(
dynamoDBBackendTestFactory
.dynamoDBConfigBuilder()
.tablePrefix(Optional.of("instanceA"))
.build(),
true);
DynamoDBBackend backendB =
dynamoDBBackendTestFactory.createNewBackend(
dynamoDBBackendTestFactory
.dynamoDBConfigBuilder()
.tablePrefix(Optional.of("instanceB"))
.build(),
true)) {

DynamoDbClient clientA = requireNonNull(backendA.client());
DynamoDbClient clientB = requireNonNull(backendB.client());

List<String> expectedTables = List.of();
soft.assertThat(clientA.listTables().tableNames())
.containsExactlyInAnyOrderElementsOf(expectedTables);
soft.assertThat(clientB.listTables().tableNames())
.containsExactlyInAnyOrderElementsOf(expectedTables);

// Setup "A"

backendA.setupSchema();

Persist persistA = backendA.createFactory().newPersist(StoreConfig.Adjustable.empty());
RepositoryLogic repoA = repositoryLogic(persistA);

expectedTables = List.of("instanceA_refs", "instanceA_objs");

soft.assertThat(clientA.listTables().tableNames())
.containsExactlyInAnyOrderElementsOf(expectedTables);
soft.assertThat(clientB.listTables().tableNames())
.containsExactlyInAnyOrderElementsOf(expectedTables);

soft.assertThat(repoA.repositoryExists()).isFalse();
repoA.initialize("main");
soft.assertThat(repoA.repositoryExists()).isTrue();

// Setup "B"

backendB.setupSchema();

Persist persistB = backendB.createFactory().newPersist(StoreConfig.Adjustable.empty());
RepositoryLogic repoB = repositoryLogic(persistB);

expectedTables =
List.of("instanceA_refs", "instanceA_objs", "instanceB_refs", "instanceB_objs");

soft.assertThat(clientA.listTables().tableNames())
.containsExactlyInAnyOrderElementsOf(expectedTables);
soft.assertThat(clientB.listTables().tableNames())
.containsExactlyInAnyOrderElementsOf(expectedTables);

soft.assertThat(repoB.repositoryExists()).isFalse();
repoB.initialize("main");
soft.assertThat(repoB.repositoryExists()).isTrue();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,16 @@ final class DynamoDBBackend implements Backend {
private final DynamoDbClient client;
private final boolean closeClient;

DynamoDBBackend(@Nonnull @jakarta.annotation.Nonnull DynamoDbClient client, boolean closeClient) {
this.client = client;
final String tableRefs;
final String tableObjs;

DynamoDBBackend(
@Nonnull @jakarta.annotation.Nonnull DynamoDBBackendConfig config, boolean closeClient) {
this.client = config.client();
this.tableRefs =
config.tablePrefix().map(prefix -> prefix + '_' + TABLE_REFS).orElse(TABLE_REFS);
this.tableObjs =
config.tablePrefix().map(prefix -> prefix + '_' + TABLE_OBJS).orElse(TABLE_OBJS);
this.closeClient = closeClient;
}

Expand All @@ -75,8 +83,8 @@ public void close() {

@Override
public void setupSchema() {
createIfMissing(TABLE_REFS);
createIfMissing(TABLE_OBJS);
createIfMissing(this.tableRefs);
createIfMissing(this.tableObjs);
}

private void createIfMissing(String name) {
Expand Down Expand Up @@ -149,7 +157,7 @@ public void eraseRepositories(Set<String> repositoryIds) {
@SuppressWarnings("resource")
DynamoDbClient c = client();

Stream.of(TABLE_REFS, TABLE_OBJS)
Stream.of(this.tableRefs, this.tableObjs)
dimas-b marked this conversation as resolved.
Show resolved Hide resolved
.forEach(
table -> {
try (BatchWrite batchWrite = new BatchWrite(this, table)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
*/
package org.projectnessie.versioned.storage.dynamodb;

import java.util.Optional;
import org.immutables.value.Value;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

@Value.Immutable
public interface DynamoDBBackendConfig {
DynamoDbClient client();

Optional<String> tablePrefix();

static ImmutableDynamoDBBackendConfig.Builder builder() {
return ImmutableDynamoDBBackendConfig.builder();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@ public DynamoDBBackendConfig newConfigInstance() {
@jakarta.annotation.Nonnull
public DynamoDBBackend buildBackend(
@Nonnull @jakarta.annotation.Nonnull DynamoDBBackendConfig config) {
return new DynamoDBBackend(config.client(), false);
return new DynamoDBBackend(config, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,21 @@ public String getName() {
return DynamoDBBackendFactory.NAME;
}

@SuppressWarnings("ClassEscapesDefinedScope")
Copy link
Member

Choose a reason for hiding this comment

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

IntelliJ flags this annotation as redundant... Could you double check, please?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed it. Although interestingly my IntilliJ complains when I remove the annotation.

Copy link
Member

Choose a reason for hiding this comment

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

What does it say? My EAP 2 build is happy :)

Copy link
Member

Choose a reason for hiding this comment

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

I guess it might be complaining about returning the package-private DynamoDBBackend type, but the interface defines the return value as Backend... I think the code is correct and IJ was overzealous :)

Copy link
Member

@dimas-b dimas-b Oct 6, 2023

Choose a reason for hiding this comment

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

Correction: I was running my old IJ built for this review before and it did not give that warning. I switched to EAP 3 (just now) and it does indeed issue the warning. Having considered the code carefully, I think the warning is justified.

I also see some usages that rely on the more specific return type... so cleaning this up is probably beyond the scope of this PR. Let's not suppress the warning, though, as a reminder to revisit this.

Copy link
Contributor

Choose a reason for hiding this comment

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

We have the same issue in BigTable impl... we return Backend there, not BigTableBackend.

@Override
public DynamoDBBackend createNewBackend() {
return new DynamoDBBackend(buildNewClient(), true);
return createNewBackend(dynamoDBConfigBuilder().build(), true);
}

@SuppressWarnings("ClassEscapesDefinedScope")
public DynamoDBBackend createNewBackend(
DynamoDBBackendConfig dynamoDBBackendConfig, boolean closeClient) {
return new DynamoDBBackend(dynamoDBBackendConfig, closeClient);
}

@VisibleForTesting
public ImmutableDynamoDBBackendConfig.Builder dynamoDBConfigBuilder() {
return DynamoDBBackendConfig.builder().client(buildNewClient());
}

@VisibleForTesting
Expand Down