Skip to content

Commit 53060b0

Browse files
babltigamikereiche
authored andcommitted
Added support of the expression based durability levels. (#1721)
Closes #1063.
1 parent 372314a commit 53060b0

20 files changed

+238
-19
lines changed

src/main/asciidoc/entity.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ This key needs to be any string with a length of maximum 250 characters.
7878
Feel free to use whatever fits your use case, be it a UUID, an email address or anything else.
7979

8080
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.
81-
Default durability requirements can also be configured through the `@Document` annotation.
82-
For example: `@Document(durabilityLevel = DurabilityLevel.MAJORITY)` will force mutations to be replicated to a majority of the Data Service nodes.
81+
Default durability requirements can also be configured through the `@Document` or `@Durability` annotations.
82+
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).
8383
[[datatypes]]
8484
== Datatypes and Converters
8585

src/main/asciidoc/index.adoc

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
= Spring Data Couchbase - Reference Documentation
2-
Michael Nitschinger, Oliver Gierke, Simon Basle, Michael Reiche
2+
Michael Nitschinger, Oliver Gierke, Simon Basle, Michael Reiche, Tigran Babloyan
33
:revnumber: {version}
44
:revdate: {localdate}
55
:spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc
66

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

99
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.
1010

src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public <T> ExecutableInsertById<T> insertById(final Class<T> domainType) {
4040
Assert.notNull(domainType, "DomainType must not be null!");
4141
return new ExecutableInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
4242
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
43-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
43+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
4444
null);
4545
}
4646

src/main/java/org/springframework/data/couchbase/core/ExecutableMutateInByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public <T> ExecutableMutateInById<T> mutateInById(final Class<T> domainType) {
5151
Assert.notNull(domainType, "DomainType must not be null!");
5252
return new ExecutableMutateInByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
5353
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
54-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
54+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
5555
null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
5656
Collections.emptyList(), false);
5757
}

src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public ExecutableRemoveById removeById(Class<?> domainType) {
5252

5353
return new ExecutableRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
5454
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
55-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
55+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
5656
null);
5757
}
5858

src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public <T> ExecutableReplaceById<T> replaceById(final Class<T> domainType) {
4040
Assert.notNull(domainType, "DomainType must not be null!");
4141
return new ExecutableReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
4242
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
43-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
43+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
4444
null);
4545
}
4646

src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public <T> ExecutableUpsertById<T> upsertById(final Class<T> domainType) {
4040
Assert.notNull(domainType, "DomainType must not be null!");
4141
return new ExecutableUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
4242
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
43-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
43+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
4444
null);
4545
}
4646

src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public <T> ReactiveInsertById<T> insertById(final Class<T> domainType) {
6161
Assert.notNull(domainType, "DomainType must not be null!");
6262
return new ReactiveInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
6363
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
64-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
64+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
6565
null, template.support());
6666
}
6767

src/main/java/org/springframework/data/couchbase/core/ReactiveMutateInByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public <T> ReactiveMutateInById<T> mutateInById(final Class<T> domainType) {
5252
Assert.notNull(domainType, "DomainType must not be null!");
5353
return new ReactiveMutateInByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
5454
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
55-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
55+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
5656
null, template.support(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList(),
5757
Collections.emptyList(), false);
5858
}

src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public ReactiveRemoveById removeById() {
6767
public ReactiveRemoveById removeById(Class<?> domainType) {
6868
return new ReactiveRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
6969
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
70-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
70+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
7171
null);
7272
}
7373

src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public <T> ReactiveReplaceById<T> replaceById(final Class<T> domainType) {
6464
Assert.notNull(domainType, "DomainType must not be null!");
6565
return new ReactiveReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
6666
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
67-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
67+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
6868
null, template.support());
6969
}
7070

src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public <T> ReactiveUpsertById<T> upsertById(final Class<T> domainType) {
5353
Assert.notNull(domainType, "DomainType must not be null!");
5454
return new ReactiveUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType),
5555
OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType),
56-
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType),
56+
OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType, template.getConverter()),
5757
null, template.support());
5858
}
5959

src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentEntity.java

+36
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.TimeZone;
2222
import java.util.concurrent.TimeUnit;
2323

