{"record":{"id":"ddb6d704e9e99ac8","repo":"Tencent/WeKnora","slug":"concurrent-wiki-task-active","errorCode":null,"errorMessage":"concurrent wiki task active","messagePattern":"concurrent wiki task active","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"internal/application/service/wiki_ingest.go","lineNumber":36,"sourceCode":"\t\"github.com/Tencent/WeKnora/internal/models/chat\"\n\t\"github.com/Tencent/WeKnora/internal/searchutil\"\n\t\"github.com/Tencent/WeKnora/internal/tracing/langfuse\"\n\t\"github.com/Tencent/WeKnora/internal/types\"\n\t\"github.com/Tencent/WeKnora/internal/types/interfaces\"\n\t\"github.com/google/uuid\"\n\t\"github.com/hibiken/asynq\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"golang.org/x/sync/singleflight\"\n)\n\n// ErrWikiIngestConcurrent is returned by the wiki ingest handler in Lite mode\n// when another batch is already running for the same KB (the in-process\n// liteLocks guard is held). The asynq/sync executor's RetryDelayFunc uses\n// errors.Is on this sentinel to apply a short, fixed retry delay instead of\n// exponential backoff, so the deferred batch retries promptly once the active\n// one releases. Standard (Redis) mode no longer takes an exclusive per-KB\n// lock (Phase 3) and never returns this.\nvar ErrWikiIngestConcurrent = errors.New(\"concurrent wiki task active\")\n\nconst (\n\t// maxContentForWiki limits the document content sent to LLM for wiki generation\n\tmaxContentForWiki = 32768\n\n\t// --- Phase 3: concurrent per-KB batches (standard/Redis mode) ---------\n\t//\n\t// Phase 3 removed the exclusive per-KB Redis \"batch in progress\" lock\n\t// (wiki:active:<kbID>). Standard mode now allows concurrent batches for\n\t// one KB, kept safe by row claiming (claimPendingList) plus per-slug\n\t// reduce locks (withSlugLock). Lite mode still serializes per KB via the\n\t// in-process liteLocks map.\n\t//\n\t// wikiClaimStaleAfter is how long a claimed-but-undrained ingest row\n\t// waits before another worker may re-claim it. It MUST exceed the asynq\n\t// task Timeout for wiki:ingest (60m) so a still-running batch's rows are\n\t// never stolen mid-flight — only genuinely crashed/abandoned claims are\n\t// recovered. This preserves the pre-claim crash behaviour where a dead","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/application/service/wiki_ingest.go#L18-L54","documentation":"ErrWikiIngestConcurrent is a sentinel returned when a wiki ingest batch task starts for a knowledge base that already has an active batch (the in-process liteLocks guard is held). The asynq RetryDelayFunc matches it with errors.Is to apply a short fixed retry delay instead of exponential backoff, so the deferred batch retries promptly. Standard/Redis mode no longer takes an exclusive per-KB lock and never returns this.","triggerScenarios":"Processing a wiki ingest payload for a KB while ProcessWikiIngest (or the batch executor in wiki_ingest_batch.go) finds another batch still running for the same KnowledgeBaseID under liteLocks.","commonSituations":"Duplicate enqueue of wiki ingest tasks for the same KB (e.g. document updated twice quickly); a previous batch still running long (LLM calls) when a new trigger fires; multiple workers without distributed locking racing on one KB.","solutions":["No action needed — the executor retries with wikiIngestRetryDelay until the active batch releases the lock.","Reduce duplicate triggers by debouncing wiki ingest enqueues per KB.","If it persists, investigate why the active batch is slow or stuck (LLM timeouts) and check liteLocks cleanup on panic paths."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// executor RetryDelayFunc\nif errors.Is(err, service.ErrWikiIngestConcurrent) {\n    return wikiIngestRetryDelay // short fixed delay, not backoff\n}","preventionTips":["Debounce wiki ingest enqueues per knowledge base.","Monitor batch durations so long LLM runs don't stack up deferred retries.","Don't treat this error as a failure — it is an expected deferral signal."],"tags":["concurrency","async-tasks","locking","wiki"],"backgroundTag":"concurrent-task-conflict","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}