fix: guard Corrections against async read from compose

all() returns List.copyOf(entries), which iterates; add()/load()
structurally modify. Once Task 10 wires all() into runTaskAsynchronously
and Task 11 wires add() from the main-thread /ia corrigir command,
the race throws ConcurrentModificationException. Guarded by entries'
own monitor like Conversations: file parse and YAML save stay outside
the lock, only fast in-memory work is under it.
This commit is contained in:
marcos
2026-08-06 03:12:27 +00:00
parent 5cfa580997
commit 7940a8fc66
@@ -22,6 +22,15 @@ final class Corrections {
} }
private final File file; private final File file;
/**
* Read by {@link #all()} from the async answer thread (Ai.compose) and
* mutated by {@link #add()}/{@link #load()} from the main thread (/ia
* corrigir). Guarded by its own monitor, like {@link Conversations}: only
* fast, in-memory work happens under the lock — file parsing and the YAML
* save stay outside it — so {@link #all()}'s {@code List.copyOf} never
* races a structural change and throws no
* {@code ConcurrentModificationException}.
*/
private final List<Entry> entries = new ArrayList<>(); private final List<Entry> entries = new ArrayList<>();
Corrections(File file) { Corrections(File file) {
@@ -30,26 +39,30 @@ final class Corrections {
} }
void load() { void load() {
entries.clear(); YamlConfiguration yaml = file.exists() ? YamlConfiguration.loadConfiguration(file) : null;
if (!file.exists()) { synchronized (entries) {
return; entries.clear();
} if (yaml == null) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); return;
for (String key : yaml.getKeys(false)) { }
String question = yaml.getString(key + ".pergunta"); for (String key : yaml.getKeys(false)) {
String answer = yaml.getString(key + ".resposta"); String question = yaml.getString(key + ".pergunta");
if (question != null && answer != null) { String answer = yaml.getString(key + ".resposta");
entries.add(new Entry(question, answer)); if (question != null && answer != null) {
entries.add(new Entry(question, answer));
}
} }
} }
} }
void add(String question, String answer) { void add(String question, String answer) {
entries.add(new Entry(question, answer));
YamlConfiguration yaml = new YamlConfiguration(); YamlConfiguration yaml = new YamlConfiguration();
for (int i = 0; i < entries.size(); i++) { synchronized (entries) {
yaml.set("c" + i + ".pergunta", entries.get(i).question()); entries.add(new Entry(question, answer));
yaml.set("c" + i + ".resposta", entries.get(i).answer()); for (int i = 0; i < entries.size(); i++) {
yaml.set("c" + i + ".pergunta", entries.get(i).question());
yaml.set("c" + i + ".resposta", entries.get(i).answer());
}
} }
try { try {
yaml.save(file); yaml.save(file);
@@ -59,7 +72,9 @@ final class Corrections {
} }
List<Entry> all() { List<Entry> all() {
return List.copyOf(entries); synchronized (entries) {
return List.copyOf(entries);
}
} }
/** Corrections sharing at least one significant word with the question. */ /** Corrections sharing at least one significant word with the question. */