agentscope-ai / agentscope-ai/agentscope-java

[Bug]: InMemoryAgentStateStore unconditional save can return another concurrent write's version

Aberta
#3,076 1 comentário 0 reações 0 responsáveis Ver no GitHub
area/core
Linguagem predominante
Java
Estrelas
5.6k
Forks
1.3k
Merge médio
4d 12h
PRs com merge (30d)
77

Descrição

## Describe the bug

`InMemoryAgentStateStore.saveIfVersion(..., UNVERSIONED)` writes the state through `save()` and then separately reads the current entry's version. Another writer can commit between those operations, so the first caller receives the second writer's version rather than the version of its own write.

This violates the method's documented successful-write return contract and can defeat a subsequent compare-and-swap: a caller whose state was already overwritten can reuse the incorrectly returned newer version and overwrite that newer state without detecting a conflict.

The race is in the unconditional branch at [InMemoryAgentStateStore.java](https://github.com/agentscope-ai/agentscope-java/blob/2b378e158ce1e555cd98c67d114d43cbc316af73/agentscope-core/src/main/java/io/agentscope/core/state/InMemoryAgentStateStore.java#L93). `setSingleState()` synchronizes the write, but the later version read occurs outside that critical section.

## To reproduce

Run the Java program below against the current `agentscope-core` source or put it on a classpath containing that module. It uses the unmodified store, no mocks or external services. The exact duplicate count depends on scheduling.

```java
import io.agentscope.core.state.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class UnconditionalVersionRepro {
record Value(String text) implements State {}

public static void main(String[] args) throws Exception {
var store = new InMemoryAgentStateStore();
var versions = ConcurrentHashMap.newKeySet();
var duplicates = new AtomicInteger();
var start = new CountDownLatch(1);
var pool = Executors.newFixedThreadPool(8);
try {
var tasks = new java.util.ArrayList>();
for (int worker = 0; worker < 8; worker++) {
int id = worker;
tasks.add(pool.submit(() -> {
start.await();
for (int i = 0; i < 1000; i++) {
long version = store.saveIfVersion("u", "s", "k",
new Value(id + ":" + i), AgentStateStore.UNVERSIONED);
if (!versions.add(version)) duplicates.incrementAndGet();
}
return null;
}));
}
start.countDown();
for (var task : tasks) task.get(15, TimeUnit.SECONDS);
System.out.printf("writes=8000 distinct_returned_versions=%d duplicate_versions=%d stored_version=%d%n",
versions.size(), duplicates.get(), store.getVersioned("u", "s", "k", Value.class).version());
if (duplicates.get() != 0) throw new AssertionError("Successful writes returned duplicate versions");
} finally {
pool.shutdownNow();
}
}
}
```

Observed on the unmodified checkout:

```text
writes=8000 distinct_returned_versions=5437 duplicate_versions=2563 stored_version=8000
java.lang.AssertionError: Successful writes returned duplicate versions
```

The interleaving is also reproducible deterministically in a JUnit test by placing a two-party barrier immediately after `super.save()` in a test-only subclass: both unconditional callers then return version 2. This only controls thread timing; the stress reproduction above uses the ordinary store.

## Expected behavior

Each successful unconditional save returns the version assigned to that particular write. With two writes to an initially absent key, the returned versions must be 1 and 2; the caller with version 1 must then fail a CAS after version 2 has been written.

## Environment

- AgentScope Java: main at `2b378e158ce1e555cd98c67d114d43cbc316af73` (`2.0.3-SNAPSHOT`)
- Java: Eclipse Temurin 17.0.15
- OS: Windows 11
- No model API, Redis, or network service is required for reproduction.

## Proposed fix and validation

Return the assigned version within the same per-session synchronized operation that writes the entry. The existing CAS helper can also handle `UNVERSIONED` by skipping the expected-version comparison and incrementing the current version.

The existing `AgentStateStoreVersioningContractTest` passes all 6 tests on the baseline. A new controlled-interleaving regression fails on the baseline because both writes report version 2. I plan to contribute a focused fix and regression test for this issue.

This report and reproduction were prepared with AI assistance. The commands and results above were executed locally against the referenced checkout.

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.