@@ -223,6 +223,24 @@ function findStartedImport(context: KnowledgeImportUploadContext) {
223223 } )
224224}
225225
226+ /** What the runner needs, with usage admitted for the caller completing the upload. */
227+ async function importPayload (
228+ principal : Principal ,
229+ context : KnowledgeImportUploadContext
230+ ) : Promise < KnowledgeImportPayload > {
231+ const { billingAttribution, userId } = await assertKnowledgeUsageAdmitted ( principal , context )
232+ const { upload } = context
233+ const name = importedName ( upload )
234+ return {
235+ importId : upload . id ,
236+ workspaceId : context . workspaceId ,
237+ userId,
238+ ...( name ? { name } : { } ) ,
239+ sourceKey : upload . storageKey ,
240+ billingAttribution,
241+ }
242+ }
243+
226244/**
227245 * Writes the durable record while the upload is being completed, before it
228246 * counts as complete. From then on the stale-import janitor owns the bundle, so
@@ -235,34 +253,27 @@ async function recordKnowledgeImport(
235253 principal : Principal ,
236254 context : KnowledgeImportUploadContext
237255) : Promise < KnowledgeImportPayload | null > {
238- const { billingAttribution, userId } = await assertKnowledgeUsageAdmitted ( principal , context )
239- const { upload } = context
256+ const payload = await importPayload ( principal , context )
240257 const inserted = await insertKnowledgeImport ( {
241- id : upload . id ,
242- workspaceId : context . workspaceId ,
243- userId,
244- sourceKey : upload . storageKey ,
258+ id : payload . importId ,
259+ workspaceId : payload . workspaceId ,
260+ userId : payload . userId ,
261+ sourceKey : payload . sourceKey ,
262+ } )
263+ return inserted ? payload : null
264+ }
265+
266+ function startKnowledgeImport ( payload : KnowledgeImportPayload ) : Promise < void > {
267+ return dispatchBackgroundTask < typeof knowledgeImportTask > ( 'knowledge-import' , payload , {
268+ runInProcess : runKnowledgeImport ,
269+ trigger : { tags : [ `workspaceId:${ payload . workspaceId } ` , `importId:${ payload . importId } ` ] } ,
245270 } )
246- if ( ! inserted ) return null
247- const name = importedName ( upload )
248- return {
249- importId : upload . id ,
250- knowledgeBaseId : generateId ( ) ,
251- workspaceId : context . workspaceId ,
252- userId,
253- ...( name ? { name } : { } ) ,
254- sourceKey : upload . storageKey ,
255- billingAttribution,
256- }
257271}
258272
259273/** Starts the runner; a dispatch that fails settles the import and drops its bundle. */
260274async function dispatchKnowledgeImport ( payload : KnowledgeImportPayload ) : Promise < void > {
261275 try {
262- await dispatchBackgroundTask < typeof knowledgeImportTask > ( 'knowledge-import' , payload , {
263- runInProcess : runKnowledgeImport ,
264- trigger : { tags : [ `workspaceId:${ payload . workspaceId } ` , `importId:${ payload . importId } ` ] } ,
265- } )
276+ await startKnowledgeImport ( payload )
266277 } catch ( error ) {
267278 await finishKnowledgeImport ( payload . importId , {
268279 status : 'failed' ,
@@ -280,6 +291,29 @@ async function dispatchKnowledgeImport(payload: KnowledgeImportPayload): Promise
280291 }
281292}
282293
294+ /**
295+ * Dispatches again an import whose record no worker has claimed: its first
296+ * dispatch was lost with the request that wrote the record, or is still
297+ * queued. Either way a second run is safe, since only one worker can claim it,
298+ * and a failure here leaves the import to the stale-import janitor rather than
299+ * failing one that may already be on its way.
300+ */
301+ async function redispatchUnclaimedImport (
302+ principal : Principal ,
303+ context : KnowledgeImportUploadContext ,
304+ record : KnowledgeImportRecord
305+ ) : Promise < void > {
306+ if ( record . status !== 'running' || record . startedAt ) return
307+ try {
308+ await startKnowledgeImport ( await importPayload ( principal , context ) )
309+ } catch ( error ) {
310+ logger . warn ( 'Could not dispatch an unclaimed knowledge import again' , {
311+ importId : record . id ,
312+ error : getErrorMessage ( error , 'Unknown error' ) ,
313+ } )
314+ }
315+ }
316+
283317export const createKnowledgeImport = defineAuthorizedKnowledgeUseCase ( {
284318 operation : knowledgeOperations . importCreate ,
285319 resolveContext : ( { input } : { input : CreateKnowledgeImportInput } ) =>
@@ -367,7 +401,10 @@ export const completeKnowledgeImport = defineAuthorizedKnowledgeUseCase({
367401 } ) => resolveUploadContext ( principal , input ) ,
368402 async execute ( { principal, context } ) : Promise < CompleteKnowledgeImportResult > {
369403 const existing = await findStartedImport ( context )
370- if ( existing ) return { import : fromRecord ( existing ) , startedKnowledgeBaseId : null }
404+ if ( existing ) {
405+ await redispatchUnclaimedImport ( principal , context , existing )
406+ return { import : fromRecord ( existing ) , startedKnowledgeBaseId : null }
407+ }
371408 let started : KnowledgeImportPayload | null
372409 try {
373410 const completed = await completeUploadSession ( {
@@ -405,7 +442,7 @@ export const completeKnowledgeImport = defineAuthorizedKnowledgeUseCase({
405442 importId : context . upload . id ,
406443 assertedWorkspaceId : context . workspaceId ,
407444 } )
408- return { import : fromRecord ( record ) , startedKnowledgeBaseId : started ?. knowledgeBaseId ?? null }
445+ return { import : fromRecord ( record ) , startedKnowledgeBaseId : started ?. importId ?? null }
409446 } ,
410447 /** Recorded once per import: a repeated completion only reads back the one already started. */
411448 projectAudit : ( { context, result } ) =>
0 commit comments