From a1d2457868d9b2e4485028446f6bc5e8a050f7ed Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 24 Sep 2026 17:05:27 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20/api/diagnostics=20500=20=C2=AB=20dictio?= =?UTF-8?q?nary=20changed=20size=20during=20iteration=20=C2=BB=20(snapshot?= =?UTF-8?q?=20des=20dicts=20avant=20it=C3=A9ration)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 ++++++++++++++- README.fr.md | 6 +++--- README.md | 6 +++--- VERSION | 2 +- backend/main.py | 9 ++++++--- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ROADMAP.md | 2 +- package.json | 2 +- tests/test_api_main.py | 33 +++++++++++++++++++++++++++++++++ 11 files changed, 65 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6932191..6dd4247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.25.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.25.1**. --- @@ -14,6 +14,19 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.25.1] — 2026-09-24 + +### Corrigé + +- **`/api/diagnostics` — erreur 500 « dictionary changed size during iteration ».** + Le calcul des statistiques d'index itérait `inv.word_index` et `index` en + direct, pendant que l'indexeur les modifiait depuis un autre thread (build au + démarrage, hooks incrémentaux) : l'itérateur de dict levait `RuntimeError` et + l'endpoint renvoyait 500. Les deux dicts sont désormais **copiés avant + itération** (copie atomique sous le GIL), ce qui supprime la course. + +--- + ## [2.25.0] — 2026-09-24 ### Ajouté diff --git a/README.fr.md b/README.fr.md index cde48b0..14d7839 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.25.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.25.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -975,8 +975,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.25.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.25.1). --- -*Projet : ObsiGate | Version : 2.25.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.25.1 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 4827147..fc26ea7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.25.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.25.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1150,8 +1150,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.25.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.25.1). --- -*Project: ObsiGate | Version: 2.25.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.25.1 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 5c18f91..d6af7e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.25.0 +2.25.1 diff --git a/backend/main.py b/backend/main.py index 9d4a0bf..aeb8b44 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4231,7 +4231,9 @@ async def api_diagnostics(current_user=Depends(require_admin)): vault_stats = {} total_files = 0 total_tags = 0 - for vname, vdata in index.items(): + # Snapshot both dicts first: the indexer mutates them from background + # threads, and iterating a live dict raises "dictionary changed size". + for vname, vdata in list(index.items()): file_count = len(vdata.get("files", [])) tag_count = len(vdata.get("tags", {})) vault_stats[vname] = {"file_count": file_count, "tag_count": tag_count} @@ -4239,7 +4241,8 @@ async def api_diagnostics(current_user=Depends(require_admin)): total_tags += tag_count # Memory estimate for inverted index - word_index_entries = sum(len(docs) for docs in inv.word_index.values()) + word_index = inv.word_index.copy() + word_index_entries = sum(len(docs) for docs in word_index.values()) mem_estimate_mb = round( (sys.getsizeof(inv.word_index) + word_index_entries * 80 + len(inv.doc_info) * 200 @@ -4253,7 +4256,7 @@ async def api_diagnostics(current_user=Depends(require_admin)): "vaults": vault_stats, }, "inverted_index": { - "unique_tokens": len(inv.word_index), + "unique_tokens": len(word_index), "total_postings": word_index_entries, "documents": inv.doc_count, "sorted_tokens": len(inv._sorted_tokens), diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 242b819..1079154 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.25.0" +version = "2.25.1" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 95829ac..0b1aca7 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.25.0" +version = "2.25.1" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 24498f7..f45a760 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.25.0", + "version": "2.25.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 99282a3..78d2cdf 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.25.0 | **Dernière mise à jour :** 2026-09-24 +> **Version :** 2.25.1 | **Dernière mise à jour :** 2026-09-24 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/package.json b/package.json index 49b8549..f6ac573 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.25.0", + "version": "2.25.1", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/test_api_main.py b/tests/test_api_main.py index 79fa9fa..61a6284 100644 --- a/tests/test_api_main.py +++ b/tests/test_api_main.py @@ -460,6 +460,39 @@ class TestConfig: # Should have some diagnostic info assert len(data) > 0 + def test_diagnostics_concurrent_index_writes(self, client): + """Regression: word_index mutated while the handler iterates it raised + "dictionary changed size during iteration" -> 500.""" + from backend.search import get_inverted_index + + inv = get_inverted_index() + original = inv.word_index + + class RaceDict(dict): + """dict whose .values() grows the dict mid-iteration, exactly like + the indexer writing from another thread.""" + def values(self): + it = iter(dict.values(self)) + first = next(it, _SENTINEL) + if first is _SENTINEL: + return iter(()) + self["__injected__"] = {"__injected_doc__": 1} + + def gen(): + yield first + yield from it # raises RuntimeError: dict changed size + return gen() + + _SENTINEL = object() + inv.word_index = RaceDict(original) + try: + resp = client.get("/api/diagnostics") + assert resp.status_code == 200, resp.text + assert "memory_estimate_mb" in resp.json()["inverted_index"] + finally: + inv.word_index = original + original.pop("__injected__", None) + # ═══════════════════════════════════════════════════════════════════ # Dashboard