Skip to content

Expression Based Durability Levels #1721

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

Merged
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/asciidoc/entity.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ This key needs to be any string with a length of maximum 250 characters.
Feel free to use whatever fits your use case, be it a UUID, an email address or anything else.

Writes to Couchbase-Server buckets can optionally be assigned durability requirements; which instruct Couchbase Server to update the specified document on multiple nodes in memory and/or disk locations across the cluster; before considering the write to be committed.
Default durability requirements can also be configured through the `@Document` annotation.
For example: `@Document(durabilityLevel = DurabilityLevel.MAJORITY)` will force mutations to be replicated to a majority of the Data Service nodes.
Default durability requirements can also be configured through the `@Document` or `@Durability` annotations.
For example: `@Document(durabilityLevel = DurabilityLevel.MAJORITY)` will force mutations to be replicated to a majority of the Data Service nodes. Both of the annotations support expression based durability level assignment via `durabilityExpression` attribute (Note SPEL is not supported).
[[datatypes]]
== Datatypes and Converters

Expand Down
4 changes: 2 additions & 2 deletions src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
= Spring Data Couchbase - Reference Documentation
Michael Nitschinger, Oliver Gierke, Simon Basle, Michael Reiche
Michael Nitschinger, Oliver Gierke, Simon Basle, Michael Reiche, Tigran Babloyan
:revnumber: {version}
:revdate: {localdate}
:spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc

(C) 2014-2022 The original author(s).
(C) 2014-2023 The original author(s).

NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public <T> ExecutableInsertById<T> insertById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public <T> ExecutableMutateInById<T> mutateInById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableMutateInByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public ExecutableRemoveById removeById(Class<?> domainType) {

return new ExecutableRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public <T> ExecutableReplaceById<T> replaceById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public <T> ExecutableUpsertById<T> upsertById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public <T> ReactiveInsertById<T> insertById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null, template.support());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public <T> ReactiveMutateInById<T> mutateInById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveMutateInByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null, template.support(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public ReactiveRemoveById removeById() {
public ReactiveRemoveById removeById(Class<?> domainType) {
return new ReactiveRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public <T> ReactiveReplaceById<T> replaceById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null, template.support());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public <T> ReactiveUpsertById<T> upsertById(final Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
null, template.support());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

import com.couchbase.client.core.msg.kv.DurabilityLevel;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.env.Environment;
Expand All @@ -36,6 +37,7 @@
* @author Michael Nitschinger
* @author Mark Paluch
* @author Michael Reiche
* @author Tigran Babloyan
*/
public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T, CouchbasePersistentProperty>
implements CouchbasePersistentEntity<T>, EnvironmentAware {
Expand All @@ -50,6 +52,7 @@ public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T,
public BasicCouchbasePersistentEntity(final TypeInformation<T> typeInformation) {
super(typeInformation);
validateExpirationConfiguration();
validateDurabilityConfiguration();
}

private void validateExpirationConfiguration() {
Expand All @@ -61,6 +64,15 @@ private void validateExpirationConfiguration() {
}
}

private void validateDurabilityConfiguration() {
Document annotation = getType().getAnnotation(Document.class);
if (annotation != null && annotation.durabilityLevel() != DurabilityLevel.NONE && StringUtils.hasLength(annotation.durabilityExpression())) {
String msg = String.format("Incorrect durability configuration on class %s using %s. "
+ "You cannot use 'durabilityLevel' and 'durabilityExpression' at the same time", getType().getName(), annotation);
throw new IllegalArgumentException(msg);
}
}

@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
Expand Down Expand Up @@ -158,6 +170,30 @@ private static int getExpiryValue(Expiry annotation, Environment environment) {
return expiryValue;
}

@Override
public DurabilityLevel getDurabilityLevel() {
return getDurabilityLevel(AnnotatedElementUtils.findMergedAnnotation(getType(), Durability.class), environment);
}

private static DurabilityLevel getDurabilityLevel(Durability annotation, Environment environment) {
if (annotation == null) {
return DurabilityLevel.NONE;
}
DurabilityLevel durabilityLevel = annotation.durabilityLevel();
String durabilityExpressionString = annotation.durabilityExpression();
if (StringUtils.hasLength(durabilityExpressionString)) {
Assert.notNull(environment, "Environment must be set to use 'durabilityExpressionString'");
String durabilityWithReplacedPlaceholders = environment.resolveRequiredPlaceholders(durabilityExpressionString);
try {
durabilityLevel = DurabilityLevel.valueOf(durabilityWithReplacedPlaceholders);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Invalid value for durability expression: " + durabilityWithReplacedPlaceholders);
}
}
return durabilityLevel;
}

@Override
public boolean isTouchOnRead() {
org.springframework.data.couchbase.core.mapping.Document annotation = getType()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@

import java.time.Duration;

import com.couchbase.client.core.msg.kv.DurabilityLevel;
import org.springframework.data.mapping.PersistentEntity;

/**
* Represents an entity that can be persisted which contains 0 or more properties.
*
* @author Michael Nitschinger
* @author Michael Reiche
* @author Tigran Babloyan
*/
public interface CouchbasePersistentEntity<T> extends PersistentEntity<T, CouchbasePersistentProperty> {

Expand Down Expand Up @@ -54,6 +56,15 @@ public interface CouchbasePersistentEntity<T> extends PersistentEntity<T, Couchb
*/
Duration getExpiryDuration();

/**
* Returns the durability level of the entity.
* <p>
* Allows the application to wait until this replication (or persistence) is successful before proceeding
*
* @return the durability level.
*/
DurabilityLevel getDurabilityLevel();

/**
* Flag for using getAndTouch operations for reads, resetting the expiration (if one was set) when the entity is
* directly read (eg. findOne, findById).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
@Target({ ElementType.TYPE })
@Expiry
@ScanConsistency
@Durability
public @interface Document {

/**
Expand Down Expand Up @@ -105,5 +106,16 @@
* The optional durabilityLevel for all mutating operations, allows the application to wait until this replication
* (or persistence) is successful before proceeding
*/
@AliasFor(annotation = Durability.class, attribute = "durabilityLevel")
DurabilityLevel durabilityLevel() default DurabilityLevel.NONE;

/**
* Same as {@link #durabilityLevel()} but allows the actual value to be set using standard Spring property sources mechanism.
* Only one might be set at the same time: either {@link #durabilityLevel()} or {@link #durabilityExpression()}. <br />
* Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}.
* <br />
* SpEL is NOT supported.
*/
@AliasFor(annotation = Durability.class, attribute = "durabilityExpression")
String durabilityExpression() default "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.springframework.data.couchbase.core.mapping;

import com.couchbase.client.core.msg.kv.DurabilityLevel;
import org.springframework.data.annotation.Persistent;

import java.lang.annotation.*;

/**
* Durability annotation
*
* @author Tigran Babloyan
*/
@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
public @interface Durability {
/**
* The optional durabilityLevel for all mutating operations, allows the application to wait until this replication
* (or persistence) is successful before proceeding
*/
DurabilityLevel durabilityLevel() default DurabilityLevel.NONE;

/**
* Same as {@link #durabilityLevel()} but allows the actual value to be set using standard Spring property sources mechanism.
* Only one might be set at the same time: either {@link #durabilityLevel()} or {@link #durabilityExpression()}. <br />
* Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}.
* <br />
* SpEL is NOT supported.
*/
String durabilityExpression() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.ScanConsistency;
Expand Down Expand Up @@ -284,12 +286,14 @@ public static String getScopeFrom(Class<?> domainType) {
return null;
}

public static DurabilityLevel getDurabilityLevel(Class<?> domainType) {
public static DurabilityLevel getDurabilityLevel(Class<?> domainType, CouchbaseConverter converter) {
if (domainType == null) {
return DurabilityLevel.NONE;
}
Document document = AnnotatedElementUtils.findMergedAnnotation(domainType, Document.class);
return document != null ? document.durabilityLevel() : DurabilityLevel.NONE;
final CouchbasePersistentEntity<?> entity = converter.getMappingContext()
.getRequiredPersistentEntity(domainType);

return entity.getDurabilityLevel();
}

public static PersistTo getPersistTo(Class<?> domainType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.data.couchbase.util.JavaIntegrationTests;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import com.couchbase.client.core.error.CouchbaseException;
Expand All @@ -63,6 +64,7 @@
*/
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
@SpringJUnitConfig(Config.class)
@TestPropertySource(properties = { "valid.document.durability = MAJORITY" })
class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {

@Autowired public CouchbaseTemplate couchbaseTemplate;
Expand Down Expand Up @@ -290,7 +292,8 @@ void findProjectingPath() {
@Test
void withDurability()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
for (Class<?> clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) {
for (Class<?> clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedDurabilityExpression.class,
UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) {
// insert, replace, upsert
for (OneAndAllEntity<User> operator : new OneAndAllEntity[]{couchbaseTemplate.insertById(clazz),
couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz)}) {
Expand Down Expand Up @@ -1132,6 +1135,31 @@ void insertByIdWithAnnotatedDurability2() {
couchbaseTemplate.removeById(UserAnnotatedDurability.class).one(user.getId());
}

@Test
void insertByIdWithAnnotatedDurabilityExpression() {
UserAnnotatedDurabilityExpression user = new UserAnnotatedDurabilityExpression(UUID.randomUUID().toString(), "firstname", "lastname");
UserAnnotatedDurabilityExpression inserted = null;

// occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests"
for (int i = 1; i != 5; i++) {
try {
inserted = couchbaseTemplate.insertById(UserAnnotatedDurabilityExpression.class)
.one(user);
break;
} catch (Exception ofe) {
System.out.println("" + i + " caught: " + ofe);
couchbaseTemplate.removeByQuery(UserAnnotatedDurabilityExpression.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
if (i == 4) {
throw ofe;
}
sleepSecs(1);
}
}
assertEquals(user, inserted);
assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(UserAnnotatedDurabilityExpression.class).one(user));
couchbaseTemplate.removeById(UserAnnotatedDurabilityExpression.class).one(user.getId());
}

@Test
void existsById() {
String id = UUID.randomUUID().toString();
Expand Down
Loading