siyuan-note/siyuan · warning
239
239
Error message
Conf.language(239)
What it means
Returned by model.RemoveBox when the per-notebook boxLock is already held (sync.Map LoadOrStore succeeds with loaded=true), meaning another mount/unmount/remove/reindex operation on the same notebook is in flight. Localized message Conf.language(239): "Related operations are being processed, please try again later". This is a transient busy signal, not a permanent failure.
Solutions
- Retry the removeNotebook call after the in-flight operation completes (short delay + bounded retries)
- Serialize operations per notebook ID in your client instead of issuing them concurrently
- Check for running tasks (e.g. /api/queryProgress or UI spinners) before issuing remove
Example fix
// before
err := model.RemoveBox(boxID) // may hit busy lock 239
// after
var err error
for i := 0; i < 5; i++ {
err = model.RemoveBox(boxID)
if err == nil || !strings.Contains(err.Error(), Conf.Language(239)) {
break
}
time.Sleep(500 * time.Millisecond)
} Defensive patterns
Strategy: retry
Try / catch
var err error
for attempt := 0; attempt < 5; attempt++ {
err = model.RemoveBox(boxID)
if err == nil {
break
}
if err.Error() != Conf.Language(239) { // busy lock is the only retryable case
break
}
time.Sleep(time.Duration(attempt+1) * 500 * time.Millisecond)
} Prevention
- Serialize notebook operations per boxID in your client with a per-key mutex
- Debounce duplicate UI clicks that fire concurrent remove/mount requests
- Retry only the busy (239) error; all other failures are permanent for that attempt
When it happens
Trigger: Concurrent API calls targeting the same notebook: removeNotebook while openNotebook/closeNotebook or a reindex is still running; rapid double-clicks in the UI firing two remove requests; automation retrying removal before the first attempt finishes.
Common situations: UI race when a user clicks delete immediately after opening/closing a notebook; scripts that fire mount and remove in parallel for the same box; slow disks making the first operation long enough to overlap the second.
Related errors
AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18).
Data as JSON: /api/errors/f2f56be850784329.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/mount.go:191
func collectBoxDeletedAttributeViewBlocks(boxID string) (ret map[string]map[string]struct{}, err error) {
rootIDs := treenode.GetRootBlockIDsByBoxID(boxID)
if 1 > len(rootIDs) {
return map[string]map[string]struct{}{}, nil
}
boundAVIDs, err := sql.QueryBoundBlockAVIDsInBox(nil, rootIDs, boxID)
if nil != err {
return nil, err
}
return groupDeletedAttributeViewBlocks(boundAVIDs), nil
}
func RemoveBox(boxID string) (err error) {
if !ast.IsNodeIDPattern(boxID) {
return errors.New("invalid notebook ID")
}
if _, loaded := boxLock.LoadOrStore(boxID, true); loaded {
err = errors.New(Conf.language(239))
return
}
defer boxLock.Delete(boxID)
if util.IsReservedFilename(boxID) {
return fmt.Errorf("can not remove [%s] caused by it is a reserved file", boxID)
}
FlushTxQueue()
sql.FlushQueue()
// 索引和笔记本目录删除后无法再读取 custom-avs,需提前收集;实际删除成功后再清理绑定行。
deletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)
if nil != err {
return fmt.Errorf("query database-bound blocks in notebook [%s] failed: %w", boxID, err)
}
isUserGuide := IsUserGuide(boxID)
localPath := filepath.Join(util.DataDir, boxID)
if !filelock.IsExist(localPath) {View on GitHub (pinned to afa823b6b4)