Skip to content

Implement AerospikeGeneration annotation #182

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 4 commits into from
Jul 3, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.aerospike.mapper.annotations;

import com.aerospike.client.policy.GenerationPolicy;
import com.aerospike.client.policy.WritePolicy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Mark a field or property to be used for optimistic concurrency control using Aerospike's generation field.
* <p/>
* The field or property must be of Integer or int type. When reading an object which has a field marked
* with @AerospikeGeneration, the returned record's generation field will be mapped into the @AerospikeGeneration field.
* When writing the record, if the @AerospikeGeneration field is non-zero, the generation will be set in the
* {@link WritePolicy#generation} field and the {@link WritePolicy#generationPolicy} will be set to
* {@link GenerationPolicy#EXPECT_GEN_EQUAL}.
* <p/>
* Example usage:
* <pre>
* &#64;AerospikeRecord(namespace = "test", set = "account")
* public class Account {
* &#64;AerospikeKey
* private int id;
* &#64;AerospikeBin
* private String name;
* &#64;AerospikeGeneration
* private int generation;
* }
* </pre>
*
* @author timfaulkes
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface AerospikeGeneration {
}
9 changes: 9 additions & 0 deletions src/main/java/com/aerospike/mapper/tools/AeroMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.aerospike.client.Record;
import com.aerospike.client.Value;
import com.aerospike.client.policy.BatchPolicy;
import com.aerospike.client.policy.GenerationPolicy;
import com.aerospike.client.policy.Policy;
import com.aerospike.client.policy.QueryPolicy;
import com.aerospike.client.policy.RecordExistsAction;
Expand Down Expand Up @@ -121,6 +122,14 @@ private <T> WritePolicy generateWritePolicyFromObject(T object) {
if (sendKey != null) {
writePolicy.sendKey = sendKey;
}

// #181 Handle @AerospikeGeneration field for optimistic concurrency control
Integer generationValue = entry.getGenerationValue(object);
if (generationValue != null && generationValue > 0) {
writePolicy.generation = generationValue;
writePolicy.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL;

Choose a reason for hiding this comment

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

If for some reason, the class's write policy is set to GenerationPolicy.EXPECT_GEN_GT will we be overriding it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I have never found any reason to use EXPECT_GEN_GT, it makes no sense as generations wrap around.

Good catches on those couple of trailing references to version instead of generation!

}

return writePolicy;
}

Expand Down
498 changes: 329 additions & 169 deletions src/main/java/com/aerospike/mapper/tools/ClassCacheEntry.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.aerospike.client.Operation;
import com.aerospike.client.Value;
import com.aerospike.client.policy.BatchPolicy;
import com.aerospike.client.policy.GenerationPolicy;
import com.aerospike.client.policy.Policy;
import com.aerospike.client.policy.QueryPolicy;
import com.aerospike.client.policy.RecordExistsAction;
Expand Down Expand Up @@ -107,6 +108,14 @@ private <T> WritePolicy generateWritePolicyFromObject(T object) {
if (sendKey != null) {
writePolicy.sendKey = sendKey;
}

// #181 Handle @AerospikeGeneration field for optimistic concurrency control
Integer generationValue = entry.getGenerationValue(object);
if (generationValue != null && generationValue > 0) {
writePolicy.generation = generationValue;
writePolicy.generationPolicy = GenerationPolicy.EXPECT_GEN_EQUAL;
}

return writePolicy;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class BinConfig {
private Integer ordinal;
private EmbedConfig embed;
private ReferenceConfig reference;
private Boolean generation;

public String getName() {
return name;
Expand Down Expand Up @@ -51,7 +52,10 @@ public ReferenceConfig getReference() {
return reference;
}


public Boolean isGeneration() {
return generation;
}

public void setName(String name) {
this.name = name;
}
Expand Down Expand Up @@ -88,6 +92,10 @@ public void setReference(ReferenceConfig reference) {
this.reference = reference;
}

public void setGeneration(Boolean generation) {
this.generation = generation;
}

public void validate(String className) {
if (StringUtils.isBlank(this.name) && StringUtils.isBlank(this.field)) {
throw new AerospikeException("Configuration for class " + className + " defines a bin which contains neither a name nor a field");
Expand Down Expand Up @@ -129,6 +137,9 @@ public BinConfig merge(BinConfig other) {
if (this.reference == null && other.reference != null) {
this.reference = other.reference;
}
if (this.generation == null && other.generation != null) {
this.generation = other.generation;
}
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ public Builder beingEmbeddedAs(AerospikeEmbed.EmbedType type, AerospikeEmbed.Emb
return this.end();
}

public Builder asGenerationField() {
this.binConfig.setGeneration(true);
return this.end();
}

public Builder beingEmbeddedAs(AerospikeEmbed.EmbedType type, AerospikeEmbed.EmbedType elementType, boolean saveKey) {
EmbedConfig embedConfig = new EmbedConfig();
embedConfig.setType(type);
Expand Down
4 changes: 3 additions & 1 deletion src/test/java/com/aerospike/mapper/AeroMapperBaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.aerospike.client.policy.ClientPolicy;

import com.aerospike.client.AerospikeClient;
import com.aerospike.client.Host;
import com.aerospike.client.IAerospikeClient;
import com.aerospike.mapper.tools.ClassCache;
import com.fasterxml.jackson.core.JsonProcessingException;
Expand All @@ -25,7 +26,8 @@ public static void setupClass() {
ClientPolicy policy = new ClientPolicy();
// Set event loops to use in asynchronous commands.
policy.eventLoops = new NioEventLoops(1);
client = new AerospikeClient(policy, "localhost", 3000);
Host[] hosts = Host.parseHosts(System.getProperty("test.host", "localhost:3000"), 3000);
client = new AerospikeClient(policy, hosts);
}

@AfterAll
Expand Down
Loading
Loading