fix: /api/diagnostics 500 « dictionary changed size during iteration » (snapshot des dicts avant itération)
This commit is contained in:
+14
-1
@@ -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).
|
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
|
> **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
|
## [2.25.0] — 2026-09-24
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
+3
-3
@@ -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.
|
**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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -975,8 +975,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 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*
|
||||||
|
|||||||
@@ -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.
|
**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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1150,8 +1150,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 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*
|
||||||
|
|||||||
+6
-3
@@ -4231,7 +4231,9 @@ async def api_diagnostics(current_user=Depends(require_admin)):
|
|||||||
vault_stats = {}
|
vault_stats = {}
|
||||||
total_files = 0
|
total_files = 0
|
||||||
total_tags = 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", []))
|
file_count = len(vdata.get("files", []))
|
||||||
tag_count = len(vdata.get("tags", {}))
|
tag_count = len(vdata.get("tags", {}))
|
||||||
vault_stats[vname] = {"file_count": file_count, "tag_count": tag_count}
|
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
|
total_tags += tag_count
|
||||||
|
|
||||||
# Memory estimate for inverted index
|
# 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(
|
mem_estimate_mb = round(
|
||||||
(sys.getsizeof(inv.word_index) + word_index_entries * 80
|
(sys.getsizeof(inv.word_index) + word_index_entries * 80
|
||||||
+ len(inv.doc_info) * 200
|
+ len(inv.doc_info) * 200
|
||||||
@@ -4253,7 +4256,7 @@ async def api_diagnostics(current_user=Depends(require_admin)):
|
|||||||
"vaults": vault_stats,
|
"vaults": vault_stats,
|
||||||
},
|
},
|
||||||
"inverted_index": {
|
"inverted_index": {
|
||||||
"unique_tokens": len(inv.word_index),
|
"unique_tokens": len(word_index),
|
||||||
"total_postings": word_index_entries,
|
"total_postings": word_index_entries,
|
||||||
"documents": inv.doc_count,
|
"documents": inv.doc_count,
|
||||||
"sorted_tokens": len(inv._sorted_tokens),
|
"sorted_tokens": len(inv._sorted_tokens),
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.25.0"
|
version = "2.25.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.25.0"
|
version = "2.25.1"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.25.0",
|
"version": "2.25.1",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# 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
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"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.",
|
"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",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -460,6 +460,39 @@ class TestConfig:
|
|||||||
# Should have some diagnostic info
|
# Should have some diagnostic info
|
||||||
assert len(data) > 0
|
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
|
# Dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user