Skip to content

Commit 77b604a

Browse files
authored
fix(storage): 优化路径锁与语义刷新并发 (#2029)
* fix(storage): refine path lock semantic refresh concurrency Use exact path locks for source commits, tree locks only for lifecycle and schema scopes, and coalesce derived semantic writes to avoid stale summary overwrites under concurrent resource and memory updates. * chore: split benchmark changes into separate PR * chore: keep semantic refresh design notes out of docs * style: format lock changes * fix: preserve resource lifecycle locks * Revert "fix: preserve resource lifecycle locks" This reverts commit d2fb274. * fix(resource): simplify lifecycle locking * fix(queuefs): consolidate semantic sidecar writes
1 parent e80b0a0 commit 77b604a

46 files changed

Lines changed: 2425 additions & 891 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/en/api/03-filesystem.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ openviking stat viking://resources/my-project/docs
497497
}
498498
```
499499

500-
The `isLocked` field reports whether the path is currently held by a path lock — either the path itself has a valid `.path.ovlock`, or any ancestor directory holds a SUBTREE lock. Returns `false` when the LockManager is unavailable or the lookup fails, so callers can avoid attempting a write only to observe `ResourceBusyError`.
500+
The `isLocked` field reports whether the path is currently held by a path lock: the path itself has a valid lock (including an exact-path lock for the target), or any ancestor directory holds a TreeLock. Returns `false` when the LockManager is unavailable or the lookup fails, so callers can avoid attempting a write only to observe `ResourceBusyError`.
501501

502502
The `count` field (directories only) contains the estimated number of items (files and subdirectories) under this directory (from vector index).
503503

docs/en/concepts/09-transaction.md

Lines changed: 107 additions & 51 deletions
Large diffs are not rendered by default.

docs/zh/api/03-filesystem.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ openviking stat viking://resources/my-project/docs
497497
}
498498
```
499499