24+
import com.couchbase.client.core.msg.kv.DurabilityLevel;
2425
import org.springframework.context.EnvironmentAware;
2526
import org.springframework.core.annotation.AnnotatedElementUtils;
2627
import org.springframework.core.env.Environment;
@@ -36,6 +37,7 @@
3637
* @author Michael Nitschinger
3738
* @author Mark Paluch
3839
* @author Michael Reiche
40+
* @author Tigran Babloyan
3941
*/
4042
public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T, CouchbasePersistentProperty>
4143
implements CouchbasePersistentEntity<T>, EnvironmentAware {
@@ -50,6 +52,7 @@ public class BasicCouchbasePersistentEntity<T> extends BasicPersistentEntity<T,
5052
public BasicCouchbasePersistentEntity(final TypeInformation<T> typeInformation) {
5153
super(typeInformation);
5254
validateExpirationConfiguration();
55+
validateDurabilityConfiguration();
5356
}
5457

5558
private void validateExpirationConfiguration() {
@@ -61,6 +64,15 @@ private void validateExpirationConfiguration() {
6164
}
6265
}
6366

67+
private void validateDurabilityConfiguration() {
68+
Document annotation = getType().getAnnotation(Document.class);
69+
if (annotation != null && annotation.durabilityLevel() != DurabilityLevel.NONE && StringUtils.hasLength(annotation.durabilityExpression())) {
70+
String msg = String.format("Incorrect durability configuration on class %s using %s. "
71+
+ "You cannot use 'durabilityLevel' and 'durabilityExpression' at the same time", getType().getName(), annotation);
72+
throw new IllegalArgumentException(msg);
73+
}
74+
}
75+
6476
@Override
6577
public void setEnvironment(Environment environment) {
6678
this.environment = environment;
@@ -158,6 +170,30 @@ private static int getExpiryValue(Expiry annotation, Environment environment) {
158170
return expiryValue;
159171
}
160172

173+
@Override
174+
public DurabilityLevel getDurabilityLevel() {
175+
return getDurabilityLevel(AnnotatedElementUtils.findMergedAnnotation(getType(), Durability.class), environment);
176+
}
177+
178+
private static DurabilityLevel getDurabilityLevel(Durability annotation, Environment environment) {
179+
if (annotation == null) {
180+
return DurabilityLevel.NONE;
181+
}
182+
DurabilityLevel durabilityLevel = annotation.durabilityLevel();
183+
String durabilityExpressionString = annotation.durabilityExpression();
184+
if (StringUtils.hasLength(durabilityExpressionString)) {
185+
Assert.notNull(environment, "Environment must be set to use 'durabilityExpressionString'");
186+
String durabilityWithReplacedPlaceholders = environment.resolveRequiredPlaceholders(durabilityExpressionString);
187+
try {
188+
durabilityLevel = DurabilityLevel.valueOf(durabilityWithReplacedPlaceholders);
189+
} catch (IllegalArgumentException e) {
190+
throw new IllegalArgumentException(
191+
"Invalid value for durability expression: " + durabilityWithReplacedPlaceholders);
192+
}
193+
}
194+
return durabilityLevel;
195+
}
196+
161197
@Override
162198
public boolean isTouchOnRead() {
163199
org.springframework.data.couchbase.core.mapping.Document annotation = getType()

src/main/java/org/springframework/data/couchbase/core/mapping/CouchbasePersistentEntity.java

+11
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818

1919
import java.time.Duration;
2020

21+
import com.couchbase.client.core.msg.kv.DurabilityLevel;
2122
import org.springframework.data.mapping.PersistentEntity;
2223

2324
/**
2425
* Represents an entity that can be persisted which contains 0 or more properties.
2526
*
2627
* @author Michael Nitschinger
2728
* @author Michael Reiche
29+
* @author Tigran Babloyan
2830
*/
2931
public interface CouchbasePersistentEntity<T> extends PersistentEntity<T, CouchbasePersistentProperty> {
3032

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

59+
/**
60+
* Returns the durability level of the entity.
61+
* <p>
62+
* Allows the application to wait until this replication (or persistence) is successful before proceeding
63+
*
64+
* @return the durability level.
65+
*/
66+
DurabilityLevel getDurabilityLevel();
67+
5768
/**
5869
* Flag for using getAndTouch operations for reads, resetting the expiration (if one was set) when the entity is
5970
* directly read (eg. findOne, findById).

src/main/java/org/springframework/data/couchbase/core/mapping/Document.java

+12
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
@Target({ ElementType.TYPE })
4848
@Expiry
4949
@ScanConsistency
50+
@Durability
5051
public @interface Document {
5152

5253
/**
@@ -105,5 +106,16 @@
105106
* The optional durabilityLevel for all mutating operations, allows the application to wait until this replication
106107
* (or persistence) is successful before proceeding
107108
*/
109+
@AliasFor(annotation = Durability.class, attribute = "durabilityLevel")
108110
DurabilityLevel durabilityLevel() default DurabilityLevel.NONE;
111+
112+
/**
113+
* Same as {@link #durabilityLevel()} but allows the actual value to be set using standard Spring property sources mechanism.
114+
* Only one might be set at the same time: either {@link #durabilityLevel()} or {@link #durabilityExpression()}. <br />
115+
* Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}.
116+
* <br />
117+
* SpEL is NOT supported.
118+
*/
119+
@AliasFor(annotation = Durability.class, attribute = "durabilityExpression")
120+
String durabilityExpression() default "";
109121
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package org.springframework.data.couchbase.core.mapping;
2+
3+
import com.couchbase.client.core.msg.kv.DurabilityLevel;
4+
import org.springframework.data.annotation.Persistent;
5+
6+
import java.lang.annotation.*;
7+
8+
/**
9+
* Durability annotation
10+
*
11+
* @author Tigran Babloyan
12+
*/
13+
@Persistent
14+
@Inherited
15+
@Retention(RetentionPolicy.RUNTIME)
16+
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
17+
public @interface Durability {
18+
/**
19+
* The optional durabilityLevel for all mutating operations, allows the application to wait until this replication
20+
* (or persistence) is successful before proceeding
21+
*/
22+
DurabilityLevel durabilityLevel() default DurabilityLevel.NONE;
23+
24+
/**
25+
* Same as {@link #durabilityLevel()} but allows the actual value to be set using standard Spring property sources mechanism.
26+
* Only one might be set at the same time: either {@link #durabilityLevel()} or {@link #durabilityExpression()}. <br />
27+
* Syntax is the same as for {@link org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)}.
28+
* <br />
29+
* SpEL is NOT supported.
30+
*/
31+
String durabilityExpression() default "";
32+
}

src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
import org.slf4j.Logger;
3232
import org.slf4j.LoggerFactory;
3333
import org.springframework.core.annotation.AnnotatedElementUtils;
34+
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
3435
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
36+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
3537
import org.springframework.data.couchbase.core.mapping.Document;
3638
import org.springframework.data.couchbase.repository.Collection;
3739
import org.springframework.data.couchbase.repository.ScanConsistency;
@@ -285,12 +287,14 @@ public static String getScopeFrom(Class<?> domainType) {
285287
return null;
286288
}
287289

288-
public static DurabilityLevel getDurabilityLevel(Class<?> domainType) {
290+
public static DurabilityLevel getDurabilityLevel(Class<?> domainType, CouchbaseConverter converter) {
289291
if (domainType == null) {
290292
return DurabilityLevel.NONE;
291293
}
292-
Document document = AnnotatedElementUtils.findMergedAnnotation(domainType, Document.class);
293-
return document != null ? document.durabilityLevel() : DurabilityLevel.NONE;
294+
final CouchbasePersistentEntity<?> entity = converter.getMappingContext()
295+
.getRequiredPersistentEntity(domainType);
296+
297+
return entity.getDurabilityLevel();
294298
}
295299

296300
public static PersistTo getPersistTo(Class<?> domainType) {

src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import org.springframework.data.couchbase.util.ClusterType;
7070
import org.springframework.data.couchbase.util.IgnoreWhen;
7171
import org.springframework.data.couchbase.util.JavaIntegrationTests;
72+
import org.springframework.test.context.TestPropertySource;
7273
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
7374

7475
import com.couchbase.client.core.error.CouchbaseException;
@@ -92,6 +93,7 @@
9293
*/
9394
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
9495
@SpringJUnitConfig(Config.class)
96+
@TestPropertySource(properties = { "valid.document.durability = MAJORITY" })
9597
class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests {
9698

9799
@Autowired public CouchbaseTemplate couchbaseTemplate;
@@ -319,7 +321,8 @@ void findProjectingPath() {
319321
@Test
320322
void withDurability()
321323
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
322-
for (Class<?> clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) {
324+
for (Class<?> clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedDurabilityExpression.class,
325+
UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) {
323326
// insert, replace, upsert
324327
for (OneAndAllEntity<User> operator : new OneAndAllEntity[]{couchbaseTemplate.insertById(clazz),
325328
couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz)}) {
@@ -1156,6 +1159,31 @@ void insertByIdWithAnnotatedDurability2() {
11561159
couchbaseTemplate.removeById(UserAnnotatedDurability.class).one(user.getId());
11571160
}
11581161

1162+
@Test
1163+
void insertByIdWithAnnotatedDurabilityExpression() {
1164+
UserAnnotatedDurabilityExpression user = new UserAnnotatedDurabilityExpression(UUID.randomUUID().toString(), "firstname", "lastname");
1165+
UserAnnotatedDurabilityExpression inserted = null;
1166+
1167+
// occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests"
1168+
for (int i = 1; i != 5; i++) {
1169+
try {
1170+
inserted = couchbaseTemplate.insertById(UserAnnotatedDurabilityExpression.class)
1171+
.one(user);
1172+
break;
1173+
} catch (Exception ofe) {
1174+
System.out.println("" + i + " caught: " + ofe);
1175+
couchbaseTemplate.removeByQuery(UserAnnotatedDurabilityExpression.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all();
1176+
if (i == 4) {
1177+
throw ofe;
1178+
}
1179+
sleepSecs(1);
1180+
}
1181+
}
1182+
assertEquals(user, inserted);
1183+
assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(UserAnnotatedDurabilityExpression.class).one(user));
1184+
couchbaseTemplate.removeById(UserAnnotatedDurabilityExpression.class).one(user.getId());
1185+
}
1186+
11591187
@Test
11601188
void existsById() {
11611189
String id = UUID.randomUUID().toString();

0 commit comments

Comments
 (0)