mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-16 21:00:03 +02:00
fix(kb): recreate a missing Qdrant container instead of spinning on Start Qdrant
The Knowledge Base's "Start Qdrant" button called start on a container that no longer existed. The server answered 200 with success: false, the modal ignored that flag, and the button polled the health check forever. Starting a dependency service whose container is gone now recreates it from its stored config through the normal install path. Its data lives on a host bind mount, so nothing is lost. The modal shows a refused start as an error, and stops waiting after five minutes if Qdrant never comes online. Fixes #1426
This commit is contained in:
parent
54ee30a449
commit
634e180edf
|
|
@ -5,6 +5,7 @@ import { inject } from '@adonisjs/core'
|
|||
import transmit from '@adonisjs/transmit/services/main'
|
||||
import { doResumableDownloadWithRetry } from '../utils/downloads.js'
|
||||
import { pickAmdHsaOverride } from '../utils/amd_hsa_override.js'
|
||||
import { recreatesOnStart } from '../utils/dependency_start.js'
|
||||
import { join } from 'path'
|
||||
import os from 'node:os'
|
||||
import env from '#start/env'
|
||||
|
|
@ -92,7 +93,7 @@ export class DockerService {
|
|||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const service = await Service.query().where('service_name', serviceName).first()
|
||||
if (!service || !service.installed) {
|
||||
if (!service) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Service ${serviceName} not found or not installed`,
|
||||
|
|
@ -101,6 +102,24 @@ export class DockerService {
|
|||
|
||||
const containers = await this.docker.listContainers({ all: true })
|
||||
const container = containers.find((c) => c.Names.includes(`/${serviceName}`))
|
||||
|
||||
if (recreatesOnStart(action, service.is_dependency_service, !!container)) {
|
||||
logger.info(`[DockerService] ${serviceName} has no container. Recreating it.`)
|
||||
// Preflight refuses a row still marked installed; the next sync would reset it anyway.
|
||||
if (service.installed) {
|
||||
service.installed = false
|
||||
await service.save()
|
||||
}
|
||||
return await this.createContainerPreflight(serviceName)
|
||||
}
|
||||
|
||||
if (!service.installed) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Service ${serviceName} not found or not installed`,
|
||||
}
|
||||
}
|
||||
|
||||
if (!container) {
|
||||
return {
|
||||
success: false,
|
||||
|
|
|
|||
18
admin/app/utils/dependency_start.ts
Normal file
18
admin/app/utils/dependency_start.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Whether a "start" request should recreate the service's container instead of
|
||||
* starting it (#1426).
|
||||
*
|
||||
* A hidden dependency service (Qdrant) has no card on the Apps page, so the
|
||||
* Knowledge Base's "Start Qdrant" button is the only control a user has over it.
|
||||
* When its container is gone there is nothing to start, and the next container sync
|
||||
* marks the row not installed, so the only way back is to recreate it from the
|
||||
* stored config. Its data lives on a host bind mount, so recreating it loses nothing.
|
||||
* Every other service keeps the old answer: the user reinstalls it from the Apps page.
|
||||
*/
|
||||
export function recreatesOnStart(
|
||||
action: 'start' | 'stop' | 'restart',
|
||||
isDependencyService: boolean,
|
||||
containerExists: boolean
|
||||
): boolean {
|
||||
return action === 'start' && isDependencyService && !containerExists
|
||||
}
|
||||
|
|
@ -55,6 +55,10 @@ const VIEWABLE_EXTENSIONS = new Set([
|
|||
'html',
|
||||
])
|
||||
|
||||
// How long "Start Qdrant" waits for the health check to come back online. Long
|
||||
// enough for a recreate that has to pull the image again.
|
||||
const QDRANT_START_TIMEOUT_MS = 5 * 60_000
|
||||
|
||||
function isViewableExtension(filename: string): boolean {
|
||||
const ext = filename.split('.').at(-1)?.toLowerCase() ?? ''
|
||||
return VIEWABLE_EXTENSIONS.has(ext)
|
||||
|
|
@ -228,6 +232,20 @@ export default function KnowledgeBaseModal({
|
|||
if (!qdrantOffline) setIsStartingQdrant(false)
|
||||
}, [qdrantOffline])
|
||||
|
||||
// Stop waiting on a Qdrant that started and then crashed, or whose recreate failed.
|
||||
useEffect(() => {
|
||||
if (!isStartingQdrant) return
|
||||
const timer = setTimeout(() => {
|
||||
setIsStartingQdrant(false)
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Qdrant did not come online. Check its logs under Settings → Service Logs & Metrics.',
|
||||
})
|
||||
}, QDRANT_START_TIMEOUT_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}, [isStartingQdrant])
|
||||
|
||||
const { data: storedFiles = [], isLoading: isLoadingFiles } = useQuery({
|
||||
queryKey: ['storedFiles'],
|
||||
queryFn: () => api.getStoredRAGFiles(),
|
||||
|
|
@ -399,7 +417,12 @@ export default function KnowledgeBaseModal({
|
|||
|
||||
const startQdrantMutation = useMutation({
|
||||
mutationFn: () => api.affectService(SERVICE_NAMES.QDRANT, 'start'),
|
||||
onSuccess: () => {
|
||||
onSuccess: (result) => {
|
||||
// A refused start comes back as success: false, not as a thrown error.
|
||||
if (!result?.success) {
|
||||
addNotification({ type: 'error', message: result?.message || 'Failed to start Qdrant.' })
|
||||
return
|
||||
}
|
||||
setIsStartingQdrant(true)
|
||||
queryClient.invalidateQueries({ queryKey: ['qdrantHealth'] })
|
||||
},
|
||||
|
|
|
|||
21
admin/tests/unit/dependency_start.spec.ts
Normal file
21
admin/tests/unit/dependency_start.spec.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import * as assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { recreatesOnStart } from '../../app/utils/dependency_start.js'
|
||||
|
||||
test('starting a dependency service whose container is gone recreates it', () => {
|
||||
assert.equal(recreatesOnStart('start', true, false), true)
|
||||
})
|
||||
|
||||
test('starting a dependency service whose container exists just starts it', () => {
|
||||
assert.equal(recreatesOnStart('start', true, true), false)
|
||||
})
|
||||
|
||||
test('stop and restart never recreate a missing dependency container', () => {
|
||||
assert.equal(recreatesOnStart('stop', true, false), false)
|
||||
assert.equal(recreatesOnStart('restart', true, false), false)
|
||||
})
|
||||
|
||||
test('starting a user-facing app with no container does not recreate it', () => {
|
||||
assert.equal(recreatesOnStart('start', false, false), false)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user