500-
`isLocked` 字段反映路径当前是否被 path lock 持有:路径自身存在有效的 `.path.ovlock`,或者任一祖先目录持有 SUBTREE 锁。当 LockManager 不可用或查询失败时返回 `false`,调用方可据此避免先写入再观察到 `ResourceBusyError`
500+
`isLocked` 字段反映路径当前是否被路径锁持有:路径自身存在有效锁(包括目标路径对应的 exact-path lock,或者任一祖先目录持有 TreeLock。当 LockManager 不可用或查询失败时返回 `false`,调用方可据此避免先写入再观察到 `ResourceBusyError`
501501

502502
`count` 字段(仅目录)包含该目录下的项目(文件和子目录)估计数量(来自向量索引)。
503503

docs/zh/concepts/09-transaction.md

Lines changed: 116 additions & 50 deletions
Large diffs are not rendered by default.

examples/opencode-plugin/lib/repo-context.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,3 @@ function formatRepoLine(item) {
6565
const abstract = item.abstract || item.overview
6666
return abstract ? `- **${name}** (${item.uri})\n ${abstract}` : `- **${name}** (${item.uri})`
6767
}
68-

openviking/async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ async def add_resource(
251251
reason: Context/reason for adding this resource.
252252
instruction: Specific instruction for processing.
253253
wait: If True, wait for processing to complete.
254-
to: Exact target URI (must not exist yet).
254+
to: Exact target URI. Existing targets keep the add_resource incremental-update behavior.
255255
parent: Target parent URI (must already exist).
256256
build_index: Whether to build vector index immediately (default: True).
257257
summarize: Whether to generate summary (default: False).

openviking/parse/tree_builder.py

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
99
v5.0 Architecture:
1010
1. Parser: parse + create directory structure in temp VikingFS
11-
2. TreeBuilder: move to AGFS + enqueue to SemanticQueue + create Resources
12-
3. SemanticProcessor: async generate L0/L1 + vectorize
11+
2. TreeBuilder: build final URI metadata and keep temp references
12+
3. ResourceProcessor: source commit from temp to final VikingFS path
13+
4. SemanticProcessor: async generate L0/L1 + vectorize
1314
1415
IMPORTANT (v5.0 Architecture):
1516
- Parser creates directory structure directly, no LLM calls
16-
- TreeBuilder moves files and enqueues to SemanticQueue
17+
- TreeBuilder does not move files; source commit is handled after URI metadata is built
1718
- SemanticProcessor handles all semantic generation asynchronously
1819
- Temporary directory approach eliminates memory pressure and enables concurrency
1920
- Resource objects are lightweight (no content fields)
@@ -40,18 +41,20 @@ class TreeBuilder:
4041
4142
New v5.0 Architecture:
4243
- Parser creates directory structure in temp VikingFS (no LLM calls)
43-
- TreeBuilder moves to AGFS + enqueues to SemanticQueue + creates Resources
44+
- TreeBuilder builds final URI metadata while preserving temp URIs
45+
- ResourceProcessor commits temp content to the final source path
4446
- SemanticProcessor handles semantic generation asynchronously
4547
4648
Process flow:
4749
1. Parser creates directory structure with files in temp VikingFS
48-
2. TreeBuilder.finalize_from_temp() moves to AGFS, enqueues to SemanticQueue, creates Resources
49-
3. SemanticProcessor generates .abstract.md and .overview.md asynchronously
50-
4. SemanticProcessor directly vectorizes and inserts to collection
50+
2. TreeBuilder.finalize_from_temp() returns final URI and temp URI metadata
51+
3. ResourceProcessor performs source commit with short path locks
52+
4. SemanticProcessor generates .abstract.md and .overview.md asynchronously
53+
5. SemanticProcessor directly vectorizes and inserts to collection
5154
5255
Key changes from v4.0:
5356
- Semantic generation moved from Parser to SemanticQueue
54-
- TreeBuilder enqueues directories for async processing
57+
- ResourceProcessor enqueues directories for async processing
5558
- Direct vectorization in SemanticProcessor (no EmbeddingQueue)
5659
"""
5760

@@ -75,31 +78,6 @@ def _get_base_uri(
7578
# Agent scope
7679
return "viking://agent"
7780

78-
async def _resolve_unique_uri(self, uri: str, max_attempts: int = 100) -> str:
79-
"""Return a URI that does not collide with an existing resource.
80-
81-
If *uri* is free, return it unchanged. Otherwise append ``_1``,
82-
``_2``, ... until a free name is found.
83-
"""
84-
viking_fs = get_viking_fs()
85-
86-
async def _exists(u: str) -> bool:
87-
try:
88-
await viking_fs.stat(u)
89-
return True
90-
except Exception:
91-
return False
92-
93-
if not await _exists(uri):
94-
return uri
95-
96-
for i in range(1, max_attempts + 1):
97-
candidate = f"{uri}_{i}"
98-
if not await _exists(candidate):
99-
return candidate
100-
101-
raise FileExistsError(f"Cannot resolve unique name for {uri} after {max_attempts} attempts")
102-
10381
# ============================================================================
10482
# v5.0 Methods (temporary directory + SemanticQueue architecture)
10583
# ============================================================================
@@ -116,10 +94,10 @@ async def finalize_from_temp(
11694
create_parent: bool = False,
11795
) -> "BuildingTree":
11896
"""
119-
Finalize processing by moving from temp to AGFS.
97+
Finalize URI metadata for a temp parse result.
12098
12199
Args:
122-
to_uri: Exact target URI (must not exist)
100+
to_uri: Exact target URI, or resources root to import under
123101
parent_uri: Target parent URI (must exist unless create_parent is True)
124102
create_parent: Whether to automatically create parent directory if it doesn't exist
125103
"""
@@ -171,6 +149,8 @@ def is_resources_root(uri: Optional[str]) -> bool:
171149
candidate_uri = to_uri
172150
else:
173151
effective_parent_uri = parent_uri or to_uri if use_to_as_parent else parent_uri
152+
if effective_parent_uri:
153+
effective_parent_uri = effective_parent_uri.rstrip("/")
174154
if effective_parent_uri:
175155
# Parent URI must exist and be a directory, or create it if requested
176156
try:
@@ -202,26 +182,16 @@ def is_resources_root(uri: Optional[str]) -> bool:
202182
base_uri = effective_parent_uri
203183
candidate_uri = VikingURI(base_uri).join(final_doc_name).uri
204184

205-
if to_uri and not use_to_as_parent:
206-
final_uri = candidate_uri
207-
elif use_to_as_parent:
208-
# Treat an explicit resources root target as "import under this
209-
# directory" while preserving the child URI so downstream logic can
210-
# incrementally update viking://resources/<child> when it exists.
211-
final_uri = candidate_uri
212-
else:
213-
final_uri = await self._resolve_unique_uri(candidate_uri)
214-
215185
tree = BuildingTree(
216186
source_path=source_path,
217187
source_format=source_format,
218188
)
219-
tree._root_uri = final_uri
189+
tree._root_uri = candidate_uri
220190
if not to_uri or use_to_as_parent:
221191
tree._candidate_uri = candidate_uri
222192

223193
# Create a minimal Context object for the root so that tree.root is not None
224-
root_context = Context(uri=final_uri, temp_uri=temp_doc_uri)
194+
root_context = Context(uri=candidate_uri, temp_uri=temp_doc_uri)
225195
tree.add_context(root_context)
226196

227197
return tree

openviking/server/error_mapping.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
AGFSHTTPError,
1212
AGFSTimeoutError,
1313
)
14-
from openviking.storage.errors import ResourceBusyError
14+
from openviking.storage.errors import LockAcquisitionError, ResourceBusyError
1515
from openviking_cli.exceptions import (
1616
ConflictError,
1717
FailedPreconditionError,
@@ -404,7 +404,21 @@ def map_exception(
404404
if isinstance(exc, OpenVikingError):
405405
return exc
406406
if isinstance(exc, ResourceBusyError):
407-
return ConflictError(str(exc), resource=resource)
407+
details: dict[str, Any] = {
408+
"resource": exc.uri or resource,
409+
"uri": exc.uri or resource,
410+
"conflict_type": exc.conflict_type,
411+
"retryable": exc.retryable,
412+
}
413+
return OpenVikingError(str(exc), code="CONFLICT", details=details)
414+
if isinstance(exc, LockAcquisitionError):
415+
details = {
416+
"resource": resource,
417+
"uri": resource,
418+
"conflict_type": "path_busy",
419+
"retryable": True,
420+
}
421+
return OpenVikingError(str(exc), code="CONFLICT", details=details)
408422
if isinstance(exc, PermissionError):
409423
return PermissionDeniedError(str(exc), resource=resource)
410424
if isinstance(exc, FileNotFoundError):

openviking/server/routers/filesystem.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,9 @@ async def mv(
207207
if "not found" in err_msg or "no such file or directory" in err_msg:
208208
raise NotFoundError(from_uri, "file")
209209
raise
210+
except Exception as exc:
211+
mapped = map_exception(exc, resource=from_uri)
212+
if mapped is not None:
213+
raise mapped from exc
214+
raise
210215
return Response(status="ok", result={"from": from_uri, "to": to_uri})

openviking/server/temp_upload_store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ async def _resolve_shared(
318318
ctx=internal_ctx,
319319
)
320320
handle = get_lock_manager().create_handle()
321-
acquired = await get_lock_manager().acquire_subtree(handle, lock_path, timeout=0.0)
321+
acquired = await get_lock_manager().acquire_tree(handle, lock_path, timeout=0.0)
322322
if not acquired:
323323
raise PermissionDeniedError("Temporary upload is being consumed.")
324324

0 commit comments

Comments
 (0)