Skip to content

[Issue] [Java Client] Removing multiple list elements by index in a single operate call causes incorrect deletions due to shifting indexes #496

@rkyshz

Description

@rkyshz

Description

When attempting to remove multiple elements from a list bin in Aerospike by specifying multiple indexes in a single operate() call, the removal does not behave as expected. It appears that Aerospike executes the individual remove operations sequentially, and since each removal shifts the list indexes, subsequent removals end up deleting incorrect elements.

Steps to reproduce:

Create a record with a list bin containing multiple string items, e.g. ["AAAA", "BBBB", "CCCC", "DDDD"]
Attempt to remove multiple indexes (e.g. 0 and 1) in a single client.operate() call with ListOperation.removeByIndex()
Verify that the intended items were removed

Expected behaviour:

Both specified indexes remove the corresponding items correctly (e.g. indexes 0 and 1 remove "AAAA" and "BBBB").

Actual behavior:

The first removal shifts the list, so the second removal deletes the wrong item or fails.

Likely cause:

Aerospike performs the list operations sequentially on the server without adjusting for index changes after each removal, causing index shifts to invalidate subsequent removals.

Versions Tested

JDK - 21
Aerospike Client - 6.1.7 & 8.0.0
Aerospike Server - aerospike-server-enterprise:6.2.0.1 (Docker)

Sample Code

import com.aerospike.client.*;
import com.aerospike.client.Record;
import com.aerospike.client.cdt.ListOperation;
import com.aerospike.client.cdt.ListReturnType;
import com.aerospike.client.policy.WritePolicy;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.Serializable;
import java.util.List;
import java.util.stream.Stream;

/**
 * @author rkyshz
 * @Date 11/09/25
 */
@Log4j2
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@SuppressWarnings({"unchecked"})
class AerospikeListDemo {

    private static AerospikeClient aerospikeClient;
    private static final String SET_NAME = "list_demo";
    private static final String NAMESPACE = "test";
    private static final String BIN_NAME = "data";
    private static final String PK = "rec001";
    private static final List<String> ITEMS = List.of("AAAA", "BBBB", "CCCC", "DDDD");

    @BeforeEach
    void setup() {
        aerospikeClient = new AerospikeClient("localhost", 3000);
        aerospikeClient.delete(getDurableDeleteWritePolicy(), new Key(NAMESPACE, SET_NAME, PK));
        appendToList(NAMESPACE, SET_NAME, PK, BIN_NAME, ITEMS);
    }


    @ParameterizedTest
    @MethodSource("removalIndicesProvider")
    void shouldRemoveItems(List<Integer> indicesToRemove) {
        try {
            removeFromListByIndex(NAMESPACE, SET_NAME, PK, BIN_NAME, indicesToRemove);

            List<String> afterDelete = (List<String>) getRecord(NAMESPACE, SET_NAME, PK).getList("data");

            for (int index : indicesToRemove) {
                String removedItem = ITEMS.get(index);
                Assertions.assertFalse(afterDelete.contains(removedItem),
                        removedItem + " should not be present");
            }
        } catch (Exception e) {
            log.error(e);
            Assertions.fail(e);
        }
    }

    static Stream<Arguments> removalIndicesProvider() {
        return Stream.of(
                Arguments.of(List.of(0,1)),
                Arguments.of(List.of(0,2)),
                Arguments.of(List.of(1,3)),
                Arguments.of(List.of(1,2)),
                Arguments.of(List.of(0,3)),
                Arguments.of(List.of(2,3)),
                Arguments.of(List.of(0)),
                Arguments.of(List.of(3))
        );
    }

    public <T extends Serializable> void appendToList(String namespace, String set, String pk,
                                                      String bin, List<T> value) {
        aerospikeClient.operate(getDurableDeleteWritePolicy(), new Key(namespace, set, pk),
                ListOperation.appendItems(bin,
                        value.stream().map(Value::get).toList()));
    }

    public <T extends Serializable> void removeFromListByIndex(String namespace, String set, String pk,
                                                               String bin, List<Integer> indices) {
        aerospikeClient.operate(getDurableDeleteWritePolicy(), new Key(namespace, set, pk),
                indices.stream().map(index -> ListOperation.removeByIndex(bin, index, ListReturnType.VALUE)).toArray(Operation[]::new));
    }

    public <T extends Serializable> Record getRecord(String namespace, String set, String pk) {
        return aerospikeClient.get(null, new Key(namespace, set, pk));
    }

    private static WritePolicy getDurableDeleteWritePolicy() {
        WritePolicy writePolicy = new WritePolicy();
        writePolicy.durableDelete = true;
        writePolicy.sendKey = true;
        return writePolicy;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions