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

Support DynamoDB as a Storage Target #220

Merged
merged 13 commits into from
Jun 13, 2023
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "
# Required to get dependabot updates for micronaut-core
micronaut-core = { module = 'io.micronaut:micronaut-core-bom', version.ref = 'micronaut' }
micronaut-cache = { module = "io.micronaut.cache:micronaut-cache-bom", version.ref = "micronaut-cache" }
micronaut-logging = { module = "io.micronaut.logging:micronaut-logging-bom", version.ref = "micronaut-logging" }
micronaut-micrometer = { module = "io.micronaut.micrometer:micronaut-micrometer-bom", version.ref = "micronaut-micrometer" }
micronaut-serde = { module = "io.micronaut.serde:micronaut-serde-bom", version.ref = "micronaut-serde" }
micronaut-sql = { module = "io.micronaut.sql:micronaut-sql-bom", version.ref = "micronaut-sql" }
micronaut-validation = { module = "io.micronaut.validation:micronaut-validation-bom", version.ref = "micronaut-validation" }

managed-microstream-aws-s3 = { module = 'one.microstream:microstream-afs-aws-s3', version.ref = 'managed-microstream' }
managed-microstream-aws-dynamodb = { module = 'one.microstream:microstream-afs-aws-dynamodb', version.ref = 'managed-microstream' }
managed-microstream-sql = { module = 'one.microstream:microstream-afs-sql', version.ref = 'managed-microstream' }
managed-microstream-storage-embedded-configuration = { module = 'one.microstream:microstream-storage-embedded-configuration', version.ref = 'managed-microstream' }
managed-microstream-cache = { module = 'one.microstream:microstream-cache', version.ref = 'managed-microstream' }
Expand All @@ -38,6 +38,7 @@ microstream-persistence-binary-jdk8 = { module = 'one.microstream:microstream-pe
microstream-persistence-binary-jdk17 = { module = 'one.microstream:microstream-persistence-binary-jdk17', version.ref = 'managed-microstream' }

testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" }
jupiter-jupiter-params = { module = 'org.junit.jupiter:junit-jupiter-params' }
jupiter-api = { module = 'org.junit.jupiter:junit-jupiter-api' }
junit-jupiter-engine = { module = 'org.junit.jupiter:junit-jupiter-engine' }
Expand Down
6 changes: 5 additions & 1 deletion microstream/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
compileOnly(mnMicrometer.micronaut.micrometer.core)
compileOnly(projects.micronautMicrostreamAnnotations)
compileOnly(libs.managed.microstream.aws.s3)
compileOnly(libs.managed.microstream.aws.dynamodb)
compileOnly(libs.managed.microstream.sql)
compileOnly(mnAws.micronaut.aws.sdk.v2)
compileOnly(mn.micronaut.management)
Expand All @@ -32,7 +33,7 @@ dependencies {
testImplementation(projects.micronautMicrostreamAnnotations)

testImplementation(projects.testSuiteUtils)

// S3 connector tests
testImplementation(libs.managed.microstream.aws.s3)
testImplementation(mnAws.micronaut.aws.sdk.v2)
Expand All @@ -42,6 +43,9 @@ dependencies {
testImplementation(mnSql.micronaut.jdbc.hikari)
testRuntimeOnly(mnSql.postgresql)

// DynamoDB tests
testImplementation(libs.managed.microstream.aws.dynamodb)

testRuntimeOnly(mnLogging.logback.classic)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2017-2023 original authors
*
* 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
*
* https://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 io.micronaut.microstream.dynamodb;

import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.annotation.Parameter;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;

import java.util.Optional;

/**
* {@link EachProperty} implementation of {@link DynamoDbStorageConfigurationProvider}.
* @author Sergio del Amo
* @since 2.0.0
*/
@EachProperty("microstream.dynamodb.storage")
public class DefaultDynamoDBStorageConfigurationProvider implements DynamoDbStorageConfigurationProvider {

@NonNull
private Class<?> rootClass;

private final String name;

@Nullable
private String dynamoDbClientName;

@NonNull
private String tableName;

public DefaultDynamoDBStorageConfigurationProvider(@Parameter String name) {
this.name = name;
}

@Override
@NonNull
public String getName() {
return name;
}

@Override
@NonNull
public Class<?> getRootClass() {
return this.rootClass;
}

/**
* Class of the Root Instance.
* <a href="https://docs.microstream.one/manual/storage/root-instances.html">Root Instances</a>
* @param rootClass Class for the Root Instance.
*/
public void setRootClass(@NonNull Class<?> rootClass) {
this.rootClass = rootClass;
}

@Override
@NonNull
public Optional<String> getDynamoDbClientName() {
return Optional.ofNullable(dynamoDbClientName);
}

/**
* The name qualifier of the defined DynamoDB Client to use.
* If unset, a client with the same name as the storage will be used.
* If there is no bean with a name qualifier matching the storage name, the default client will be used.
*
* @param dynamoDbClientName the name qualifier of the S3Client to use
*/
public void setDynamoDbClientName(@Nullable String dynamoDbClientName) {
this.dynamoDbClientName = dynamoDbClientName;
}

@NonNull
@Override
public String getTableName() {
return tableName;
}

/**
* @param tableName Name of the DynamoDB table to use.
*/
public void setTableName(@NonNull String tableName) {
this.tableName = tableName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2017-2023 original authors
*
* 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
*
* https://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 io.micronaut.microstream.dynamodb;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.microstream.conf.RootClassConfigurationProvider;

import java.util.Optional;

/**
* Dynamo DB Storage Configuration.
* @author Sergio del Amo
* @since 2.0.0
*/
public interface DynamoDbStorageConfigurationProvider extends RootClassConfigurationProvider {

/**
* The name qualifier of the defined DynamoDBClient to use.
* If unset, a client with the same name as the storage will be used.
* If there is no bean with a name qualifier matching the storage name, the default client will be used.
*
* @return Returns the name qualifier of the S3Client to use.
*/
@NonNull
Optional<String> getDynamoDbClientName();

/**
*
* @return Returns the name of the DynamoDB table to use.
*/
@NonNull
String getTableName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2017-2023 original authors
*
* 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
*
* https://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 io.micronaut.microstream.dynamodb;

import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.inject.qualifiers.Qualifiers;
import jakarta.inject.Singleton;
import one.microstream.afs.aws.dynamodb.types.DynamoDbConnector;
import one.microstream.afs.blobstore.types.BlobStoreFileSystem;
import one.microstream.storage.embedded.types.EmbeddedStorage;
import one.microstream.storage.embedded.types.EmbeddedStorageFoundation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

/**
* Factory for an S3 based EmbeddedStorageFoundation.
*
* @author Tim Yates
* @since 2.0.0
*/
@Factory
public class DynamoDbStorageFoundationFactory {

private static final Logger LOG = LoggerFactory.getLogger(DynamoDbStorageFoundationFactory.class);

/**
* @param ctx Bean Context.
* @param provider A {@link DynamoDbStorageConfigurationProvider} provider.
* @return A {@link EmbeddedStorageFoundation}.
*/
@Singleton
@EachBean(DynamoDbStorageConfigurationProvider.class)
EmbeddedStorageFoundation<?> createFoundation(
DynamoDbStorageConfigurationProvider provider,
BeanContext ctx
) {
String dynamoDbClientName = provider.getDynamoDbClientName().orElse(provider.getName());

if (LOG.isDebugEnabled()) {
LOG.debug("Looking for DynamoDbClient named '{}'", dynamoDbClientName);
}

DynamoDbClient dynamoDbclient = ctx.findBean(DynamoDbClient.class, Qualifiers.byName(dynamoDbClientName))
.orElseGet(() -> defaultClient(ctx, dynamoDbClientName));

if (LOG.isDebugEnabled()) {
LOG.debug("Got DynamoDbClient {}", dynamoDbclient);
}

BlobStoreFileSystem fileSystem = BlobStoreFileSystem.New(
DynamoDbConnector.Caching(dynamoDbclient)
);

return EmbeddedStorage.Foundation(fileSystem.ensureDirectoryPath(provider.getTableName()));
}

private DynamoDbClient defaultClient(BeanContext ctx, String dynamoDbClientName) {
if (LOG.isDebugEnabled()) {
LOG.debug("No DynamoDbClient named '{}' found. Looking for a default", dynamoDbClientName);
}
return ctx.getBean(DynamoDbClient.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2017-2023 original authors
*
* 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
*
* https://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.
*/
/**
* Classes related to the use of DynamoDB as a Storage Target.
* @author Sergio del Amo
* @since 2.0.0
*/
@Requires(classes = {DynamoDbConnector.class, BlobStoreFileSystem.class})
@Requires(beans = {DynamoDbClient.class})
package io.micronaut.microstream.dynamodb;

import io.micronaut.context.annotation.Requires;
import one.microstream.afs.aws.dynamodb.types.DynamoDbConnector;
import one.microstream.afs.blobstore.types.BlobStoreFileSystem;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.micronaut.microstream.dynamodb

import io.micronaut.context.annotation.Requires
import io.micronaut.context.annotation.Value
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton
import software.amazon.awssdk.auth.credentials.AwsCredentials
import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder

@Requires(property = "dynamodb-local.host")
@Requires(property = "dynamodb-local.port")
@Singleton
class DynamoDbClientBuilderListener
implements BeanCreatedEventListener<DynamoDbClientBuilder> {

private final URI endpoint
private final String accessKeyId
private final String secretAccessKey

DynamoDbClientBuilderListener(@Value('${dynamodb-local.host}') String host,
@Value('${dynamodb-local.port}') String port) {
this.endpoint = "http://${host}:${port}".toURI()
this.accessKeyId = "fakeMyKeyId"
this.secretAccessKey = "fakeSecretAccessKey"
}

@Override
DynamoDbClientBuilder onCreated(BeanCreatedEvent<DynamoDbClientBuilder> event) {
event.getBean().endpointOverride(endpoint)
.credentialsProvider(() -> new AwsCredentials() {
@Override
String accessKeyId() {
return accessKeyId;
}

@Override
String secretAccessKey() {
return secretAccessKey;
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.micronaut.microstream.dynamodb

import io.micronaut.context.BeanContext
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@MicronautTest(startApplication = false)
class DynamoDbStorageConfigurationProviderNoBeanByDefaultSpec extends Specification {

@Inject
BeanContext beanContext

void "bean of type DynamoDbStorageConfigurationProvider does not exists by default"() {
expect:
!beanContext.containsBean(DynamoDbStorageConfigurationProvider)
}
}
Loading